github.com/alecthomas/golangci-lint@v1.4.2-0.20180609094924-581a3564ff68/pkg/result/processors/nolint.go (about) 1 package processors 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "go/ast" 8 "go/parser" 9 "go/token" 10 "io/ioutil" 11 "sort" 12 "strings" 13 14 "github.com/golangci/golangci-lint/pkg/result" 15 ) 16 17 type ignoredRange struct { 18 linters []string 19 result.Range 20 col int 21 } 22 23 func (i *ignoredRange) isAdjacent(col, start int) bool { 24 return col == i.col && i.To == start-1 25 } 26 27 func (i *ignoredRange) doesMatch(issue *result.Issue) bool { 28 if issue.Line() < i.From || issue.Line() > i.To { 29 return false 30 } 31 32 if len(i.linters) == 0 { 33 return true 34 } 35 36 for _, l := range i.linters { 37 if l == issue.FromLinter { 38 return true 39 } 40 } 41 42 return false 43 } 44 45 type fileData struct { 46 ignoredRanges []ignoredRange 47 isGenerated bool 48 } 49 50 type filesCache map[string]*fileData 51 52 type Nolint struct { 53 fset *token.FileSet 54 cache filesCache 55 } 56 57 func NewNolint(fset *token.FileSet) *Nolint { 58 return &Nolint{ 59 fset: fset, 60 cache: filesCache{}, 61 } 62 } 63 64 var _ Processor = &Nolint{} 65 66 func (p Nolint) Name() string { 67 return "nolint" 68 } 69 70 func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) { 71 return filterIssuesErr(issues, p.shouldPassIssue) 72 } 73 74 var ( 75 genHdr = []byte("// Code generated") 76 genFtr = []byte("DO NOT EDIT") 77 ) 78 79 // isGenerated reports whether the source file is generated code. 80 // Using a bit laxer rules than https://golang.org/s/generatedcode to 81 // match more generated code. 82 func isGenerated(src []byte) bool { 83 sc := bufio.NewScanner(bytes.NewReader(src)) 84 var hdr, ftr bool 85 for sc.Scan() { 86 b := sc.Bytes() 87 if bytes.HasPrefix(b, genHdr) { 88 hdr = true 89 } 90 if bytes.Contains(b, genFtr) { 91 ftr = true 92 } 93 } 94 return hdr && ftr 95 } 96 97 func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) { 98 fd := p.cache[i.FilePath()] 99 if fd != nil { 100 return fd, nil 101 } 102 103 fd = &fileData{} 104 p.cache[i.FilePath()] = fd 105 106 src, err := ioutil.ReadFile(i.FilePath()) 107 if err != nil { 108 return nil, fmt.Errorf("can't read file %s: %s", i.FilePath(), err) 109 } 110 111 fd.isGenerated = isGenerated(src) 112 if fd.isGenerated { // don't report issues for autogenerated files 113 return fd, nil 114 } 115 116 file, err := parser.ParseFile(p.fset, i.FilePath(), src, parser.ParseComments) 117 if err != nil { 118 return nil, fmt.Errorf("can't parse file %s", i.FilePath()) 119 } 120 121 fd.ignoredRanges = buildIgnoredRangesForFile(file, p.fset) 122 return fd, nil 123 } 124 125 func buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet) []ignoredRange { 126 inlineRanges := extractFileCommentsInlineRanges(fset, f.Comments...) 127 128 if len(inlineRanges) == 0 { 129 return nil 130 } 131 132 e := rangeExpander{ 133 fset: fset, 134 ranges: ignoredRanges(inlineRanges), 135 } 136 137 ast.Walk(&e, f) 138 139 return e.ranges 140 } 141 142 func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) { 143 fd, err := p.getOrCreateFileData(i) 144 if err != nil { 145 return false, err 146 } 147 148 if fd.isGenerated { // don't report issues for autogenerated files 149 return false, nil 150 } 151 152 for _, ir := range fd.ignoredRanges { 153 if ir.doesMatch(i) { 154 return false, nil 155 } 156 } 157 158 return true, nil 159 } 160 161 type ignoredRanges []ignoredRange 162 163 func (ir ignoredRanges) Len() int { return len(ir) } 164 func (ir ignoredRanges) Swap(i, j int) { ir[i], ir[j] = ir[j], ir[i] } 165 func (ir ignoredRanges) Less(i, j int) bool { return ir[i].To < ir[j].To } 166 167 type rangeExpander struct { 168 fset *token.FileSet 169 ranges ignoredRanges 170 } 171 172 func (e *rangeExpander) Visit(node ast.Node) ast.Visitor { 173 if node == nil { 174 return e 175 } 176 177 startPos := e.fset.Position(node.Pos()) 178 start := startPos.Line 179 end := e.fset.Position(node.End()).Line 180 found := sort.Search(len(e.ranges), func(i int) bool { 181 return e.ranges[i].To+1 >= start 182 }) 183 184 if found < len(e.ranges) && e.ranges[found].isAdjacent(startPos.Column, start) { 185 r := &e.ranges[found] 186 if r.From > start { 187 r.From = start 188 } 189 if r.To < end { 190 r.To = end 191 } 192 } 193 194 return e 195 } 196 197 func extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange { 198 var ret []ignoredRange 199 for _, g := range comments { 200 for _, c := range g.List { 201 text := strings.TrimLeft(c.Text, "/ ") 202 if !strings.HasPrefix(text, "nolint") { 203 continue 204 } 205 206 var linters []string 207 if strings.HasPrefix(text, "nolint:") { 208 // ignore specific linters 209 text = strings.Split(text, "//")[0] // allow another comment after this comment 210 linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",") 211 for _, linter := range linterItems { 212 linterName := strings.TrimSpace(linter) // TODO: validate it here 213 linters = append(linters, linterName) 214 } 215 } // else ignore all linters 216 217 pos := fset.Position(g.Pos()) 218 ret = append(ret, ignoredRange{ 219 Range: result.Range{ 220 From: pos.Line, 221 To: fset.Position(g.End()).Line, 222 }, 223 col: pos.Column, 224 linters: linters, 225 }) 226 } 227 } 228 229 return ret 230 } 231 232 func (p Nolint) Finish() {}