github.com/songrgg/gometalinter@v2.0.6-0.20180425200507-2cbec6168e84+incompatible/directives.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/parser" 7 "go/token" 8 "os" 9 "sort" 10 "strings" 11 "sync" 12 "time" 13 ) 14 15 type ignoredRange struct { 16 col int 17 start, end int 18 linters []string 19 matched bool 20 } 21 22 func (i *ignoredRange) matches(issue *Issue) bool { 23 if issue.Line < i.start || issue.Line > i.end { 24 return false 25 } 26 if len(i.linters) == 0 { 27 return true 28 } 29 for _, l := range i.linters { 30 if l == issue.Linter { 31 return true 32 } 33 } 34 return false 35 } 36 37 func (i *ignoredRange) near(col, start int) bool { 38 return col == i.col && i.end == start-1 39 } 40 41 func (i *ignoredRange) String() string { 42 linters := strings.Join(i.linters, ",") 43 if len(i.linters) == 0 { 44 linters = "all" 45 } 46 return fmt.Sprintf("%s:%d-%d", linters, i.start, i.end) 47 } 48 49 type ignoredRanges []*ignoredRange 50 51 func (ir ignoredRanges) Len() int { return len(ir) } 52 func (ir ignoredRanges) Swap(i, j int) { ir[i], ir[j] = ir[j], ir[i] } 53 func (ir ignoredRanges) Less(i, j int) bool { return ir[i].end < ir[j].end } 54 55 type directiveParser struct { 56 lock sync.Mutex 57 files map[string]ignoredRanges 58 fset *token.FileSet 59 } 60 61 func newDirectiveParser() *directiveParser { 62 return &directiveParser{ 63 files: map[string]ignoredRanges{}, 64 fset: token.NewFileSet(), 65 } 66 } 67 68 // IsIgnored returns true if the given linter issue is ignored by a linter directive. 69 func (d *directiveParser) IsIgnored(issue *Issue) bool { 70 d.lock.Lock() 71 path := issue.Path.Relative() 72 ranges, ok := d.files[path] 73 if !ok { 74 ranges = d.parseFile(path) 75 sort.Sort(ranges) 76 d.files[path] = ranges 77 } 78 d.lock.Unlock() 79 for _, r := range ranges { 80 if r.matches(issue) { 81 debug("nolint: matched %s to issue %s", r, issue) 82 r.matched = true 83 return true 84 } 85 } 86 return false 87 } 88 89 // Unmatched returns all the ranges which were never used to ignore an issue 90 func (d *directiveParser) Unmatched() map[string]ignoredRanges { 91 unmatched := map[string]ignoredRanges{} 92 for path, ranges := range d.files { 93 for _, ignore := range ranges { 94 if !ignore.matched { 95 unmatched[path] = append(unmatched[path], ignore) 96 } 97 } 98 } 99 return unmatched 100 } 101 102 // LoadFiles from a list of directories 103 func (d *directiveParser) LoadFiles(paths []string) error { 104 d.lock.Lock() 105 defer d.lock.Unlock() 106 filenames, err := pathsToFileGlobs(paths) 107 if err != nil { 108 return err 109 } 110 for _, filename := range filenames { 111 ranges := d.parseFile(filename) 112 sort.Sort(ranges) 113 d.files[filename] = ranges 114 } 115 return nil 116 } 117 118 // Takes a set of ignoredRanges, determines if they immediately precede a statement 119 // construct, and expands the range to include that construct. Why? So you can 120 // precede a function or struct with //nolint 121 type rangeExpander struct { 122 fset *token.FileSet 123 ranges ignoredRanges 124 } 125 126 func (a *rangeExpander) Visit(node ast.Node) ast.Visitor { 127 if node == nil { 128 return a 129 } 130 startPos := a.fset.Position(node.Pos()) 131 start := startPos.Line 132 end := a.fset.Position(node.End()).Line 133 found := sort.Search(len(a.ranges), func(i int) bool { 134 return a.ranges[i].end+1 >= start 135 }) 136 if found < len(a.ranges) && a.ranges[found].near(startPos.Column, start) { 137 r := a.ranges[found] 138 if r.start > start { 139 r.start = start 140 } 141 if r.end < end { 142 r.end = end 143 } 144 } 145 return a 146 } 147 148 func (d *directiveParser) parseFile(path string) ignoredRanges { 149 start := time.Now() 150 debug("nolint: parsing %s for directives", path) 151 file, err := parser.ParseFile(d.fset, path, nil, parser.ParseComments) 152 if err != nil { 153 debug("nolint: failed to parse %q: %s", path, err) 154 return nil 155 } 156 ranges := extractCommentGroupRange(d.fset, file.Comments...) 157 visitor := &rangeExpander{fset: d.fset, ranges: ranges} 158 ast.Walk(visitor, file) 159 debug("nolint: parsing %s took %s", path, time.Since(start)) 160 return visitor.ranges 161 } 162 163 func extractCommentGroupRange(fset *token.FileSet, comments ...*ast.CommentGroup) (ranges ignoredRanges) { 164 for _, g := range comments { 165 for _, c := range g.List { 166 text := strings.TrimLeft(c.Text, "/ ") 167 var linters []string 168 if strings.HasPrefix(text, "nolint") { 169 if strings.HasPrefix(text, "nolint:") { 170 for _, linter := range strings.Split(text[7:], ",") { 171 linters = append(linters, strings.TrimSpace(linter)) 172 } 173 } 174 pos := fset.Position(g.Pos()) 175 rng := &ignoredRange{ 176 col: pos.Column, 177 start: pos.Line, 178 end: fset.Position(g.End()).Line, 179 linters: linters, 180 } 181 ranges = append(ranges, rng) 182 } 183 } 184 } 185 return 186 } 187 188 func filterIssuesViaDirectives(directives *directiveParser, issues chan *Issue) chan *Issue { 189 out := make(chan *Issue, 1000000) 190 go func() { 191 for issue := range issues { 192 if !directives.IsIgnored(issue) { 193 out <- issue 194 } 195 } 196 197 if config.WarnUnmatchedDirective { 198 for _, issue := range warnOnUnusedDirective(directives) { 199 out <- issue 200 } 201 } 202 close(out) 203 }() 204 return out 205 } 206 207 func warnOnUnusedDirective(directives *directiveParser) []*Issue { 208 out := []*Issue{} 209 210 cwd, err := os.Getwd() 211 if err != nil { 212 warning("failed to get working directory %s", err) 213 } 214 215 for path, ranges := range directives.Unmatched() { 216 for _, ignore := range ranges { 217 issue, _ := NewIssue("nolint", config.formatTemplate) 218 issue.Path = newIssuePath(cwd, path) 219 issue.Line = ignore.start 220 issue.Col = ignore.col 221 issue.Message = "nolint directive did not match any issue" 222 out = append(out, issue) 223 } 224 } 225 return out 226 }