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
7package lint
8
9import (
10	"bytes"
11	"flag"
12	"fmt"
13	"go/ast"
14	"go/parser"
15	"go/printer"
16	"go/token"
17	"go/types"
18	"io/ioutil"
19	"path"
20	"regexp"
21	"strconv"
22	"strings"
23	"testing"
24)
25
26var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
27
28func TestAll(t *testing.T) {
29	l := new(Linter)
30	rx, err := regexp.Compile(*lintMatch)
31	if err != nil {
32		t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
33	}
34
35	baseDir := "testdata"
36	fis, err := ioutil.ReadDir(baseDir)
37	if err != nil {
38		t.Fatalf("ioutil.ReadDir: %v", err)
39	}
40	if len(fis) == 0 {
41		t.Fatalf("no files in %v", baseDir)
42	}
43	for _, fi := range fis {
44		if !rx.MatchString(fi.Name()) {
45			continue
46		}
47		//t.Logf("Testing %s", fi.Name())
48		src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name()))
49		if err != nil {
50			t.Fatalf("Failed reading %s: %v", fi.Name(), err)
51		}
52
53		ins := parseInstructions(t, fi.Name(), src)
54		if ins == nil {
55			t.Errorf("Test file %v does not have instructions", fi.Name())
56			continue
57		}
58
59		ps, err := l.Lint(fi.Name(), src)
60		if err != nil {
61			t.Errorf("Linting %s: %v", fi.Name(), err)
62			continue
63		}
64
65		for _, in := range ins {
66			ok := false
67			for i, p := range ps {
68				if p.Position.Line != in.Line {
69					continue
70				}
71				if in.Match.MatchString(p.Text) {
72					// check replacement if we are expecting one
73					if in.Replacement != "" {
74						// ignore any inline comments, since that would be recursive
75						r := p.ReplacementLine
76						if i := strings.Index(r, " //"); i >= 0 {
77							r = r[:i]
78						}
79						if r != in.Replacement {
80							t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, r, in.Replacement)
81						}
82					}
83
84					// remove this problem from ps
85					copy(ps[i:], ps[i+1:])
86					ps = ps[:len(ps)-1]
87
88					//t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
89					ok = true
90					break
91				}
92			}
93			if !ok {
94				t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
95			}
96		}
97		for _, p := range ps {
98			t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Line, p.Text)
99		}
100	}
101}
102
103type instruction struct {
104	Line        int            // the line number this applies to
105	Match       *regexp.Regexp // what pattern to match
106	Replacement string         // what the suggested replacement line should be
107}
108
109// parseInstructions parses instructions from the comments in a Go source file.
110// It returns nil if none were parsed.
111func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
112	fset := token.NewFileSet()
113	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
114	if err != nil {
115		t.Fatalf("Test file %v does not parse: %v", filename, err)
116	}
117	var ins []instruction
118	for _, cg := range f.Comments {
119		ln := fset.Position(cg.Pos()).Line
120		raw := cg.Text()
121		for _, line := range strings.Split(raw, "\n") {
122			if line == "" || strings.HasPrefix(line, "#") {
123				continue
124			}
125			if line == "OK" && ins == nil {
126				// so our return value will be non-nil
127				ins = make([]instruction, 0)
128				continue
129			}
130			if strings.Contains(line, "MATCH") {
131				rx, err := extractPattern(line)
132				if err != nil {
133					t.Fatalf("At %v:%d: %v", filename, ln, err)
134				}
135				matchLine := ln
136				if i := strings.Index(line, "MATCH:"); i >= 0 {
137					// This is a match for a different line.
138					lns := strings.TrimPrefix(line[i:], "MATCH:")
139					lns = lns[:strings.Index(lns, " ")]
140					matchLine, err = strconv.Atoi(lns)
141					if err != nil {
142						t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err)
143					}
144				}
145				var repl string
146				if r, ok := extractReplacement(line); ok {
147					repl = r
148				}
149				ins = append(ins, instruction{
150					Line:        matchLine,
151					Match:       rx,
152					Replacement: repl,
153				})
154			}
155		}
156	}
157	return ins
158}
159
160func extractPattern(line string) (*regexp.Regexp, error) {
161	a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
162	if a == -1 || a == b {
163		return nil, fmt.Errorf("malformed match instruction %q", line)
164	}
165	pat := line[a+1 : b]
166	rx, err := regexp.Compile(pat)
167	if err != nil {
168		return nil, fmt.Errorf("bad match pattern %q: %v", pat, err)
169	}
170	return rx, nil
171}
172
173func extractReplacement(line string) (string, bool) {
174	// Look for this:  / -> `
175	// (the end of a match and start of a backtick string),
176	// and then the closing backtick.
177	const start = "/ -> `"
178	a, b := strings.Index(line, start), strings.LastIndex(line, "`")
179	if a < 0 || a > b {
180		return "", false
181	}
182	return line[a+len(start) : b], true
183}
184
185func render(fset *token.FileSet, x interface{}) string {
186	var buf bytes.Buffer
187	if err := printer.Fprint(&buf, fset, x); err != nil {
188		panic(err)
189	}
190	return buf.String()
191}
192
193func TestLine(t *testing.T) {
194	tests := []struct {
195		src    string
196		offset int
197		want   string
198	}{
199		{"single line file", 5, "single line file"},
200		{"single line file with newline\n", 5, "single line file with newline\n"},
201		{"first\nsecond\nthird\n", 2, "first\n"},
202		{"first\nsecond\nthird\n", 9, "second\n"},
203		{"first\nsecond\nthird\n", 14, "third\n"},
204		{"first\nsecond\nthird with no newline", 16, "third with no newline"},
205		{"first byte\n", 0, "first byte\n"},
206	}
207	for _, test := range tests {
208		got := srcLine([]byte(test.src), token.Position{Offset: test.offset})
209		if got != test.want {
210			t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want)
211		}
212	}
213}
214
215func TestLintName(t *testing.T) {
216	tests := []struct {
217		name, want string
218	}{
219		{"foo_bar", "fooBar"},
220		{"foo_bar_baz", "fooBarBaz"},
221		{"Foo_bar", "FooBar"},
222		{"foo_WiFi", "fooWiFi"},
223		{"id", "id"},
224		{"Id", "ID"},
225		{"foo_id", "fooID"},
226		{"fooId", "fooID"},
227		{"fooUid", "fooUID"},
228		{"idFoo", "idFoo"},
229		{"uidFoo", "uidFoo"},
230		{"midIdDle", "midIDDle"},
231		{"APIProxy", "APIProxy"},
232		{"ApiProxy", "APIProxy"},
233		{"apiProxy", "apiProxy"},
234		{"_Leading", "_Leading"},
235		{"___Leading", "_Leading"},
236		{"trailing_", "trailing"},
237		{"trailing___", "trailing"},
238		{"a_b", "aB"},
239		{"a__b", "aB"},
240		{"a___b", "aB"},
241		{"Rpc1150", "RPC1150"},
242		{"case3_1", "case3_1"},
243		{"case3__1", "case3_1"},
244		{"IEEE802_16bit", "IEEE802_16bit"},
245		{"IEEE802_16Bit", "IEEE802_16Bit"},
246	}
247	for _, test := range tests {
248		got := lintName(test.name)
249		if got != test.want {
250			t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want)
251		}
252	}
253}
254
255func TestExportedType(t *testing.T) {
256	tests := []struct {
257		typString string
258		exp       bool
259	}{
260		{"int", true},
261		{"string", false}, // references the shadowed builtin "string"
262		{"T", true},
263		{"t", false},
264		{"*T", true},
265		{"*t", false},
266		{"map[int]complex128", true},
267	}
268	for _, test := range tests {
269		src := `package foo; type T int; type t int; type string struct{}`
270		fset := token.NewFileSet()
271		file, err := parser.ParseFile(fset, "foo.go", src, 0)
272		if err != nil {
273			t.Fatalf("Parsing %q: %v", src, err)
274		}
275		// use the package name as package path
276		config := &types.Config{}
277		pkg, err := config.Check(file.Name.Name, fset, []*ast.File{file}, nil)
278		if err != nil {
279			t.Fatalf("Type checking %q: %v", src, err)
280		}
281		tv, err := types.Eval(fset, pkg, token.NoPos, test.typString)
282		if err != nil {
283			t.Errorf("types.Eval(%q): %v", test.typString, err)
284			continue
285		}
286		if got := exportedType(tv.Type); got != test.exp {
287			t.Errorf("exportedType(%v) = %t, want %t", tv.Type, got, test.exp)
288		}
289	}
290}
291
292func TestIsGenerated(t *testing.T) {
293	tests := []struct {
294		source    string
295		generated bool
296	}{
297		{"// Code Generated by some tool. DO NOT EDIT.", false},
298		{"// Code generated by some tool. DO NOT EDIT.", true},
299		{"// Code generated by some tool. DO NOT EDIT", false},
300		{"// Code generated  DO NOT EDIT.", true},
301		{"// Code generated DO NOT EDIT.", false},
302		{"\t\t// Code generated by some tool. DO NOT EDIT.\npackage foo\n", false},
303		{"// Code generated by some tool. DO NOT EDIT.\npackage foo\n", true},
304		{"package foo\n// Code generated by some tool. DO NOT EDIT.\ntype foo int\n", true},
305		{"package foo\n // Code generated by some tool. DO NOT EDIT.\ntype foo int\n", false},
306		{"package foo\n// Code generated by some tool. DO NOT EDIT. \ntype foo int\n", false},
307		{"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.\n", true},
308		{"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.", true},
309	}
310
311	for i, test := range tests {
312		got := isGenerated([]byte(test.source))
313		if got != test.generated {
314			t.Errorf("test %d, isGenerated() = %v, want %v", i, got, test.generated)
315		}
316	}
317}
318