package processors import ( "bufio" "bytes" "fmt" "go/ast" "go/parser" "go/token" "io/ioutil" "sort" "strings" "github.com/golangci/golangci-lint/pkg/result" ) type ignoredRange struct { linters []string result.Range col int } func (i *ignoredRange) isAdjacent(col, start int) bool { return col == i.col && i.To == start-1 } func (i *ignoredRange) doesMatch(issue *result.Issue) bool { if issue.Line() < i.From || issue.Line() > i.To { return false } if len(i.linters) == 0 { return true } for _, l := range i.linters { if l == issue.FromLinter { return true } } return false } type fileData struct { ignoredRanges []ignoredRange isGenerated bool } type filesCache map[string]*fileData type Nolint struct { fset *token.FileSet cache filesCache } func NewNolint(fset *token.FileSet) *Nolint { return &Nolint{ fset: fset, cache: filesCache{}, } } var _ Processor = &Nolint{} func (p Nolint) Name() string { return "nolint" } func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) { return filterIssuesErr(issues, p.shouldPassIssue) } var ( genHdr = []byte("// Code generated") genFtr = []byte("DO NOT EDIT") ) // isGenerated reports whether the source file is generated code. // Using a bit laxer rules than https://golang.org/s/generatedcode to // match more generated code. func isGenerated(src []byte) bool { sc := bufio.NewScanner(bytes.NewReader(src)) var hdr, ftr bool for sc.Scan() { b := sc.Bytes() if bytes.HasPrefix(b, genHdr) { hdr = true } if bytes.Contains(b, genFtr) { ftr = true } } return hdr && ftr } func (p *Nolint) getOrCreateFileData(i *result.Issue) (*fileData, error) { fd := p.cache[i.FilePath()] if fd != nil { return fd, nil } fd = &fileData{} p.cache[i.FilePath()] = fd src, err := ioutil.ReadFile(i.FilePath()) if err != nil { return nil, fmt.Errorf("can't read file %s: %s", i.FilePath(), err) } fd.isGenerated = isGenerated(src) if fd.isGenerated { // don't report issues for autogenerated files return fd, nil } file, err := parser.ParseFile(p.fset, i.FilePath(), src, parser.ParseComments) if err != nil { return nil, fmt.Errorf("can't parse file %s", i.FilePath()) } fd.ignoredRanges = buildIgnoredRangesForFile(file, p.fset) return fd, nil } func buildIgnoredRangesForFile(f *ast.File, fset *token.FileSet) []ignoredRange { inlineRanges := extractFileCommentsInlineRanges(fset, f.Comments...) if len(inlineRanges) == 0 { return nil } e := rangeExpander{ fset: fset, ranges: ignoredRanges(inlineRanges), } ast.Walk(&e, f) return e.ranges } func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) { fd, err := p.getOrCreateFileData(i) if err != nil { return false, err } if fd.isGenerated { // don't report issues for autogenerated files return false, nil } for _, ir := range fd.ignoredRanges { if ir.doesMatch(i) { return false, nil } } return true, nil } type ignoredRanges []ignoredRange func (ir ignoredRanges) Len() int { return len(ir) } func (ir ignoredRanges) Swap(i, j int) { ir[i], ir[j] = ir[j], ir[i] } func (ir ignoredRanges) Less(i, j int) bool { return ir[i].To < ir[j].To } type rangeExpander struct { fset *token.FileSet ranges ignoredRanges } func (e *rangeExpander) Visit(node ast.Node) ast.Visitor { if node == nil { return e } startPos := e.fset.Position(node.Pos()) start := startPos.Line end := e.fset.Position(node.End()).Line found := sort.Search(len(e.ranges), func(i int) bool { return e.ranges[i].To+1 >= start }) if found < len(e.ranges) && e.ranges[found].isAdjacent(startPos.Column, start) { r := &e.ranges[found] if r.From > start { r.From = start } if r.To < end { r.To = end } } return e } func extractFileCommentsInlineRanges(fset *token.FileSet, comments ...*ast.CommentGroup) []ignoredRange { var ret []ignoredRange for _, g := range comments { for _, c := range g.List { text := strings.TrimLeft(c.Text, "/ ") if !strings.HasPrefix(text, "nolint") { continue } var linters []string if strings.HasPrefix(text, "nolint:") { // ignore specific linters text = strings.Split(text, "//")[0] // allow another comment after this comment linterItems := strings.Split(strings.TrimPrefix(text, "nolint:"), ",") for _, linter := range linterItems { linterName := strings.TrimSpace(linter) // TODO: validate it here linters = append(linters, linterName) } } // else ignore all linters pos := fset.Position(g.Pos()) ret = append(ret, ignoredRange{ Range: result.Range{ From: pos.Line, To: fset.Position(g.End()).Line, }, col: pos.Column, linters: linters, }) } } return ret } func (p Nolint) Finish() {}