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/discovery/discovery.go

237lines · modecode

1package discovery
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "log/slog"
8 "regexp"
9 "slices"
10 "time"
11
12 "github.com/cloudflare/pint/internal/comments"
13 "github.com/cloudflare/pint/internal/diags"
14 "github.com/cloudflare/pint/internal/parser"
15)
16
17const (
18 FileOwnerComment = "file/owner"
19 FileDisabledCheckComment = "file/disable"
20 FileSnoozeCheckComment = "file/snooze"
21 RuleOwnerComment = "rule/owner"
22)
23
24type FileIgnoreError struct {
25 Diagnostic diags.Diagnostic
26}
27
28func (fe FileIgnoreError) Error() string {
29 return fe.Diagnostic.Message
30}
31
32type ChangeType uint8
33
34func (c ChangeType) String() string {
35 switch c {
36 case Unknown:
37 return "unknown"
38 case Noop:
39 return "noop"
40 case Added:
41 return "added"
42 case Modified:
43 return "modified"
44 case Removed:
45 return "removed"
46 case Moved:
47 return "moved"
48 default:
49 return "---"
50 }
51}
52
53func (c *ChangeType) MarshalJSON() ([]byte, error) {
54 return json.Marshal(c.String())
55}
56
57const (
58 Unknown ChangeType = iota
59 Noop
60 Added
61 Modified
62 Removed
63 Moved
64)
65
66type Path struct {
67 Name string // File path, it can be symlink.
68 SymlinkTarget string // Symlink target, or the same as name if not a symlink.
69}
70
71func (p Path) String() string {
72 if p.Name == p.SymlinkTarget {
73 return p.Name
74 }
75 return fmt.Sprintf("%s ~> %s", p.Name, p.SymlinkTarget)
76}
77
78type Entry struct {
79 PathError error
80 Path Path
81 Owner string
82 ModifiedLines []int
83 DisabledChecks []string
84 Rule parser.Rule
85 State ChangeType
86}
87
88func readRules(reportedPath, sourcePath string, r io.Reader, p parser.Parser, allowedOwners []*regexp.Regexp) (entries []Entry, err error) {
89 content, err := parser.ReadContent(r)
90 if err != nil {
91 return nil, err
92 }
93
94 contentLines := diags.LineRange{
95 First: min(content.TotalLines, 1),
96 Last: content.TotalLines,
97 }
98
99 var badOwners []comments.Comment
100 var fileOwner string
101 var disabledChecks []string
102 for _, comment := range content.FileComments {
103 // nolint:exhaustive
104 switch comment.Type {
105 case comments.FileOwnerType:
106 owner := comment.Value.(comments.Owner)
107 if isValidOwner(owner.Name, allowedOwners) {
108 fileOwner = owner.Name
109 } else {
110 badOwners = append(badOwners, comment)
111 }
112 case comments.FileDisableType:
113 disable := comment.Value.(comments.Disable)
114 if !slices.Contains(disabledChecks, disable.Match) {
115 disabledChecks = append(disabledChecks, disable.Match)
116 }
117 case comments.FileSnoozeType:
118 snooze := comment.Value.(comments.Snooze)
119 if !snooze.Until.After(time.Now()) {
120 continue
121 }
122 if !slices.Contains(disabledChecks, snooze.Match) {
123 disabledChecks = append(disabledChecks, snooze.Match)
124 }
125 slog.Debug(
126 "Check snoozed by comment",
127 slog.String("check", snooze.Match),
128 slog.String("match", snooze.Match),
129 slog.Time("until", snooze.Until),
130 )
131 case comments.InvalidComment:
132 entries = append(entries, Entry{
133 Path: Path{
134 Name: sourcePath,
135 SymlinkTarget: reportedPath,
136 },
137 PathError: comment.Value.(comments.Invalid).Err,
138 Owner: fileOwner,
139 ModifiedLines: contentLines.Expand(),
140 })
141 }
142 }
143
144 if content.Ignored != nil {
145 entries = append(entries, Entry{
146 Path: Path{
147 Name: sourcePath,
148 SymlinkTarget: reportedPath,
149 },
150 PathError: FileIgnoreError{
151 Diagnostic: *content.Ignored,
152 },
153 Owner: fileOwner,
154 ModifiedLines: contentLines.Expand(),
155 })
156 return entries, nil
157 }
158
159 rules, err := p.Parse(content.Body)
160 if err != nil {
161 slog.Warn(
162 "Failed to parse file content",
163 slog.Any("err", err),
164 slog.String("path", sourcePath),
165 slog.String("lines", contentLines.String()),
166 )
167 entries = append(entries, Entry{
168 Path: Path{
169 Name: sourcePath,
170 SymlinkTarget: reportedPath,
171 },
172 PathError: err,
173 Owner: fileOwner,
174 ModifiedLines: contentLines.Expand(),
175 })
176 return entries, nil
177 }
178
179 for _, rule := range rules {
180 ruleOwner := fileOwner
181 for _, owner := range comments.Only[comments.Owner](rule.Comments, comments.RuleOwnerType) {
182 ruleOwner = owner.Name
183 }
184 entries = append(entries, Entry{
185 Path: Path{
186 Name: sourcePath,
187 SymlinkTarget: reportedPath,
188 },
189 Rule: rule,
190 ModifiedLines: rule.Lines.Expand(),
191 Owner: ruleOwner,
192 DisabledChecks: disabledChecks,
193 })
194 }
195
196 if len(rules) == 0 && len(badOwners) > 0 {
197 for _, comment := range badOwners {
198 owner := comment.Value.(comments.Owner)
199 entries = append(entries, Entry{
200 Path: Path{
201 Name: sourcePath,
202 SymlinkTarget: reportedPath,
203 },
204 PathError: comments.OwnerError{
205 Diagnostic: diags.Diagnostic{
206 Message: fmt.Sprintf("This file is set as owned by `%s` but `%s` doesn't match any of the allowed owner values.", owner.Name, owner.Name),
207 Pos: diags.PositionRanges{
208 {
209 Line: owner.Line,
210 FirstColumn: comment.Offset + 1,
211 LastColumn: comment.Offset + len(owner.Name),
212 },
213 },
214 FirstColumn: comment.Offset + 1,
215 LastColumn: comment.Offset + len(owner.Name),
216 },
217 },
218 ModifiedLines: contentLines.Expand(),
219 })
220 }
221 }
222
223 slog.Debug("File parsed", slog.String("path", sourcePath), slog.Int("rules", len(entries)))
224 return entries, nil
225}
226
227func isValidOwner(s string, valid []*regexp.Regexp) bool {
228 if len(valid) == 0 {
229 return true
230 }
231 for _, v := range valid {
232 if v.MatchString(s) {
233 return true
234 }
235 }
236 return false
237}
238