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"
21 // parseExpectations parses the content of a "// want ..." comment
22 // and returns the expectations, a mixture of diagnostics ("rx") and
24 func parseExpectations(text string) ([]string, error) {
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
30 sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings
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))
37 pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail
45 case scanner.String, scanner.RawString:
46 rx, err := scanRegexp(tok)
50 expects = append(expects, rx)
54 return nil, fmt.Errorf("%s", scanErr)
59 return nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok))
64 func check(t *testing.T, fset *token.FileSet, diagnostics []types.Object) {
70 files := map[string]struct{}{}
71 for _, d := range diagnostics {
72 files[fset.Position(d.Pos()).Filename] = struct{}{}
75 want := make(map[key][]string)
77 // processComment parses expectations out of comments.
78 processComment := func(filename string, linenum int, text string) {
79 text = strings.TrimSpace(text)
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)
86 t.Errorf("%s:%d: in 'want' comment: %s", filename, linenum, err)
90 want[key{filename, linenum}] = expects
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)
102 for _, cgroup := range af.Comments {
103 for _, c := range cgroup.List {
105 text := strings.TrimPrefix(c.Text, "//")
107 continue // not a //-comment
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("// "):]
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)
128 checkMessage := func(posn token.Position, name, message string) {
129 k := key{posn.Filename, posn.Line}
131 var unmatched []string
132 for i, exp := range expects {
134 // matched: remove the expectation.
135 expects[i] = expects[len(expects)-1]
136 expects = expects[:len(expects)-1]
140 unmatched = append(unmatched, fmt.Sprintf("%q", exp))
142 if unmatched == nil {
143 t.Errorf("%v: unexpected: %v", posn, message)
145 t.Errorf("%v: %q does not match pattern %s",
146 posn, message, strings.Join(unmatched, " or "))
150 // Check the diagnostics match expectations.
151 for _, f := range diagnostics {
152 posn := fset.Position(f.Pos())
153 checkMessage(posn, "", f.Name())
156 // Reject surplus expectations.
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".
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)
170 sort.Strings(surplus)
171 for _, err := range surplus {
176 func TestAll(t *testing.T) {
177 c := NewChecker(false)
179 r, err := lint.NewRunner(&stats)
184 dir := analysistest.TestData()
185 cfg := &packages.Config{
188 Env: append(os.Environ(), "GOPATH="+dir, "GO111MODULE=off", "GOPROXY=off"),
190 pkgs, err := r.Run(cfg, []string{"./..."}, []*analysis.Analyzer{c.Analyzer()}, true)
196 check(t, pkgs[0].Fset, res)