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 }