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() {}