github.com/pankona/gometalinter@v2.0.11+incompatible/_linters/src/honnef.co/go/tools/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 // import "honnef.co/go/tools/lint/testutil"
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"go/parser"
    13  	"go/token"
    14  	"io/ioutil"
    15  	"os"
    16  	"path"
    17  	"path/filepath"
    18  	"regexp"
    19  	"strconv"
    20  	"strings"
    21  	"testing"
    22  
    23  	"honnef.co/go/tools/lint"
    24  
    25  	"golang.org/x/tools/go/loader"
    26  )
    27  
    28  var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
    29  
    30  func TestAll(t *testing.T, c lint.Checker, dir string) {
    31  	baseDir := filepath.Join("testdata", dir)
    32  	fis, err := ioutil.ReadDir(baseDir)
    33  	if err != nil {
    34  		t.Fatalf("ioutil.ReadDir: %v", err)
    35  	}
    36  	if len(fis) == 0 {
    37  		t.Fatalf("no files in %v", baseDir)
    38  	}
    39  	rx, err := regexp.Compile(*lintMatch)
    40  	if err != nil {
    41  		t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
    42  	}
    43  
    44  	files := map[int][]os.FileInfo{}
    45  	for _, fi := range fis {
    46  		if !rx.MatchString(fi.Name()) {
    47  			continue
    48  		}
    49  		if !strings.HasSuffix(fi.Name(), ".go") {
    50  			continue
    51  		}
    52  		parts := strings.Split(fi.Name(), "_")
    53  		v := 0
    54  		if len(parts) > 1 && strings.HasPrefix(parts[len(parts)-1], "go1") {
    55  			var err error
    56  			s := parts[len(parts)-1][len("go1"):]
    57  			s = s[:len(s)-len(".go")]
    58  			v, err = strconv.Atoi(s)
    59  			if err != nil {
    60  				t.Fatalf("cannot process file name %q: %s", fi.Name(), err)
    61  			}
    62  		}
    63  		files[v] = append(files[v], fi)
    64  	}
    65  
    66  	conf := &loader.Config{
    67  		ParserMode: parser.ParseComments,
    68  	}
    69  	sources := map[string][]byte{}
    70  	for _, fi := range fis {
    71  		filename := path.Join(baseDir, fi.Name())
    72  		src, err := ioutil.ReadFile(filename)
    73  		if err != nil {
    74  			t.Errorf("Failed reading %s: %v", fi.Name(), err)
    75  			continue
    76  		}
    77  		f, err := conf.ParseFile(filename, src)
    78  		if err != nil {
    79  			t.Errorf("error parsing %s: %s", filename, err)
    80  			continue
    81  		}
    82  		sources[fi.Name()] = src
    83  		conf.CreateFromFiles(fi.Name(), f)
    84  	}
    85  
    86  	lprog, err := conf.Load()
    87  	if err != nil {
    88  		t.Fatalf("error loading program: %s", err)
    89  	}
    90  
    91  	for version, fis := range files {
    92  		l := &lint.Linter{Checker: c, GoVersion: version}
    93  
    94  		res := l.Lint(lprog, conf)
    95  		for _, fi := range fis {
    96  			name := fi.Name()
    97  			src := sources[name]
    98  
    99  			ins := parseInstructions(t, name, src)
   100  
   101  			for _, in := range ins {
   102  				ok := false
   103  				for i, p := range res {
   104  					if p.Position.Line != in.Line || filepath.Base(p.Position.Filename) != name {
   105  						continue
   106  					}
   107  					if in.Match.MatchString(p.Text) {
   108  						// remove this problem from ps
   109  						copy(res[i:], res[i+1:])
   110  						res = res[:len(res)-1]
   111  
   112  						//t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
   113  						ok = true
   114  						break
   115  					}
   116  				}
   117  				if !ok {
   118  					t.Errorf("Lint failed at %s:%d; /%v/ did not match", name, in.Line, in.Match)
   119  				}
   120  			}
   121  		}
   122  		for _, p := range res {
   123  			name := filepath.Base(p.Position.Filename)
   124  			for _, fi := range fis {
   125  				if name == fi.Name() {
   126  					t.Errorf("Unexpected problem at %s: %v", p.Position, p.Text)
   127  					break
   128  				}
   129  			}
   130  		}
   131  	}
   132  }
   133  
   134  type instruction struct {
   135  	Line        int            // the line number this applies to
   136  	Match       *regexp.Regexp // what pattern to match
   137  	Replacement string         // what the suggested replacement line should be
   138  }
   139  
   140  // parseInstructions parses instructions from the comments in a Go source file.
   141  // It returns nil if none were parsed.
   142  func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
   143  	fset := token.NewFileSet()
   144  	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
   145  	if err != nil {
   146  		t.Fatalf("Test file %v does not parse: %v", filename, err)
   147  	}
   148  	var ins []instruction
   149  	for _, cg := range f.Comments {
   150  		ln := fset.PositionFor(cg.Pos(), false).Line
   151  		raw := cg.Text()
   152  		for _, line := range strings.Split(raw, "\n") {
   153  			if line == "" || strings.HasPrefix(line, "#") {
   154  				continue
   155  			}
   156  			if line == "OK" && ins == nil {
   157  				// so our return value will be non-nil
   158  				ins = make([]instruction, 0)
   159  				continue
   160  			}
   161  			if !strings.Contains(line, "MATCH") {
   162  				continue
   163  			}
   164  			rx, err := extractPattern(line)
   165  			if err != nil {
   166  				t.Fatalf("At %v:%d: %v", filename, ln, err)
   167  			}
   168  			matchLine := ln
   169  			if i := strings.Index(line, "MATCH:"); i >= 0 {
   170  				// This is a match for a different line.
   171  				lns := strings.TrimPrefix(line[i:], "MATCH:")
   172  				lns = lns[:strings.Index(lns, " ")]
   173  				matchLine, err = strconv.Atoi(lns)
   174  				if err != nil {
   175  					t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
   176  				}
   177  			}
   178  			var repl string
   179  			if r, ok := extractReplacement(line); ok {
   180  				repl = r
   181  			}
   182  			ins = append(ins, instruction{
   183  				Line:        matchLine,
   184  				Match:       rx,
   185  				Replacement: repl,
   186  			})
   187  		}
   188  	}
   189  	return ins
   190  }
   191  
   192  func extractPattern(line string) (*regexp.Regexp, error) {
   193  	n := strings.Index(line, " ")
   194  	if n == 01 {
   195  		return nil, fmt.Errorf("malformed match instruction %q", line)
   196  	}
   197  	line = line[n+1:]
   198  	var pat string
   199  	switch line[0] {
   200  	case '/':
   201  		a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
   202  		if a == -1 || a == b {
   203  			return nil, fmt.Errorf("malformed match instruction %q", line)
   204  		}
   205  		pat = line[a+1 : b]
   206  	case '"':
   207  		a, b := strings.Index(line, `"`), strings.LastIndex(line, `"`)
   208  		if a == -1 || a == b {
   209  			return nil, fmt.Errorf("malformed match instruction %q", line)
   210  		}
   211  		pat = regexp.QuoteMeta(line[a+1 : b])
   212  	default:
   213  		return nil, fmt.Errorf("malformed match instruction %q", line)
   214  	}
   215  
   216  	rx, err := regexp.Compile(pat)
   217  	if err != nil {
   218  		return nil, fmt.Errorf("bad match pattern %q: %v", pat, err)
   219  	}
   220  	return rx, nil
   221  }
   222  
   223  func extractReplacement(line string) (string, bool) {
   224  	// Look for this:  / -> `
   225  	// (the end of a match and start of a backtick string),
   226  	// and then the closing backtick.
   227  	const start = "/ -> `"
   228  	a, b := strings.Index(line, start), strings.LastIndex(line, "`")
   229  	if a < 0 || a > b {
   230  		return "", false
   231  	}
   232  	return line[a+len(start) : b], true
   233  }