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