github.com/golangci/go-tools@v0.0.0-20190318060251-af6baa5dc196/lint/testutil/util.go (about)

     1  // Copyright (c) 2013 The Go Authors. All rights reserved.
     2  //
     3  // Use of this source code is governed by a BSD-style
     4  // license that can be found in the LICENSE file or at
     5  // https://developers.google.com/open-source/licenses/bsd.
     6  
     7  // Package testutil provides helpers for testing staticcheck.
     8  package testutil // import "github.com/golangci/go-tools/lint/testutil"
     9  
    10  import (
    11  	"fmt"
    12  	"go/parser"
    13  	"go/token"
    14  	"io/ioutil"
    15  	"os"
    16  	"path/filepath"
    17  	"regexp"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  
    23  	"golang.org/x/tools/go/packages"
    24  	"github.com/golangci/go-tools/config"
    25  	"github.com/golangci/go-tools/lint"
    26  )
    27  
    28  func TestAll(t *testing.T, c lint.Checker, dir string) {
    29  	testPackages(t, c, dir)
    30  }
    31  
    32  func testPackages(t *testing.T, c lint.Checker, dir string) {
    33  	gopath := filepath.Join("testdata", dir)
    34  	gopath, err := filepath.Abs(gopath)
    35  	if err != nil {
    36  		t.Fatal(err)
    37  	}
    38  	fis, err := ioutil.ReadDir(filepath.Join(gopath, "src"))
    39  	if err != nil {
    40  		if os.IsNotExist(err) {
    41  			// no packages to test
    42  			return
    43  		}
    44  		t.Fatal("couldn't get test packages:", err)
    45  	}
    46  
    47  	var paths []string
    48  	for _, fi := range fis {
    49  		if strings.HasSuffix(fi.Name(), ".disabled") {
    50  			continue
    51  		}
    52  		paths = append(paths, fi.Name())
    53  	}
    54  
    55  	conf := &packages.Config{
    56  		Mode:  packages.LoadAllSyntax,
    57  		Tests: true,
    58  		Env:   append(os.Environ(), "GOPATH="+gopath),
    59  	}
    60  
    61  	pkgs, err := packages.Load(conf, paths...)
    62  	if err != nil {
    63  		t.Error("Error loading packages:", err)
    64  		return
    65  	}
    66  
    67  	versions := map[int][]*packages.Package{}
    68  	for _, pkg := range pkgs {
    69  		path := strings.TrimSuffix(pkg.Types.Path(), ".test")
    70  		parts := strings.Split(path, "_")
    71  
    72  		version := 0
    73  		if len(parts) > 1 {
    74  			part := parts[len(parts)-1]
    75  			if len(part) >= 4 && strings.HasPrefix(part, "go1") {
    76  				v, err := strconv.Atoi(part[len("go1"):])
    77  				if err != nil {
    78  					continue
    79  				}
    80  				version = v
    81  			}
    82  		}
    83  		versions[version] = append(versions[version], pkg)
    84  	}
    85  
    86  	for version, pkgs := range versions {
    87  		sources := map[string][]byte{}
    88  		var files []string
    89  
    90  		for _, pkg := range pkgs {
    91  			files = append(files, pkg.GoFiles...)
    92  			for _, fi := range pkg.GoFiles {
    93  				src, err := ioutil.ReadFile(fi)
    94  				if err != nil {
    95  					t.Fatal(err)
    96  				}
    97  				sources[fi] = src
    98  			}
    99  		}
   100  
   101  		sort.Strings(files)
   102  		filesUniq := make([]string, 0, len(files))
   103  		if len(files) < 2 {
   104  			filesUniq = files
   105  		} else {
   106  			filesUniq = append(filesUniq, files[0])
   107  			prev := files[0]
   108  			for _, f := range files[1:] {
   109  				if f == prev {
   110  					continue
   111  				}
   112  				prev = f
   113  				filesUniq = append(filesUniq, f)
   114  			}
   115  		}
   116  
   117  		lintGoVersion(t, c, version, pkgs, filesUniq, sources)
   118  	}
   119  }
   120  
   121  func lintGoVersion(
   122  	t *testing.T,
   123  	c lint.Checker,
   124  	version int,
   125  	pkgs []*packages.Package,
   126  	files []string,
   127  	sources map[string][]byte,
   128  ) {
   129  	l := &lint.Linter{Checkers: []lint.Checker{c}, GoVersion: version, Config: config.Config{Checks: []string{"all"}}}
   130  	problems := l.Lint(pkgs, nil)
   131  
   132  	for _, fi := range files {
   133  		src := sources[fi]
   134  
   135  		ins := parseInstructions(t, fi, src)
   136  
   137  		for _, in := range ins {
   138  			ok := false
   139  			for i, p := range problems {
   140  				if p.Position.Line != in.Line || p.Position.Filename != fi {
   141  					continue
   142  				}
   143  				if in.Match.MatchString(p.Text) {
   144  					// remove this problem from ps
   145  					copy(problems[i:], problems[i+1:])
   146  					problems = problems[:len(problems)-1]
   147  
   148  					ok = true
   149  					break
   150  				}
   151  			}
   152  			if !ok {
   153  				t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi, in.Line, in.Match)
   154  			}
   155  		}
   156  	}
   157  	for _, p := range problems {
   158  		t.Errorf("Unexpected problem at %s: %v", p.Position, p.Text)
   159  	}
   160  }
   161  
   162  type instruction struct {
   163  	Line        int            // the line number this applies to
   164  	Match       *regexp.Regexp // what pattern to match
   165  	Replacement string         // what the suggested replacement line should be
   166  }
   167  
   168  // parseInstructions parses instructions from the comments in a Go source file.
   169  // It returns nil if none were parsed.
   170  func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
   171  	fset := token.NewFileSet()
   172  	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
   173  	if err != nil {
   174  		t.Fatalf("Test file %v does not parse: %v", filename, err)
   175  	}
   176  	var ins []instruction
   177  	for _, cg := range f.Comments {
   178  		ln := fset.PositionFor(cg.Pos(), false).Line
   179  		raw := cg.Text()
   180  		for _, line := range strings.Split(raw, "\n") {
   181  			if line == "" || strings.HasPrefix(line, "#") {
   182  				continue
   183  			}
   184  			if line == "OK" && ins == nil {
   185  				// so our return value will be non-nil
   186  				ins = make([]instruction, 0)
   187  				continue
   188  			}
   189  			if !strings.Contains(line, "MATCH") {
   190  				continue
   191  			}
   192  			rx, err := extractPattern(line)
   193  			if err != nil {
   194  				t.Fatalf("At %v:%d: %v", filename, ln, err)
   195  			}
   196  			matchLine := ln
   197  			if i := strings.Index(line, "MATCH:"); i >= 0 {
   198  				// This is a match for a different line.
   199  				lns := strings.TrimPrefix(line[i:], "MATCH:")
   200  				lns = lns[:strings.Index(lns, " ")]
   201  				matchLine, err = strconv.Atoi(lns)
   202  				if err != nil {
   203  					t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
   204  				}
   205  			}
   206  			var repl string
   207  			if r, ok := extractReplacement(line); ok {
   208  				repl = r
   209  			}
   210  			ins = append(ins, instruction{
   211  				Line:        matchLine,
   212  				Match:       rx,
   213  				Replacement: repl,
   214  			})
   215  		}
   216  	}
   217  	return ins
   218  }
   219  
   220  func extractPattern(line string) (*regexp.Regexp, error) {
   221  	n := strings.Index(line, " ")
   222  	if n == 01 {
   223  		return nil, fmt.Errorf("malformed match instruction %q", line)
   224  	}
   225  	line = line[n+1:]
   226  	var pat string
   227  	switch line[0] {
   228  	case '/':
   229  		a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
   230  		if a == -1 || a == b {
   231  			return nil, fmt.Errorf("malformed match instruction %q", line)
   232  		}
   233  		pat = line[a+1 : b]
   234  	case '"':
   235  		a, b := strings.Index(line, `"`), strings.LastIndex(line, `"`)
   236  		if a == -1 || a == b {
   237  			return nil, fmt.Errorf("malformed match instruction %q", line)
   238  		}
   239  		pat = regexp.QuoteMeta(line[a+1 : b])
   240  	default:
   241  		return nil, fmt.Errorf("malformed match instruction %q", line)
   242  	}
   243  
   244  	rx, err := regexp.Compile(pat)
   245  	if err != nil {
   246  		return nil, fmt.Errorf("bad match pattern %q: %v", pat, err)
   247  	}
   248  	return rx, nil
   249  }
   250  
   251  func extractReplacement(line string) (string, bool) {
   252  	// Look for this:  / -> `
   253  	// (the end of a match and start of a backtick string),
   254  	// and then the closing backtick.
   255  	const start = "/ -> `"
   256  	a, b := strings.Index(line, start), strings.LastIndex(line, "`")
   257  	if a < 0 || a > b {
   258  		return "", false
   259  	}
   260  	return line[a+len(start) : b], true
   261  }