cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.65.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/comments/comments.go

338lines · modecode

1package comments
2
3import (
4 "bufio"
5 "fmt"
6 "strings"
7 "time"
8 "unicode"
9)
10
11type Type uint8
12
13const (
14 UnknownType Type = iota
15 InvalidComment
16 IgnoreFileType // ignore/file
17 IgnoreLineType // ignore/line
18 IgnoreBeginType // ignore/begin
19 IgnoreEndType // ignore/end
20 IgnoreNextLineType // ignore/next-line
21 FileOwnerType // file/owner
22 RuleOwnerType // rule/owner
23 FileDisableType // file/disable
24 DisableType // disable
25 FileSnoozeType // file/snooze
26 SnoozeType // snooze
27 RuleSetType // rule/set
28)
29
30var (
31 Prefix = "pint"
32
33 IgnoreFileComment = "ignore/file"
34 IgnoreLineComment = "ignore/line"
35 IgnoreBeginComment = "ignore/begin"
36 IgnoreEndComment = "ignore/end"
37 IgnoreNextLineComment = "ignore/next-line"
38 FileOwnerComment = "file/owner"
39 RuleOwnerComment = "rule/owner"
40 FileDisableComment = "file/disable"
41 DisableComment = "disable"
42 FileSnoozeComment = "file/snooze"
43 SnoozeComment = "snooze"
44 RuleSetComment = "rule/set"
45)
46
47type CommentValue interface {
48 String() string
49}
50
51type Comment struct {
52 Value CommentValue
53 Type Type
54 Offset int
55}
56
57func parseType(s string) Type {
58 switch s {
59 case IgnoreFileComment:
60 return IgnoreFileType
61 case IgnoreLineComment:
62 return IgnoreLineType
63 case IgnoreBeginComment:
64 return IgnoreBeginType
65 case IgnoreEndComment:
66 return IgnoreEndType
67 case IgnoreNextLineComment:
68 return IgnoreNextLineType
69 case FileOwnerComment:
70 return FileOwnerType
71 case RuleOwnerComment:
72 return RuleOwnerType
73 case FileDisableComment:
74 return FileDisableType
75 case DisableComment:
76 return DisableType
77 case FileSnoozeComment:
78 return FileSnoozeType
79 case SnoozeComment:
80 return SnoozeType
81 case RuleSetComment:
82 return RuleSetType
83 default:
84 return UnknownType
85 }
86}
87
88type CommentError struct {
89 Err error
90 Line int
91}
92
93func (ce CommentError) Error() string {
94 return ce.Err.Error()
95}
96
97type Invalid struct {
98 Err CommentError
99}
100
101func (i Invalid) String() string {
102 return i.Err.Error()
103}
104
105type Owner struct {
106 Name string
107}
108
109func (o Owner) String() string {
110 return o.Name
111}
112
113type Disable struct {
114 Match string
115}
116
117func (d Disable) String() string {
118 return d.Match
119}
120
121type Snooze struct {
122 Until time.Time
123 Match string
124}
125
126func (s Snooze) String() string {
127 return fmt.Sprintf("%s %s", s.Until.Format(time.RFC3339), s.Match)
128}
129
130type RuleSet struct {
131 Value string
132}
133
134func (r RuleSet) String() string {
135 return r.Value
136}
137
138func parseSnooze(s string) (snz Snooze, err error) {
139 parts := strings.SplitN(s, " ", 2)
140 if len(parts) != 2 {
141 return Snooze{}, fmt.Errorf("invalid snooze comment, expected '$TIME $MATCH' got %q", s)
142 }
143
144 snz = Snooze{Match: parts[1]}
145 snz.Until, err = time.Parse(time.RFC3339, parts[0])
146 if err != nil {
147 snz.Until, err = time.Parse("2006-01-02", parts[0])
148 }
149 if err != nil {
150 return snz, fmt.Errorf("invalid snooze timestamp: %w", err)
151 }
152 return snz, nil
153}
154
155func parseValue(typ Type, s string) (CommentValue, error) {
156 switch typ {
157 case IgnoreFileType, IgnoreLineType, IgnoreBeginType, IgnoreEndType, IgnoreNextLineType:
158 if s != "" {
159 return nil, fmt.Errorf("unexpected comment suffix: %q", s)
160 }
161 case FileOwnerType:
162 if s == "" {
163 return nil, fmt.Errorf("missing %s value", FileOwnerComment)
164 }
165 return Owner{Name: s}, nil
166 case RuleOwnerType:
167 if s == "" {
168 return nil, fmt.Errorf("missing %s value", RuleOwnerComment)
169 }
170 return Owner{Name: s}, nil
171 case FileDisableType:
172 if s == "" {
173 return nil, fmt.Errorf("missing %s value", FileDisableComment)
174 }
175 return Disable{Match: s}, nil
176 case DisableType:
177 if s == "" {
178 return nil, fmt.Errorf("missing %s value", DisableComment)
179 }
180 return Disable{Match: s}, nil
181 case FileSnoozeType:
182 if s == "" {
183 return nil, fmt.Errorf("missing %s value", FileSnoozeComment)
184 }
185 return parseSnooze(s)
186 case SnoozeType:
187 if s == "" {
188 return nil, fmt.Errorf("missing %s value", SnoozeComment)
189 }
190 return parseSnooze(s)
191 case RuleSetType:
192 if s == "" {
193 return nil, fmt.Errorf("missing %s value", RuleSetComment)
194 }
195 return RuleSet{Value: s}, nil
196 case UnknownType, InvalidComment:
197 // pass
198 }
199 return nil, nil
200}
201
202const (
203 needsHash uint8 = iota
204 needsPrefix
205 readsPrefix
206 needsType
207 readsType
208 needsValue
209 readsValue
210)
211
212func parseComment(s string) (parsed []Comment, err error) {
213 var buf strings.Builder
214 var c Comment
215
216 state := needsHash
217 for i, r := range s + "\n" {
218 READRUNE:
219 switch state {
220 case needsHash:
221 if r != '#' {
222 goto NEXT
223 }
224 state = needsPrefix
225 buf.Reset()
226 c.Type = UnknownType
227 c.Value = nil
228 c.Offset = i
229 case needsPrefix:
230 if unicode.IsSpace(r) {
231 goto NEXT
232 }
233 state = readsPrefix
234 goto READRUNE
235 case readsPrefix:
236 if unicode.IsLetter(r) {
237 _, _ = buf.WriteRune(r)
238 goto NEXT
239 }
240 if unicode.IsSpace(r) {
241 // Invalid comment prefix, ignore this comment.
242 if buf.String() != Prefix {
243 buf.Reset()
244 state = needsHash
245 goto NEXT
246 }
247 buf.Reset()
248 state = needsType
249 goto NEXT
250 }
251 // Invalid character in the prefix, ignore this comment.
252 state = needsHash
253 case needsType:
254 if r == '#' {
255 state = needsHash
256 goto READRUNE
257 }
258 if unicode.IsSpace(r) {
259 goto NEXT
260 }
261 state = readsType
262 goto READRUNE
263 case readsType:
264 if unicode.IsLetter(r) || r == '/' || r == '-' {
265 _, _ = buf.WriteRune(r)
266 goto NEXT
267 }
268 if unicode.IsSpace(r) || r == '\n' {
269 c.Type = parseType(buf.String())
270 buf.Reset()
271 if c.Type == UnknownType {
272 state = needsHash
273 } else {
274 state = needsValue
275 }
276
277 }
278 case needsValue:
279 if unicode.IsSpace(r) {
280 goto NEXT
281 }
282 state = readsValue
283 goto READRUNE
284 case readsValue:
285 if r == '\n' {
286 goto NEXT
287 }
288 _, _ = buf.WriteRune(r)
289 }
290 NEXT:
291 }
292
293 if c.Type != UnknownType {
294 c.Value, err = parseValue(c.Type, strings.TrimSpace(buf.String()))
295 parsed = append(parsed, c)
296 }
297
298 return parsed, err
299}
300
301func Parse(lineno int, text string) (comments []Comment) {
302 sc := bufio.NewScanner(strings.NewReader(text))
303 var index int
304 for sc.Scan() {
305 line := sc.Text()
306 parsed, err := parseComment(line)
307 if err != nil {
308 comments = append(comments, Comment{
309 Type: InvalidComment,
310 Value: Invalid{Err: CommentError{Line: lineno + index, Err: err}},
311 })
312 continue
313 }
314 comments = append(comments, parsed...)
315 index++
316 }
317 return comments
318}
319
320func Only[T any](src []Comment, typ Type) []T {
321 dst := make([]T, 0, len(src))
322 for _, c := range src {
323 if c.Type != typ {
324 continue
325 }
326 dst = append(dst, c.Value.(T))
327 }
328 return dst
329}
330
331func IsRuleComment(typ Type) bool {
332 // nolint:exhaustive
333 switch typ {
334 case RuleOwnerType, DisableType, SnoozeType, RuleSetType:
335 return true
336 }
337 return false
338}
339