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