1package analyzer
2
3import (
4 "go/ast"
5 "go/token"
6 "sort"
7 "strings"
8
9 "github.com/bnema/hexcheck/config"
10 "golang.org/x/tools/go/analysis"
11)
12
13// DirectiveCategory is the diagnostic category used for malformed
14// //hexcheck:ignore directives.
15const DirectiveCategory = "invalid-ignore-directive"
16
17const ignoreDirective = "//hexcheck:ignore"
18
19type ignoreKey struct {
20 file string
21 line int
22 rule string
23}
24
25// ignoreIndex records which rules are suppressed on which lines.
26type ignoreIndex map[ignoreKey][]*ignoreUsage
27
28type ignoreUsage struct {
29 pos token.Pos
30 rule string
31 used bool
32 filePath string
33}
34
35// collectIgnores indexes `//hexcheck:ignore rule[,rule] reason` directives.
36// A directive suppresses matching diagnostics on its own line and on the
37// following line. A reason is required, like config allow entries.
38func (r runner) collectIgnores(file *ast.File) {
39 known := config.DefaultRuleSeverities()
40 for _, group := range file.Comments {
41 for _, comment := range group.List {
42 rest, ok := strings.CutPrefix(comment.Text, ignoreDirective)
43 if !ok || (rest != "" && rest[0] != ' ' && rest[0] != '\t') {
44 continue
45 }
46 fields := strings.Fields(rest)
47 if len(fields) < 2 {
48 r.reportDirective(comment.Pos(), "hexcheck:ignore needs a rule list and a reason")
49 continue
50 }
51 pos := r.pass.Fset.Position(comment.Pos())
52 for rule := range strings.SplitSeq(fields[0], ",") {
53 if _, ok := known[rule]; !ok {
54 r.reportDirective(comment.Pos(), "hexcheck:ignore names unknown rule "+rule)
55 continue
56 }
57 usage := &ignoreUsage{pos: comment.Pos(), rule: rule, filePath: r.filePath(file)}
58 for _, line := range []int{pos.Line, pos.Line + 1} {
59 key := ignoreKey{file: pos.Filename, line: line, rule: rule}
60 r.ignores[key] = append(r.ignores[key], usage)
61 }
62 }
63 }
64 }
65}
66
67func (r runner) isIgnoredInline(pos token.Pos, rule string) bool {
68 if r.ignores == nil {
69 return false
70 }
71 p := r.pass.Fset.Position(pos)
72 usages := r.ignores[ignoreKey{file: p.Filename, line: p.Line, rule: rule}]
73 for _, usage := range usages {
74 usage.used = true
75 }
76 return len(usages) > 0
77}
78
79func (r runner) reportUnusedIgnores() {
80 seen := map[*ignoreUsage]bool{}
81 var unused []*ignoreUsage
82 for _, usages := range r.ignores {
83 for _, usage := range usages {
84 if !seen[usage] && !usage.used {
85 unused = append(unused, usage)
86 }
87 seen[usage] = true
88 }
89 }
90 sort.Slice(unused, func(i, j int) bool {
91 if unused[i].pos != unused[j].pos {
92 return unused[i].pos < unused[j].pos
93 }
94 return unused[i].rule < unused[j].rule
95 })
96 for _, usage := range unused {
97 r.report(usage.pos, "unused-ignore-directive", usage.filePath, "hexcheck:ignore %s suppresses no diagnostics", usage.rule)
98 }
99}
100
101func (r runner) reportDirective(pos token.Pos, msg string) {
102 r.pass.Report(analysis.Diagnostic{Pos: pos, Category: DirectiveCategory, Message: "[error] " + DirectiveCategory + ": " + msg})
103}