github.com/azazeal/revive@v1.0.9/test/utils.go (about) 1 package test 2 3 import ( 4 "bufio" 5 "bytes" 6 "fmt" 7 "go/ast" 8 "go/parser" 9 "go/printer" 10 "go/token" 11 "go/types" 12 "io/ioutil" 13 "os" 14 "strconv" 15 "strings" 16 "testing" 17 18 "github.com/azazeal/revive/lint" 19 "github.com/pkg/errors" 20 ) 21 22 func testRule(t *testing.T, filename string, rule lint.Rule, config ...*lint.RuleConfig) { 23 baseDir := "../testdata/" 24 filename = filename + ".go" 25 src, err := ioutil.ReadFile(baseDir + filename) 26 if err != nil { 27 t.Fatalf("Bad filename path in test for %s: %v", rule.Name(), err) 28 } 29 stat, err := os.Stat(baseDir + filename) 30 if err != nil { 31 t.Fatalf("Cannot get file info for %s: %v", rule.Name(), err) 32 } 33 c := map[string]lint.RuleConfig{} 34 if config != nil { 35 c[rule.Name()] = *config[0] 36 } 37 if parseInstructions(t, filename, src) == nil { 38 assertSuccess(t, baseDir, stat, []lint.Rule{rule}, c) 39 return 40 } 41 assertFailures(t, baseDir, stat, src, []lint.Rule{rule}, c) 42 } 43 44 func assertSuccess(t *testing.T, baseDir string, fi os.FileInfo, rules []lint.Rule, config map[string]lint.RuleConfig) error { 45 l := lint.New(func(file string) ([]byte, error) { 46 return ioutil.ReadFile(baseDir + file) 47 }) 48 49 ps, err := l.Lint([][]string{[]string{fi.Name()}}, rules, lint.Config{ 50 Rules: config, 51 }) 52 if err != nil { 53 return err 54 } 55 56 failures := "" 57 for p := range ps { 58 failures += p.Failure 59 } 60 if failures != "" { 61 t.Errorf("Expected the rule to pass but got the following failures: %s", failures) 62 } 63 return nil 64 } 65 66 func assertFailures(t *testing.T, baseDir string, fi os.FileInfo, src []byte, rules []lint.Rule, config map[string]lint.RuleConfig) error { 67 l := lint.New(func(file string) ([]byte, error) { 68 return ioutil.ReadFile(baseDir + file) 69 }) 70 71 ins := parseInstructions(t, fi.Name(), src) 72 if ins == nil { 73 return errors.Errorf("Test file %v does not have instructions", fi.Name()) 74 } 75 76 ps, err := l.Lint([][]string{[]string{fi.Name()}}, rules, lint.Config{ 77 Rules: config, 78 }) 79 if err != nil { 80 return err 81 } 82 83 failures := []lint.Failure{} 84 for f := range ps { 85 failures = append(failures, f) 86 } 87 88 for _, in := range ins { 89 ok := false 90 for i, p := range failures { 91 if p.Position.Start.Line != in.Line { 92 continue 93 } 94 if in.Match == p.Failure { 95 // check replacement if we are expecting one 96 if in.Replacement != "" { 97 // ignore any inline comments, since that would be recursive 98 r := p.ReplacementLine 99 if i := strings.Index(r, " //"); i >= 0 { 100 r = r[:i] 101 } 102 if r != in.Replacement { 103 t.Errorf("Lint failed at %s:%d; got replacement %q, want %q", fi.Name(), in.Line, r, in.Replacement) 104 } 105 } 106 107 // remove this problem from ps 108 copy(failures[i:], failures[i+1:]) 109 failures = failures[:len(failures)-1] 110 111 // t.Logf("/%v/ matched at %s:%d", in.Match, fi.Name(), in.Line) 112 ok = true 113 break 114 } 115 } 116 if !ok { 117 t.Errorf("Lint failed at %s:%d; /%v/ did not match", fi.Name(), in.Line, in.Match) 118 } 119 } 120 for _, p := range failures { 121 t.Errorf("Unexpected problem at %s:%d: %v", fi.Name(), p.Position.Start.Line, p.Failure) 122 } 123 return nil 124 } 125 126 type instruction struct { 127 Line int // the line number this applies to 128 Match string // what pattern to match 129 Replacement string // what the suggested replacement line should be 130 } 131 132 // parseInstructions parses instructions from the comments in a Go source file. 133 // It returns nil if none were parsed. 134 func parseInstructions(t *testing.T, filename string, src []byte) []instruction { 135 fset := token.NewFileSet() 136 f, err := parser.ParseFile(fset, filename, src, parser.ParseComments) 137 if err != nil { 138 t.Fatalf("Test file %v does not parse: %v", filename, err) 139 } 140 var ins []instruction 141 for _, cg := range f.Comments { 142 ln := fset.Position(cg.Pos()).Line 143 raw := cg.Text() 144 for _, line := range strings.Split(raw, "\n") { 145 if line == "" || strings.HasPrefix(line, "#") { 146 continue 147 } 148 if line == "OK" && ins == nil { 149 // so our return value will be non-nil 150 ins = make([]instruction, 0) 151 continue 152 } 153 if strings.Contains(line, "MATCH") { 154 match, err := extractPattern(line) 155 if err != nil { 156 t.Fatalf("At %v:%d: %v", filename, ln, err) 157 } 158 matchLine := ln 159 if i := strings.Index(line, "MATCH:"); i >= 0 { 160 // This is a match for a different line. 161 lns := strings.TrimPrefix(line[i:], "MATCH:") 162 lns = lns[:strings.Index(lns, " ")] 163 matchLine, err = strconv.Atoi(lns) 164 if err != nil { 165 t.Fatalf("Bad match line number %q at %v:%d: %v", lns, filename, ln, err) 166 } 167 } 168 var repl string 169 if r, ok := extractReplacement(line); ok { 170 repl = r 171 } 172 ins = append(ins, instruction{ 173 Line: matchLine, 174 Match: match, 175 Replacement: repl, 176 }) 177 } 178 } 179 } 180 return ins 181 } 182 183 func extractPattern(line string) (string, error) { 184 a, b := strings.Index(line, "/"), strings.LastIndex(line, "/") 185 if a == -1 || a == b { 186 return "", fmt.Errorf("malformed match instruction %q", line) 187 } 188 return line[a+1 : b], nil 189 } 190 191 func extractReplacement(line string) (string, bool) { 192 // Look for this: / -> ` 193 // (the end of a match and start of a backtick string), 194 // and then the closing backtick. 195 const start = "/ -> `" 196 a, b := strings.Index(line, start), strings.LastIndex(line, "`") 197 if a < 0 || a > b { 198 return "", false 199 } 200 return line[a+len(start) : b], true 201 } 202 203 func render(fset *token.FileSet, x interface{}) string { 204 var buf bytes.Buffer 205 if err := printer.Fprint(&buf, fset, x); err != nil { 206 panic(err) 207 } 208 return buf.String() 209 } 210 211 func srcLine(src []byte, p token.Position) string { 212 // Run to end of line in both directions if not at line start/end. 213 lo, hi := p.Offset, p.Offset+1 214 for lo > 0 && src[lo-1] != '\n' { 215 lo-- 216 } 217 for hi < len(src) && src[hi-1] != '\n' { 218 hi++ 219 } 220 return string(src[lo:hi]) 221 } 222 223 // TestLine tests srcLine function 224 func TestLine(t *testing.T) { //revive:disable-line:exported 225 tests := []struct { 226 src string 227 offset int 228 want string 229 }{ 230 {"single line file", 5, "single line file"}, 231 {"single line file with newline\n", 5, "single line file with newline\n"}, 232 {"first\nsecond\nthird\n", 2, "first\n"}, 233 {"first\nsecond\nthird\n", 9, "second\n"}, 234 {"first\nsecond\nthird\n", 14, "third\n"}, 235 {"first\nsecond\nthird with no newline", 16, "third with no newline"}, 236 {"first byte\n", 0, "first byte\n"}, 237 } 238 for _, test := range tests { 239 got := srcLine([]byte(test.src), token.Position{Offset: test.offset}) 240 if got != test.want { 241 t.Errorf("srcLine(%q, offset=%d) = %q, want %q", test.src, test.offset, got, test.want) 242 } 243 } 244 } 245 246 // TestLintName tests lint.Name function 247 func TestLintName(t *testing.T) { //revive:disable-line:exported 248 tests := []struct { 249 name, want string 250 }{ 251 {"foo_bar", "fooBar"}, 252 {"foo_bar_baz", "fooBarBaz"}, 253 {"Foo_bar", "FooBar"}, 254 {"foo_WiFi", "fooWiFi"}, 255 {"id", "id"}, 256 {"Id", "ID"}, 257 {"foo_id", "fooID"}, 258 {"fooId", "fooID"}, 259 {"fooUid", "fooUID"}, 260 {"idFoo", "idFoo"}, 261 {"uidFoo", "uidFoo"}, 262 {"midIdDle", "midIDDle"}, 263 {"APIProxy", "APIProxy"}, 264 {"ApiProxy", "APIProxy"}, 265 {"apiProxy", "apiProxy"}, 266 {"_Leading", "_Leading"}, 267 {"___Leading", "_Leading"}, 268 {"trailing_", "trailing"}, 269 {"trailing___", "trailing"}, 270 {"a_b", "aB"}, 271 {"a__b", "aB"}, 272 {"a___b", "aB"}, 273 {"Rpc1150", "RPC1150"}, 274 {"case3_1", "case3_1"}, 275 {"case3__1", "case3_1"}, 276 {"IEEE802_16bit", "IEEE802_16bit"}, 277 {"IEEE802_16Bit", "IEEE802_16Bit"}, 278 } 279 for _, test := range tests { 280 got := lint.Name(test.name, nil, nil) 281 if got != test.want { 282 t.Errorf("lintName(%q) = %q, want %q", test.name, got, test.want) 283 } 284 } 285 } 286 287 // exportedType reports whether typ is an exported type. 288 // It is imprecise, and will err on the side of returning true, 289 // such as for composite types. 290 func exportedType(typ types.Type) bool { 291 switch T := typ.(type) { 292 case *types.Named: 293 // Builtin types have no package. 294 return T.Obj().Pkg() == nil || T.Obj().Exported() 295 case *types.Map: 296 return exportedType(T.Key()) && exportedType(T.Elem()) 297 case interface { 298 Elem() types.Type 299 }: // array, slice, pointer, chan 300 return exportedType(T.Elem()) 301 } 302 // Be conservative about other types, such as struct, interface, etc. 303 return true 304 } 305 306 // TestExportedType tests exportedType function 307 func TestExportedType(t *testing.T) { //revive:disable-line:exported 308 tests := []struct { 309 typString string 310 exp bool 311 }{ 312 {"int", true}, 313 {"string", false}, // references the shadowed builtin "string" 314 {"T", true}, 315 {"t", false}, 316 {"*T", true}, 317 {"*t", false}, 318 {"map[int]complex128", true}, 319 } 320 for _, test := range tests { 321 src := `package foo; type T int; type t int; type string struct{}` 322 fset := token.NewFileSet() 323 file, err := parser.ParseFile(fset, "foo.go", src, 0) 324 if err != nil { 325 t.Fatalf("Parsing %q: %v", src, err) 326 } 327 // use the package name as package path 328 config := &types.Config{} 329 pkg, err := config.Check(file.Name.Name, fset, []*ast.File{file}, nil) 330 if err != nil { 331 t.Fatalf("Type checking %q: %v", src, err) 332 } 333 tv, err := types.Eval(fset, pkg, token.NoPos, test.typString) 334 if err != nil { 335 t.Errorf("types.Eval(%q): %v", test.typString, err) 336 continue 337 } 338 if got := exportedType(tv.Type); got != test.exp { 339 t.Errorf("exportedType(%v) = %t, want %t", tv.Type, got, test.exp) 340 } 341 } 342 } 343 344 var ( 345 genHdr = []byte("// Code generated ") 346 genFtr = []byte(" DO NOT EDIT.") 347 ) 348 349 // isGenerated reports whether the source file is generated code 350 // according the rules from https://golang.org/s/generatedcode. 351 func isGenerated(src []byte) bool { 352 sc := bufio.NewScanner(bytes.NewReader(src)) 353 for sc.Scan() { 354 b := sc.Bytes() 355 if bytes.HasPrefix(b, genHdr) && bytes.HasSuffix(b, genFtr) && len(b) >= len(genHdr)+len(genFtr) { 356 return true 357 } 358 } 359 return false 360 } 361 362 // TestIsGenerated tests isGenerated function 363 func TestIsGenerated(t *testing.T) { //revive:disable-line:exported 364 tests := []struct { 365 source string 366 generated bool 367 }{ 368 {"// Code Generated by some tool. DO NOT EDIT.", false}, 369 {"// Code generated by some tool. DO NOT EDIT.", true}, 370 {"// Code generated by some tool. DO NOT EDIT", false}, 371 {"// Code generated DO NOT EDIT.", true}, 372 {"// Code generated DO NOT EDIT.", false}, 373 {"\t\t// Code generated by some tool. DO NOT EDIT.\npackage foo\n", false}, 374 {"// Code generated by some tool. DO NOT EDIT.\npackage foo\n", true}, 375 {"package foo\n// Code generated by some tool. DO NOT EDIT.\ntype foo int\n", true}, 376 {"package foo\n // Code generated by some tool. DO NOT EDIT.\ntype foo int\n", false}, 377 {"package foo\n// Code generated by some tool. DO NOT EDIT. \ntype foo int\n", false}, 378 {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.\n", true}, 379 {"package foo\ntype foo int\n// Code generated by some tool. DO NOT EDIT.", true}, 380 } 381 382 for i, test := range tests { 383 got := isGenerated([]byte(test.source)) 384 if got != test.generated { 385 t.Errorf("test %d, isGenerated() = %v, want %v", i, got, test.generated) 386 } 387 } 388 }