cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.73.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/comments/comments.go

357lines · modecode

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