cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.79.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/comments/comments.go

363lines · 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 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(commentType Type, s string, line int) (CommentValue, error) {
165 // nolint:exhaustive
166 switch commentType {
167 case IgnoreFileType, IgnoreLineType, IgnoreBeginType, IgnoreEndType, IgnoreNextLineType:
168 if s != "" {
169 return nil, fmt.Errorf("unexpected comment suffix: %q", s)
170 }
171 return nil, nil
172 case FileOwnerType:
173 if s == "" {
174 return nil, fmt.Errorf("missing %s value", FileOwnerComment)
175 }
176 return Owner{Name: s, Line: line}, nil
177 case RuleOwnerType:
178 if s == "" {
179 return nil, fmt.Errorf("missing %s value", RuleOwnerComment)
180 }
181 return Owner{Name: s, Line: 0}, nil // comment attached to the rule, line numbers are unreliable
182 case FileDisableType:
183 if s == "" {
184 return nil, fmt.Errorf("missing %s value", FileDisableComment)
185 }
186 return Disable{Match: s}, nil
187 case DisableType:
188 if s == "" {
189 return nil, fmt.Errorf("missing %s value", DisableComment)
190 }
191 return Disable{Match: s}, nil
192 case FileSnoozeType:
193 if s == "" {
194 return nil, fmt.Errorf("missing %s value", FileSnoozeComment)
195 }
196 return parseSnooze(s)
197 case SnoozeType:
198 if s == "" {
199 return nil, fmt.Errorf("missing %s value", SnoozeComment)
200 }
201 return parseSnooze(s)
202 case RuleSetType:
203 if s == "" {
204 return nil, fmt.Errorf("missing %s value", RuleSetComment)
205 }
206 return RuleSet{Value: s}, nil
207 case UnknownType, InvalidComment:
208 // these are never passed here
209 return nil, nil
210 default:
211 // this is not reachable
212 return nil, nil
213 }
214}
215
216const (
217 needsHash uint8 = iota
218 needsPrefix
219 readsPrefix
220 needsType
221 readsType
222 needsValue
223 readsValue
224)
225
226func parseComment(s string, line int) (parsed []Comment) {
227 var err error
228 var buf strings.Builder
229 var c Comment
230
231 state := needsHash
232 for i, r := range s + "\n" {
233 READRUNE:
234 switch state {
235 case needsHash:
236 if r != '#' {
237 goto NEXT
238 }
239 state = needsPrefix
240 buf.Reset()
241 c.Type = UnknownType
242 c.Value = nil
243 c.Offset = i
244 case needsPrefix:
245 if unicode.IsSpace(r) {
246 goto NEXT
247 }
248 state = readsPrefix
249 goto READRUNE
250 case readsPrefix:
251 if unicode.IsLetter(r) {
252 _, _ = buf.WriteRune(r)
253 goto NEXT
254 }
255 if unicode.IsSpace(r) {
256 // Invalid comment prefix, ignore this comment.
257 if buf.String() != Prefix {
258 buf.Reset()
259 state = needsHash
260 goto NEXT
261 }
262 buf.Reset()
263 state = needsType
264 goto NEXT
265 }
266 // Invalid character in the prefix, ignore this comment.
267 state = needsHash
268 case needsType:
269 if r == '#' {
270 state = needsHash
271 goto READRUNE
272 }
273 if unicode.IsSpace(r) {
274 goto NEXT
275 }
276 state = readsType
277 goto READRUNE
278 case readsType:
279 if unicode.IsLetter(r) || r == '/' || r == '-' {
280 _, _ = buf.WriteRune(r)
281 goto NEXT
282 }
283 if unicode.IsSpace(r) || r == '\n' {
284 c.Type = parseType(buf.String())
285 buf.Reset()
286 if c.Type == UnknownType {
287 state = needsHash
288 } else {
289 state = needsValue
290 }
291
292 }
293 case needsValue:
294 if unicode.IsSpace(r) {
295 goto NEXT
296 }
297 state = readsValue
298 goto READRUNE
299 case readsValue:
300 if r == '\n' {
301 goto NEXT
302 }
303 _, _ = buf.WriteRune(r)
304 }
305 NEXT:
306 }
307
308 if c.Type != UnknownType {
309 c.Value, err = parseValue(c.Type, strings.TrimSpace(buf.String()), line)
310 if err != nil {
311 c.Type = InvalidComment
312 c.Value = Invalid{
313 Err: CommentError{
314 Diagnostic: diags.Diagnostic{
315 Message: "This comment is not a valid pint control comment: " + err.Error(),
316 Pos: diags.PositionRanges{
317 {
318 Line: line,
319 FirstColumn: c.Offset + 1,
320 LastColumn: len(s),
321 },
322 },
323 FirstColumn: 1,
324 LastColumn: len(s),
325 Kind: diags.Issue,
326 },
327 },
328 }
329 }
330 parsed = append(parsed, c)
331 }
332
333 return parsed
334}
335
336func Parse(lineno int, text string) (comments []Comment) {
337 var index int
338 for line := range strings.SplitSeq(text, "\n") {
339 comments = append(comments, parseComment(line, lineno+index)...)
340 index++
341 }
342 return comments
343}
344
345func Only[T any](src []Comment, commentType Type) []T {
346 dst := make([]T, 0, len(src))
347 for _, c := range src {
348 if c.Type != commentType {
349 continue
350 }
351 dst = append(dst, c.Value.(T))
352 }
353 return dst
354}
355
356func IsRuleComment(commentType Type) bool {
357 // nolint:exhaustive
358 switch commentType {
359 case RuleOwnerType, DisableType, SnoozeType, RuleSetType:
360 return true
361 }
362 return false
363}
364