| // Copyright (c) 2013 The Go Authors. All rights reserved. |
| // |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file or at |
| // https://developers.google.com/open-source/licenses/bsd. |
| |
| package lint |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/printer" |
| "go/token" |
| "go/types" |
| "io/ioutil" |
| "path" |
| "regexp" |
| "strconv" |
| "strings" |
| "testing" |
| ) |
| |
| var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern") |
| |
| func TestAll(t *testing.T) { |
| l := new(Linter) |
| rx, err := regexp.Compile(*lintMatch) |
| if err != nil { |
| t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err) |
| } |
| |
| baseDir := "testdata" |
| fis, err := ioutil.ReadDir(baseDir) |
| if err != nil { |
| t.Fatalf("ioutil.ReadDir: %v", err) |
| } |
| if len(fis) == 0 { |
| t.Fatalf("no files in %v", baseDir) |
| } |
| for _, fi := range fis { |
| if !rx.MatchString(fi.Name()) { |
| continue |
| } |
| //t.Logf("Testing %s", fi.Name()) |
| src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name())) |
| if err != nil { |
| t.Fatalf("Failed reading %s: %v", fi.Name(), err) |
| } |
| |
| ins := parseInstructions(t, fi.Name(), src) |
| if ins == nil { |
| t.Errorf("Test file %v does not have instructions", fi.Name()) |
| continue |
| } |
| |
| ps, err := l.Lint(fi.Name(), src) |
| if err != nil { |
| t.Errorf("Linting %s: %v", fi.Name(), err) |
| continue |
| } |
| |
| for _, in := range ins { |
| ok := false |
| for i, p := range ps { |
| if p.Position.Line != in.Line { |
| continue |
| } |
| if in.Match.MatchString(p.Text) { |
| // check replacement if we are expecting one |
| if in.Replacement != "" { |
| // ignore any inline comments, since that would be recursive |
| r := p.ReplacementLine |
| if i := strings.Index(r, " //"); i >= 0 { |
| r = r[:i] |
| } |
| if r != in.Replacement { |
| t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, r, in.Replacement) |
| } |
| } |
| |
| // remove this problem from ps |
| copy(ps[i:], ps[i+1:]) |
| ps = ps[:len(ps)-1] |
| |
| //t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line) |
| ok = true |
| break |
| } |
| } |
| if !ok { |
| t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match) |
| } |
| } |
| for _, p := range ps { |
| t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Line, p.Text) |
| } |
| } |
| } |
| |
| type instruction struct { |
| Line int // the line number this applies to |
| Match *regexp.Regexp // what pattern to match |
| Replacement string // what the suggested replacement line should be |
| } |
| |
| // parseInstructions parses instructions from the comments in a Go source file. |
| // It returns nil if none were parsed. |
| func parseInstructions(t *testing.T, filename string, src []byte) []instruction { |
| fset := token.NewFileSet() |
| f, err := parser.ParseFile(fset, filename, src, parser.ParseComments) |
| if err != nil { |
| t.Fatalf("Test file %v does not parse: %v", filename, err) |
| } |
| var ins []instruction |
| for _, cg := range f.Comments { |
| ln := fset.Position(cg.Pos()).Line |
| raw := cg.Text() |
| for _, line := range strings.Split(raw, "\n") { |
| if line == "" || strings.HasPrefix(line, "#") { |
| continue |
| } |
| if line == "OK" && ins == nil { |
| // so our return value will be non-nil |
| ins = make([]instruction, 0) |
| continue |
| } |
| if strings.Contains(line, "MATCH") { |
| rx, err := extractPattern(line) |
| if err != nil { |
| t.Fatalf("At %v:%d: %v", filename, ln, err) |
| } |
| matchLine := ln |
| if i := strings.Index(line, "MATCH:"); i >= 0 { |
| // This is a match for a different line. |
| lns := strings.TrimPrefix(line[i:], "MATCH:") |
| lns = lns[:strings.Index(lns, " ")] |
| matchLine, err = strconv.Atoi(lns) |
| if err != nil { |
| t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err) |
| } |
| } |
| var repl string |
| if r, ok := extractReplacement(line); ok { |
| repl = r |
| } |
| ins = append(ins, instruction{ |
| Line: matchLine, |
| Match: rx, |
| Replacement: repl, |
| }) |
| } |
| } |
| } |
| return ins |
| } |
| |
| func extractPattern(line string) (*regexp.Regexp, error) { |
| a, b := strings.Index(line, "/"), strings.LastIndex(line, "/") |
| if a == -1 || a == b { |
| return nil, fmt.Errorf("malformed match instruction %q", line) |
| } |
| pat := line[a+1 : b] |
| rx, err := regexp.Compile(pat) |
| if err != nil { |
| return nil, fmt.Errorf("bad match pattern %q: %v", pat, err) |
| } |
| return rx, nil |
| } |
| |
| func extractReplacement(line string) (string, bool) { |
| // Look for this: / -> ` |
| // (the end of a match and start of a backtick string), |
| // and then the closing backtick. |
| const start = "/ -> `" |
| a, b := strings.Index(line, start), strings.LastIndex(line, "`") |
| if a < 0 || a > b { |
| return "", false |
| } |
| return line[a+len(start) : b], true |
| } |
| |
| func render(fset *token.FileSet, x interface{}) string { |
| var buf bytes.Buffer |
| if err := printer.Fprint(&buf, fset, x); err != nil { |
| panic(err) |
| } |
| return buf.String() |
| } |
| |
| func TestLine(t *testing.T) { |
| tests := []struct { |
| src string |
| offset int |
| want string |
| }{ |
| {"single line file", 5, "single line file"}, |
| {"single line file with newline\n", 5, "single line file with newline\n"}, |
| {"first\nsecond\nthird\n", 2, "first\n"}, |
| {"first\nsecond\nthird\n", 9, "second\n"}, |
| {"first\nsecond\nthird\n", 14, "third\n"}, |
| {"first\nsecond\nthird with no newline", 16, "third with no newline"}, |
| {"first byte\n", 0, "first byte\n"}, |
| } |
| for _, test := range tests { |
| got := srcLine([]byte(test.src), token.Position{Offset: test.offset}) |
| if got != test.want { |
| t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want) |
| } |
| } |
| } |
| |
| func TestLintName(t *testing.T) { |
| tests := []struct { |
| name, want string |
| }{ |
| {"foo_bar", "fooBar"}, |
| {"foo_bar_baz", "fooBarBaz"}, |
| {"Foo_bar", "FooBar"}, |
| {"foo_WiFi", "fooWiFi"}, |
| {"id", "id"}, |
| {"Id", "ID"}, |
| {"foo_id", "fooID"}, |
| {"fooId", "fooID"}, |
| {"fooUid", "fooUID"}, |
| {"idFoo", "idFoo"}, |
| {"uidFoo", "uidFoo"}, |
| {"midIdDle", "midIDDle"}, |
| {"APIProxy", "APIProxy"}, |
| {"ApiProxy", "APIProxy"}, |
| {"apiProxy", "apiProxy"}, |
| {"_Leading", "_Leading"}, |
| {"___Leading", "_Leading"}, |
| {"trailing_", "trailing"}, |
| {"trailing___", "trailing"}, |
| {"a_b", "aB"}, |
| {"a__b", "aB"}, |
| {"a___b", "aB"}, |
| {"Rpc1150", "RPC1150"}, |
| {"case3_1", "case3_1"}, |
| {"case3__1", "case3_1"}, |
| {"IEEE802_16bit", "IEEE802_16bit"}, |
| {"IEEE802_16Bit", "IEEE802_16Bit"}, |
| } |
| for _, test := range tests { |
| got := lintName(test.name) |
| if got != test.want { |
| t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want) |
| } |
| } |
| } |
| |
| func TestExportedType(t *testing.T) { |
| tests := []struct { |
| typString string |
| exp bool |
| }{ |
| {"int", true}, |
| {"string", false}, // references the shadowed builtin "string" |
| {"T", true}, |
| {"t", false}, |
| {"*T", true}, |
| {"*t", false}, |
| {"map[int]complex128", true}, |
| } |
| for _, test := range tests { |
| src := `package foo; type T int; type t int; type string struct{}` |
| fset := token.NewFileSet() |
| file, err := parser.ParseFile(fset, "foo.go", src, 0) |
| if err != nil { |
| t.Fatalf("Parsing %q: %v", src, err) |
| } |
| // use the package name as package path |
| config := &types.Config{} |
| pkg, err := config.Check(file.Name.Name, fset, []*ast.File{file}, nil) |
| if err != nil { |
| t.Fatalf("Type checking %q: %v", src, err) |
| } |
| tv, err := types.Eval(fset, pkg, token.NoPos, test.typString) |
| if err != nil { |
| t.Errorf("types.Eval(%q): %v", test.typString, err) |
| continue |
| } |
| if got := exportedType(tv.Type); got != test.exp { |
| t.Errorf("exportedType(%v) = %t, want %t", tv.Type, got, test.exp) |
| } |
| } |
| } |
| |
| func TestIsGenerated(t *testing.T) { |
| tests := []struct { |
| source string |
| generated bool |
| }{ |
| {"// Code Generated by some tool. DO NOT EDIT.", false}, |
| {"// Code generated by some tool. DO NOT EDIT.", true}, |
| {"// Code generated by some tool. DO NOT EDIT", false}, |
| {"// Code generated DO NOT EDIT.", true}, |
| {"// Code generated DO NOT EDIT.", false}, |
| {"\t\t// Code generated by some tool. DO NOT EDIT.\npackage foo\n", false}, |
| {"// Code generated by some tool. DO NOT EDIT.\npackage foo\n", true}, |
| {"package foo\n// Code generated by some tool. DO NOT EDIT.\ntype foo int\n", true}, |
| {"package foo\n // Code generated by some tool. DO NOT EDIT.\ntype foo int\n", false}, |
| {"package foo\n// Code generated by some tool. DO NOT EDIT. \ntype foo int\n", false}, |
| {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.\n", true}, |
| {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.", true}, |
| } |
| |
| for i, test := range tests { |
| got := isGenerated([]byte(test.source)) |
| if got != test.generated { |
| t.Errorf("test %d, isGenerated() = %v, want %v", i, got, test.generated) |
| } |
| } |
| } |