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	"go/parser"
13	"go/printer"
14	"go/token"
15	"io/ioutil"
16	"path"
17	"regexp"
18	"strings"
19	"testing"
20)
21
22var lintMatch = flag.String("lint.match", "", "restrict testdata matches to this pattern")
23
24func TestAll(t *testing.T) {
25	l := new(Linter)
26	rx, err := regexp.Compile(*lintMatch)
27	if err != nil {
28		t.Fatalf("Bad -lint.match value %q: %v", *lintMatch, err)
29	}
30
31	baseDir := "testdata"
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	for _, fi := range fis {
40		if !rx.MatchString(fi.Name()) {
41			continue
42		}
43		//t.Logf("Testing %s", fi.Name())
44		src, err := ioutil.ReadFile(path.Join(baseDir, fi.Name()))
45		if err != nil {
46			t.Fatalf("Failed reading %s: %v", fi.Name(), err)
47		}
48
49		ins := parseInstructions(t, fi.Name(), src)
50		if ins == nil {
51			t.Errorf("Test file %v does not have instructions", fi.Name())
52			continue
53		}
54
55		ps, err := l.Lint(fi.Name(), src)
56		if err != nil {
57			t.Errorf("Linting %s: %v", fi.Name(), err)
58			continue
59		}
60
61		for _, in := range ins {
62			ok := false
63			for i, p := range ps {
64				if p.Position.Line != in.Line {
65					continue
66				}
67				if in.Match.MatchString(p.Text) {
68					// remove this problem from ps
69					copy(ps[i:], ps[i+1:])
70					ps = ps[:len(ps)-1]
71
72					//t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line)
73					ok = true
74					break
75				}
76			}
77			if !ok {
78				t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match)
79			}
80		}
81		for _, p := range ps {
82			t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Line, p.Text)
83		}
84	}
85}
86
87type instruction struct {
88	Line  int            // the line number this applies to
89	Match *regexp.Regexp // what pattern to match
90}
91
92// parseInstructions parses instructions from the comments in a Go source file.
93// It returns nil if none were parsed.
94func parseInstructions(t *testing.T, filename string, src []byte) []instruction {
95	fset := token.NewFileSet()
96	f, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
97	if err != nil {
98		t.Fatalf("Test file %v does not parse: %v", filename, err)
99	}
100	var ins []instruction
101	for _, cg := range f.Comments {
102		ln := fset.Position(cg.Pos()).Line
103		raw := cg.Text()
104		for _, line := range strings.Split(raw, "\n") {
105			if line == "" || strings.HasPrefix(line, "#") {
106				continue
107			}
108			if line == "OK" && ins == nil {
109				// so our return value will be non-nil
110				ins = make([]instruction, 0)
111				continue
112			}
113			if strings.Contains(line, "MATCH") {
114				a, b := strings.Index(line, "/"), strings.LastIndex(line, "/")
115				if a == -1 || a == b {
116					t.Fatalf("Malformed match instruction %q at %v:%d", line, filename, ln)
117				}
118				pat := line[a+1 : b]
119				rx, err := regexp.Compile(pat)
120				if err != nil {
121					t.Fatalf("Bad match pattern %q at %v:%d: %v", pat, filename, ln, err)
122				}
123				ins = append(ins, instruction{
124					Line:  ln,
125					Match: rx,
126				})
127			}
128		}
129	}
130	return ins
131}
132
133func render(fset *token.FileSet, x interface{}) string {
134	var buf bytes.Buffer
135	if err := printer.Fprint(&buf, fset, x); err != nil {
136		panic(err)
137	}
138	return buf.String()
139}
140
141func TestLine(t *testing.T) {
142	tests := []struct {
143		src    string
144		offset int
145		want   string
146	}{
147		{"single line file", 5, "single line file"},
148		{"single line file with newline\n", 5, "single line file with newline\n"},
149		{"first\nsecond\nthird\n", 2, "first\n"},
150		{"first\nsecond\nthird\n", 9, "second\n"},
151		{"first\nsecond\nthird\n", 14, "third\n"},
152		{"first\nsecond\nthird with no newline", 16, "third with no newline"},
153		{"first byte\n", 0, "first byte\n"},
154	}
155	for _, test := range tests {
156		got := srcLine([]byte(test.src), token.Position{Offset: test.offset})
157		if got != test.want {
158			t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want)
159		}
160	}
161}
162
163func TestLintName(t *testing.T) {
164	tests := []struct {
165		name, want string
166	}{
167		{"foo_bar", "fooBar"},
168		{"foo_bar_baz", "fooBarBaz"},
169		{"Foo_bar", "FooBar"},
170		{"foo_WiFi", "fooWiFi"},
171		{"id", "id"},
172		{"Id", "ID"},
173		{"foo_id", "fooID"},
174		{"fooId", "fooID"},
175		{"fooUid", "fooUID"},
176		{"idFoo", "idFoo"},
177		{"uidFoo", "uidFoo"},
178		{"midIdDle", "midIDDle"},
179		{"APIProxy", "APIProxy"},
180		{"ApiProxy", "APIProxy"},
181		{"apiProxy", "apiProxy"},
182		{"_Leading", "_Leading"},
183		{"___Leading", "_Leading"},
184		{"trailing_", "trailing"},
185		{"trailing___", "trailing"},
186		{"a_b", "aB"},
187		{"a__b", "aB"},
188		{"a___b", "aB"},
189		{"Rpc1150", "RPC1150"},
190	}
191	for _, test := range tests {
192		got := lintName(test.name)
193		if got != test.want {
194			t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want)
195		}
196	}
197}
198