cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.64.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/reporter/comments.go

295lines · modecode

1package reporter
2
3import (
4 "context"
5 "log/slog"
6 "slices"
7 "strings"
8
9 "github.com/cloudflare/pint/internal/checks"
10 "github.com/cloudflare/pint/internal/output"
11)
12
13type PendingComment struct {
14 path string
15 text string
16 line int
17 anchor checks.Anchor
18}
19
20type ExistingComment struct {
21 meta any
22 path string
23 text string
24 line int
25}
26
27type Commenter interface {
28 Describe() string
29 Destinations(context.Context) ([]any, error)
30 Summary(context.Context, any, Summary, []error) error
31 List(context.Context, any) ([]ExistingComment, error)
32 Create(context.Context, any, PendingComment) error
33 Delete(context.Context, any, ExistingComment) error
34 CanCreate(int) bool
35 CanDelete(ExistingComment) bool
36 IsEqual(ExistingComment, PendingComment) bool
37}
38
39func NewCommentReporter(c Commenter) CommentReporter {
40 return CommentReporter{c: c}
41}
42
43type CommentReporter struct {
44 c Commenter
45}
46
47func (cr CommentReporter) Submit(summary Summary) (err error) {
48 return Submit(context.Background(), summary, cr.c)
49}
50
51func makeComments(summary Summary) (comments []PendingComment) {
52 var buf strings.Builder
53 for _, reports := range dedupReports(summary.reports) {
54 mergeDetails := identicalDetails(reports)
55
56 buf.Reset()
57
58 buf.WriteString(problemIcon(reports[0].Problem.Severity))
59 buf.WriteString(" **")
60 buf.WriteString(reports[0].Problem.Severity.String())
61 buf.WriteString("** reported by [pint](https://cloudflare.github.io/pint/) **")
62 buf.WriteString(reports[0].Problem.Reporter)
63 buf.WriteString("** check.\n\n")
64 for _, report := range reports {
65 buf.WriteString("------\n\n")
66 buf.WriteString(report.Problem.Text)
67 buf.WriteString("\n\n")
68 if !mergeDetails && report.Problem.Details != "" {
69 buf.WriteString(report.Problem.Details)
70 buf.WriteString("\n\n")
71 }
72 if report.Path.SymlinkTarget != report.Path.Name {
73 buf.WriteString(":leftwards_arrow_with_hook: This problem was detected on a symlinked file ")
74 buf.WriteRune('`')
75 buf.WriteString(report.Path.Name)
76 buf.WriteString("`.\n\n")
77 }
78 }
79 if mergeDetails && reports[0].Problem.Details != "" {
80 buf.WriteString("------\n\n")
81 buf.WriteString(reports[0].Problem.Details)
82 buf.WriteString("\n\n")
83 }
84 buf.WriteString("------\n\n")
85 buf.WriteString(":information_source: To see documentation covering this check and instructions on how to resolve it [click here](https://cloudflare.github.io/pint/checks/")
86 buf.WriteString(reports[0].Problem.Reporter)
87 buf.WriteString(".html).\n")
88
89 line := reports[0].Problem.Lines.Last
90 for i := reports[0].Problem.Lines.Last; i >= reports[0].Problem.Lines.First; i-- {
91 if slices.Contains(reports[0].ModifiedLines, i) {
92 line = i
93 break
94 }
95 }
96
97 comments = append(comments, PendingComment{
98 anchor: reports[0].Problem.Anchor,
99 path: reports[0].Path.SymlinkTarget,
100 line: line,
101 text: buf.String(),
102 })
103 }
104 return comments
105}
106
107func dedupReports(src []Report) (dst [][]Report) {
108 for _, report := range src {
109 index := -1
110 for i, d := range dst {
111 if d[0].Problem.Severity != report.Problem.Severity {
112 continue
113 }
114 if d[0].Problem.Reporter != report.Problem.Reporter {
115 continue
116 }
117 if d[0].Path.SymlinkTarget != report.Path.SymlinkTarget {
118 continue
119 }
120 if d[0].Problem.Lines.First != report.Problem.Lines.First {
121 continue
122 }
123 if d[0].Problem.Lines.Last != report.Problem.Lines.Last {
124 continue
125 }
126 if d[0].Problem.Anchor != report.Problem.Anchor {
127 continue
128 }
129 index = i
130 break
131 }
132 if index < 0 {
133 dst = append(dst, []Report{report})
134 continue
135 }
136 // Skip this report if we have exact same message already
137 if dst[index][0].Problem.Text == report.Problem.Text && dst[index][0].Problem.Details == report.Problem.Details {
138 continue
139 }
140 dst[index] = append(dst[index], report)
141 }
142 return dst
143}
144
145func identicalDetails(src []Report) bool {
146 if len(src) <= 1 {
147 return false
148 }
149 var details string
150 for _, report := range src {
151 if details == "" {
152 details = report.Problem.Details
153 continue
154 }
155 if details != report.Problem.Details {
156 return false
157 }
158 }
159 return true
160}
161
162func problemIcon(s checks.Severity) string {
163 // nolint:exhaustive
164 switch s {
165 case checks.Warning:
166 return ":warning:"
167 case checks.Information:
168 return ":information_source:"
169 default:
170 return ":stop_sign:"
171 }
172}
173
174func errsToComment(errs []error) string {
175 var buf strings.Builder
176 buf.WriteString("There were some errors when pint was trying to create a report.\n")
177 buf.WriteString("Some review comments might be outdated or missing.\n")
178 buf.WriteString("List of all errors:\n\n")
179 for _, err := range errs {
180 buf.WriteString("- `")
181 buf.WriteString(err.Error())
182 buf.WriteString("`\n")
183 }
184 return buf.String()
185}
186
187func Submit(ctx context.Context, s Summary, c Commenter) error {
188 slog.Info("Will now report problems", slog.String("reporter", c.Describe()))
189 dsts, err := c.Destinations(ctx)
190 if err != nil {
191 return err
192 }
193
194 for _, dst := range dsts {
195 slog.Info("Found a report destination", slog.String("reporter", c.Describe()), slog.Any("dst", dst))
196 if err = updateDestination(ctx, s, c, dst); err != nil {
197 return err
198 }
199 }
200
201 slog.Info("Finished reporting problems", slog.String("reporter", c.Describe()))
202 return nil
203}
204
205func updateDestination(ctx context.Context, s Summary, c Commenter, dst any) (err error) {
206 slog.Info("Listing existing comments", slog.String("reporter", c.Describe()))
207 existingComments, err := c.List(ctx, dst)
208 if err != nil {
209 return err
210 }
211
212 var created int
213 var errs []error
214 pendingComments := makeComments(s)
215 for _, pending := range pendingComments {
216 slog.Debug("Got pending comment",
217 slog.String("reporter", c.Describe()),
218 slog.String("path", pending.path),
219 slog.Int("line", pending.line),
220 slog.String("msg", pending.text),
221 )
222 for _, existing := range existingComments {
223 if c.IsEqual(existing, pending) {
224 slog.Debug("Comment already exists",
225 slog.String("reporter", c.Describe()),
226 slog.String("path", pending.path),
227 slog.Int("line", pending.line),
228 )
229 goto NEXTCreate
230 }
231 }
232 slog.Debug("Comment doesn't exist yet and needs to be created",
233 slog.String("reporter", c.Describe()),
234 slog.String("path", pending.path),
235 slog.Int("line", pending.line),
236 )
237
238 if !c.CanCreate(created) {
239 slog.Debug("Cannot create new comment",
240 slog.String("reporter", c.Describe()),
241 slog.String("path", pending.path),
242 slog.Int("line", pending.line),
243 )
244 goto NEXTCreate
245 }
246
247 if err := c.Create(ctx, dst, pending); err != nil {
248 slog.Error("Failed to create a new comment",
249 slog.String("reporter", c.Describe()),
250 slog.String("path", pending.path),
251 slog.Int("line", pending.line),
252 slog.Any("err", err),
253 )
254 return err
255 }
256 created++
257 NEXTCreate:
258 }
259
260 for _, existing := range existingComments {
261 for _, pending := range pendingComments {
262 if c.IsEqual(existing, pending) {
263 goto NEXTDelete
264 }
265 }
266 if !c.CanDelete(existing) {
267 goto NEXTDelete
268 }
269 if err := c.Delete(ctx, dst, existing); err != nil {
270 slog.Error("Failed to delete a stale comment",
271 slog.String("reporter", c.Describe()),
272 slog.String("path", existing.path),
273 slog.Int("line", existing.line),
274 slog.Any("err", err),
275 )
276 errs = append(errs, err)
277 }
278 NEXTDelete:
279 }
280
281 slog.Info("Creating report summary",
282 slog.String("reporter", c.Describe()),
283 slog.Int("reports", len(s.reports)),
284 slog.Int("online", int(s.OnlineChecks)),
285 slog.Int("offline", int(s.OnlineChecks)),
286 slog.String("duration", output.HumanizeDuration(s.Duration)),
287 slog.Int("entries", s.TotalEntries),
288 slog.Int("checked", int(s.CheckedEntries)),
289 )
290 if err := c.Summary(ctx, dst, s, errs); err != nil {
291 return err
292 }
293
294 return nil
295}
296