cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.71.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/comments/comments.go

360lines · modecode

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