github.com/wgliang/gometalinter@v2.0.6-0.20180523041418-a75adcf7cb0e+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  }