cloudflare/pint
Publicmirrored from https://github.com/cloudflare/pintAvailable
internal/reporter/comments.go
295lines · modecode
| 1 | package reporter |
| 2 | |
| 3 | import ( |
| 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 | |
| 13 | type PendingComment struct { |
| 14 | path string |
| 15 | text string |
| 16 | line int |
| 17 | anchor checks.Anchor |
| 18 | } |
| 19 | |
| 20 | type ExistingComment struct { |
| 21 | meta any |
| 22 | path string |
| 23 | text string |
| 24 | line int |
| 25 | } |
| 26 | |
| 27 | type 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 | |
| 39 | func NewCommentReporter(c Commenter) CommentReporter { |
| 40 | return CommentReporter{c: c} |
| 41 | } |
| 42 | |
| 43 | type CommentReporter struct { |
| 44 | c Commenter |
| 45 | } |
| 46 | |
| 47 | func (cr CommentReporter) Submit(summary Summary) (err error) { |
| 48 | return Submit(context.Background(), summary, cr.c) |
| 49 | } |
| 50 | |
| 51 | func 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 | |
| 107 | func 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 | |
| 145 | func 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 | |
| 162 | func 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 | |
| 174 | func 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 | |
| 187 | func 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 | |
| 205 | func 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 | |