1package unused
2
3import (
4	"fmt"
5	"go/parser"
6	"go/token"
7	"go/types"
8	"os"
9	"sort"
10	"strconv"
11	"strings"
12	"testing"
13	"text/scanner"
14
15	"golang.org/x/tools/go/analysis"
16	"golang.org/x/tools/go/analysis/analysistest"
17	"golang.org/x/tools/go/packages"
18	"honnef.co/go/tools/lint"
19)
20
21// parseExpectations parses the content of a "// want ..." comment
22// and returns the expectations, a mixture of diagnostics ("rx") and
23// facts (name:"rx").
24func parseExpectations(text string) ([]string, error) {
25	var scanErr string
26	sc := new(scanner.Scanner).Init(strings.NewReader(text))
27	sc.Error = func(s *scanner.Scanner, msg string) {
28		scanErr = msg // e.g. bad string escape
29	}
30	sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings
31
32	scanRegexp := func(tok rune) (string, error) {
33		if tok != scanner.String && tok != scanner.RawString {
34			return "", fmt.Errorf("got %s, want regular expression",
35				scanner.TokenString(tok))
36		}
37		pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail
38		return pattern, nil
39	}
40
41	var expects []string
42	for {
43		tok := sc.Scan()
44		switch tok {
45		case scanner.String, scanner.RawString:
46			rx, err := scanRegexp(tok)
47			if err != nil {
48				return nil, err
49			}
50			expects = append(expects, rx)
51
52		case scanner.EOF:
53			if scanErr != "" {
54				return nil, fmt.Errorf("%s", scanErr)
55			}
56			return expects, nil
57
58		default:
59			return nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
60		}
61	}
62}
63
64func check(t *testing.T, fset *token.FileSet, diagnostics []types.Object) {
65	type key struct {
66		file string
67		line int
68	}
69
70	files := map[string]struct{}{}
71	for _, d := range diagnostics {
72		files[fset.Position(d.Pos()).Filename] = struct{}{}
73	}
74
75	want := make(map[key][]string)
76
77	// processComment parses expectations out of comments.
78	processComment := func(filename string, linenum int, text string) {
79		text = strings.TrimSpace(text)
80
81		// Any comment starting with "want" is treated
82		// as an expectation, even without following whitespace.
83		if rest := strings.TrimPrefix(text, "want"); rest != text {
84			expects, err := parseExpectations(rest)
85			if err != nil {
86				t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err)
87				return
88			}
89			if expects != nil {
90				want[key{filename, linenum}] = expects
91			}
92		}
93	}
94
95	// Extract 'want' comments from Go files.
96	fset2 := token.NewFileSet()
97	for f := range files {
98		af, err := parser.ParseFile(fset2, f, nil, parser.ParseComments)
99		if err != nil {
100			t.Fatal(err)
101		}
102		for _, cgroup := range af.Comments {
103			for _, c := range cgroup.List {
104
105				text := strings.TrimPrefix(c.Text, "//")
106				if text == c.Text {
107					continue // not a //-comment
108				}
109
110				// Hack: treat a comment of the form "//...// want..."
111				// as if it starts at 'want'.
112				// This allows us to add comments on comments,
113				// as required when testing the buildtag analyzer.
114				if i := strings.Index(text, "// want"); i >= 0 {
115					text = text[i+len("// "):]
116				}
117
118				// It's tempting to compute the filename
119				// once outside the loop, but it's
120				// incorrect because it can change due
121				// to //line directives.
122				posn := fset2.Position(c.Pos())
123				processComment(posn.Filename, posn.Line, text)
124			}
125		}
126	}
127
128	checkMessage := func(posn token.Position, name, message string) {
129		k := key{posn.Filename, posn.Line}
130		expects := want[k]
131		var unmatched []string
132		for i, exp := range expects {
133			if exp == message {
134				// matched: remove the expectation.
135				expects[i] = expects[len(expects)-1]
136				expects = expects[:len(expects)-1]
137				want[k] = expects
138				return
139			}
140			unmatched = append(unmatched, fmt.Sprintf("%q", exp))
141		}
142		if unmatched == nil {
143			t.Errorf("%v: unexpected: %v", posn, message)
144		} else {
145			t.Errorf("%v: %q does not match pattern %s",
146				posn, message, strings.Join(unmatched, " or "))
147		}
148	}
149
150	// Check the diagnostics match expectations.
151	for _, f := range diagnostics {
152		posn := fset.Position(f.Pos())
153		checkMessage(posn, "", f.Name())
154	}
155
156	// Reject surplus expectations.
157	//
158	// Sometimes an Analyzer reports two similar diagnostics on a
159	// line with only one expectation. The reader may be confused by
160	// the error message.
161	// TODO(adonovan): print a better error:
162	// "got 2 diagnostics here; each one needs its own expectation".
163	var surplus []string
164	for key, expects := range want {
165		for _, exp := range expects {
166			err := fmt.Sprintf("%s:%d: no diagnostic was reported matching %q", key.file, key.line, exp)
167			surplus = append(surplus, err)
168		}
169	}
170	sort.Strings(surplus)
171	for _, err := range surplus {
172		t.Errorf("%s", err)
173	}
174}
175
176func TestAll(t *testing.T) {
177	c := NewChecker(false)
178	var stats lint.Stats
179	r, err := lint.NewRunner(&stats)
180	if err != nil {
181		t.Fatal(err)
182	}
183
184	dir := analysistest.TestData()
185	cfg := &packages.Config{
186		Dir:   dir,
187		Tests: true,
188		Env:   append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
189	}
190	pkgs, err := r.Run(cfg, []string{"./..."}, []*analysis.Analyzer{c.Analyzer()}, true)
191	if err != nil {
192		t.Fatal(err)
193	}
194
195	res := c.Result()
196	check(t, pkgs[0].Fset, res)
197}
198