|
| 1 | +package test |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | + "go/parser" |
| 7 | + "go/token" |
| 8 | + "os" |
| 9 | + "path/filepath" |
| 10 | + "regexp" |
| 11 | + "sort" |
| 12 | + "strconv" |
| 13 | + "strings" |
| 14 | + "testing" |
| 15 | + "text/scanner" |
| 16 | + |
| 17 | + "github.com/stretchr/testify/require" |
| 18 | + |
| 19 | + "github.com/golangci/golangci-lint/pkg/result" |
| 20 | +) |
| 21 | + |
| 22 | +const keyword = "want" |
| 23 | + |
| 24 | +type jsonResult struct { |
| 25 | + Issues []*result.Issue |
| 26 | +} |
| 27 | + |
| 28 | +type expectation struct { |
| 29 | + kind string // either "fact" or "diagnostic" |
| 30 | + name string // name of object to which fact belongs, or "package" ("fact" only) |
| 31 | + rx *regexp.Regexp |
| 32 | +} |
| 33 | + |
| 34 | +type key struct { |
| 35 | + file string |
| 36 | + line int |
| 37 | +} |
| 38 | + |
| 39 | +// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go |
| 40 | +func analyze(t *testing.T, sourcePath string, rawData []byte) { |
| 41 | + fileData, err := os.ReadFile(sourcePath) |
| 42 | + require.NoError(t, err) |
| 43 | + |
| 44 | + want, err := parseComments(sourcePath, fileData) |
| 45 | + require.NoError(t, err) |
| 46 | + |
| 47 | + var reportData jsonResult |
| 48 | + err = json.Unmarshal(rawData, &reportData) |
| 49 | + require.NoError(t, err) |
| 50 | + |
| 51 | + for _, issue := range reportData.Issues { |
| 52 | + if !strings.HasPrefix(issue.Pos.Filename, testdataDir) { |
| 53 | + issue.Pos.Filename = filepath.Join(testdataDir, issue.Pos.Filename) |
| 54 | + } |
| 55 | + checkMessage(t, want, issue.Pos, "diagnostic", issue.FromLinter, issue.Text) |
| 56 | + } |
| 57 | + |
| 58 | + var surplus []string |
| 59 | + for key, expects := range want { |
| 60 | + for _, exp := range expects { |
| 61 | + err := fmt.Sprintf("%s:%d: no %s was reported matching %#q", key.file, key.line, exp.kind, exp.rx) |
| 62 | + surplus = append(surplus, err) |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + sort.Strings(surplus) |
| 67 | + |
| 68 | + for _, err := range surplus { |
| 69 | + t.Errorf("%s", err) |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go |
| 74 | +func parseComments(sourcePath string, fileData []byte) (map[key][]expectation, error) { |
| 75 | + fset := token.NewFileSet() |
| 76 | + |
| 77 | + // the error is ignored to let 'typecheck' handle compilation error |
| 78 | + f, _ := parser.ParseFile(fset, sourcePath, fileData, parser.ParseComments) |
| 79 | + |
| 80 | + want := make(map[key][]expectation) |
| 81 | + |
| 82 | + for _, comment := range f.Comments { |
| 83 | + for _, c := range comment.List { |
| 84 | + text := strings.TrimPrefix(c.Text, "//") |
| 85 | + if text == c.Text { // not a //-comment. |
| 86 | + text = strings.TrimPrefix(text, "/*") |
| 87 | + text = strings.TrimSuffix(text, "*/") |
| 88 | + } |
| 89 | + |
| 90 | + if i := strings.Index(text, "// "+keyword); i >= 0 { |
| 91 | + text = text[i+len("// "):] |
| 92 | + } |
| 93 | + |
| 94 | + posn := fset.Position(c.Pos()) |
| 95 | + |
| 96 | + text = strings.TrimSpace(text) |
| 97 | + |
| 98 | + if rest := strings.TrimPrefix(text, keyword); rest != text { |
| 99 | + delta, expects, err := parseExpectations(rest) |
| 100 | + if err != nil { |
| 101 | + return nil, err |
| 102 | + } |
| 103 | + |
| 104 | + want[key{sourcePath, posn.Line + delta}] = expects |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + return want, nil |
| 110 | +} |
| 111 | + |
| 112 | +// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go |
| 113 | +func parseExpectations(text string) (lineDelta int, expects []expectation, err error) { |
| 114 | + var scanErr string |
| 115 | + sc := new(scanner.Scanner).Init(strings.NewReader(text)) |
| 116 | + sc.Error = func(s *scanner.Scanner, msg string) { |
| 117 | + scanErr = msg // e.g. bad string escape |
| 118 | + } |
| 119 | + sc.Mode = scanner.ScanIdents | scanner.ScanStrings | scanner.ScanRawStrings | scanner.ScanInts |
| 120 | + |
| 121 | + scanRegexp := func(tok rune) (*regexp.Regexp, error) { |
| 122 | + if tok != scanner.String && tok != scanner.RawString { |
| 123 | + return nil, fmt.Errorf("got %s, want regular expression", |
| 124 | + scanner.TokenString(tok)) |
| 125 | + } |
| 126 | + pattern, _ := strconv.Unquote(sc.TokenText()) // can't fail |
| 127 | + return regexp.Compile(pattern) |
| 128 | + } |
| 129 | + |
| 130 | + for { |
| 131 | + tok := sc.Scan() |
| 132 | + switch tok { |
| 133 | + case '+': |
| 134 | + tok = sc.Scan() |
| 135 | + if tok != scanner.Int { |
| 136 | + return 0, nil, fmt.Errorf("got +%s, want +Int", scanner.TokenString(tok)) |
| 137 | + } |
| 138 | + lineDelta, _ = strconv.Atoi(sc.TokenText()) |
| 139 | + case scanner.String, scanner.RawString: |
| 140 | + rx, err := scanRegexp(tok) |
| 141 | + if err != nil { |
| 142 | + return 0, nil, err |
| 143 | + } |
| 144 | + expects = append(expects, expectation{"diagnostic", "", rx}) |
| 145 | + |
| 146 | + case scanner.Ident: |
| 147 | + name := sc.TokenText() |
| 148 | + tok = sc.Scan() |
| 149 | + if tok != ':' { |
| 150 | + return 0, nil, fmt.Errorf("got %s after %s, want ':'", |
| 151 | + scanner.TokenString(tok), name) |
| 152 | + } |
| 153 | + tok = sc.Scan() |
| 154 | + rx, err := scanRegexp(tok) |
| 155 | + if err != nil { |
| 156 | + return 0, nil, err |
| 157 | + } |
| 158 | + expects = append(expects, expectation{"diagnostic", name, rx}) |
| 159 | + |
| 160 | + case scanner.EOF: |
| 161 | + if scanErr != "" { |
| 162 | + return 0, nil, fmt.Errorf("%s", scanErr) |
| 163 | + } |
| 164 | + return lineDelta, expects, nil |
| 165 | + |
| 166 | + default: |
| 167 | + return 0, nil, fmt.Errorf("unexpected %s", scanner.TokenString(tok)) |
| 168 | + } |
| 169 | + } |
| 170 | +} |
| 171 | + |
| 172 | +// inspired by https://github.com/golang/tools/blob/b3b5c13b291f9653da6f31b95db100a2e26bd186/go/analysis/analysistest/analysistest.go |
| 173 | +func checkMessage(t *testing.T, want map[key][]expectation, posn token.Position, kind, name, message string) { |
| 174 | + k := key{posn.Filename, posn.Line} |
| 175 | + expects := want[k] |
| 176 | + var unmatched []string |
| 177 | + |
| 178 | + for i, exp := range expects { |
| 179 | + if exp.kind == kind && (exp.name == "" || exp.name == name) { |
| 180 | + if exp.rx.MatchString(message) { |
| 181 | + // matched: remove the expectation. |
| 182 | + expects[i] = expects[len(expects)-1] |
| 183 | + expects = expects[:len(expects)-1] |
| 184 | + want[k] = expects |
| 185 | + return |
| 186 | + } |
| 187 | + unmatched = append(unmatched, fmt.Sprintf("%#q", exp.rx)) |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + if unmatched == nil { |
| 192 | + t.Errorf("%v: unexpected %s: %v", posn, kind, message) |
| 193 | + } else { |
| 194 | + t.Errorf("%v: %s %q does not match pattern %s", |
| 195 | + posn, kind, message, strings.Join(unmatched, " or ")) |
| 196 | + } |
| 197 | +} |
0 commit comments