1package check 2 3import ( 4 "errors" 5 "flag" 6 "fmt" 7 "go/ast" 8 "go/build" 9 "go/token" 10 "go/types" 11 "log" 12 "os" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "sort" 17 "strings" 18 "sync" 19 20 "github.com/go-critic/go-critic/framework/linter" 21 "github.com/go-critic/go-critic/framework/lintmain/internal/hotload" 22 "github.com/go-toolsmith/pkgload" 23 "github.com/logrusorgru/aurora" 24 "golang.org/x/tools/go/packages" 25) 26 27// Main implements sub-command entry point. 28func Main() { 29 var p program 30 p.infoList = linter.GetCheckersInfo() 31 32 steps := []struct { 33 name string 34 fn func() error 35 }{ 36 {"load plugin", p.loadPlugin}, 37 {"bind checker params", p.bindCheckerParams}, 38 {"bind default enabled list", p.bindDefaultEnabledList}, 39 {"parse args", p.parseArgs}, 40 {"assign checker params", p.assignCheckerParams}, 41 {"load program", p.loadProgram}, 42 {"init checkers", p.initCheckers}, 43 {"run checkers", p.runCheckers}, 44 {"exit if found issues", p.exit}, 45 } 46 47 for _, step := range steps { 48 if err := step.fn(); err != nil { 49 log.Fatalf("%s: %v", step.name, err) 50 } 51 } 52} 53 54type program struct { 55 ctx *linter.Context 56 57 fset *token.FileSet 58 59 loadedPackages []*packages.Package 60 61 infoList []*linter.CheckerInfo 62 63 checkers []*linter.Checker 64 65 packages []string 66 67 foundIssues bool 68 69 checkerParams boundCheckerParams 70 71 filters struct { 72 enableAll bool 73 enable []string 74 disable []string 75 defaultCheckers []string 76 } 77 78 workDir string 79 gopath string 80 goroot string 81 82 exitCode int 83 checkTests bool 84 checkGenerated bool 85 shorterErrLocation bool 86 coloredOutput bool 87 verbose bool 88} 89 90func (p *program) exit() error { 91 if p.foundIssues { 92 os.Exit(p.exitCode) 93 } 94 return nil 95} 96 97func (p *program) runCheckers() error { 98 for _, pkg := range p.loadedPackages { 99 if p.verbose { 100 log.Printf("\tdebug: checking %q package (%d files)", 101 pkg.String(), len(pkg.Syntax)) 102 } 103 p.checkPackage(pkg) 104 } 105 106 return nil 107} 108 109func (p *program) checkPackage(pkg *packages.Package) { 110 p.ctx.SetPackageInfo(pkg.TypesInfo, pkg.Types) 111 for _, f := range pkg.Syntax { 112 filename := p.getFilename(f) 113 if !p.checkTests && strings.HasSuffix(filename, "_test.go") { 114 continue 115 } 116 if !p.checkGenerated && p.isGenerated(f) { 117 continue 118 } 119 p.ctx.SetFileInfo(filename, f) 120 p.checkFile(f) 121 } 122} 123 124func (p *program) checkFile(f *ast.File) { 125 warnings := make([][]linter.Warning, len(p.checkers)) 126 127 var wg sync.WaitGroup 128 wg.Add(len(p.checkers)) 129 for i, c := range p.checkers { 130 // All checkers are expected to use *lint.Context 131 // as read-only structure, so no copying is required. 132 go func(i int, c *linter.Checker) { 133 defer func() { 134 wg.Done() 135 // Checker signals unexpected error with panic(error). 136 r := recover() 137 if r == nil { 138 return // There were no panic 139 } 140 if err, ok := r.(error); ok { 141 log.Printf("%s: error: %v\n", c.Info.Name, err) 142 panic(err) 143 } else { 144 // Some other kind of run-time panic. 145 // Undo the recover and resume panic. 146 panic(r) 147 } 148 }() 149 150 warnings[i] = append(warnings[i], c.Check(f)...) 151 }(i, c) 152 } 153 wg.Wait() 154 155 for i, c := range p.checkers { 156 for _, warn := range warnings[i] { 157 p.foundIssues = true 158 loc := p.ctx.FileSet.Position(warn.Node.Pos()).String() 159 if p.shorterErrLocation { 160 loc = p.shortenLocation(loc) 161 } 162 printWarning(p, c.Info.Name, loc, warn.Text) 163 } 164 } 165 166} 167 168func (p *program) initCheckers() error { 169 parseKeys := func(keys []string, byName, byTag map[string]bool) { 170 for _, key := range keys { 171 if strings.HasPrefix(key, "#") { 172 byTag[key[len("#"):]] = true 173 } else { 174 byName[key] = true 175 } 176 } 177 } 178 179 enabledByName := make(map[string]bool) 180 enabledTags := make(map[string]bool) 181 parseKeys(p.filters.enable, enabledByName, enabledTags) 182 disabledByName := make(map[string]bool) 183 disabledTags := make(map[string]bool) 184 parseKeys(p.filters.disable, disabledByName, disabledTags) 185 186 enabledByTag := func(info *linter.CheckerInfo) bool { 187 for _, tag := range info.Tags { 188 if enabledTags[tag] { 189 return true 190 } 191 } 192 return false 193 } 194 disabledByTag := func(info *linter.CheckerInfo) string { 195 for _, tag := range info.Tags { 196 if disabledTags[tag] { 197 return tag 198 } 199 } 200 return "" 201 } 202 203 for _, info := range p.infoList { 204 enabled := p.filters.enableAll || 205 enabledByName[info.Name] || 206 enabledByTag(info) 207 notice := "" 208 209 switch { 210 case !enabled: 211 notice = "not enabled by name or tag (-enable)" 212 case disabledByName[info.Name]: 213 enabled = false 214 notice = "disabled by name (-disable)" 215 default: 216 if tag := disabledByTag(info); tag != "" { 217 enabled = false 218 notice = fmt.Sprintf("disabled by %q tag (-disable)", tag) 219 } 220 } 221 222 if p.verbose && !enabled { 223 log.Printf("\tdebug: %s: %s", info.Name, notice) 224 } 225 if enabled { 226 p.checkers = append(p.checkers, linter.NewChecker(p.ctx, info)) 227 } 228 } 229 if p.verbose { 230 for _, c := range p.checkers { 231 log.Printf("\tdebug: %s is enabled", c.Info.Name) 232 } 233 } 234 235 if len(p.checkers) == 0 { 236 return errors.New("empty checkers set selected") 237 } 238 return nil 239} 240 241func (p *program) loadProgram() error { 242 sizes := types.SizesFor("gc", runtime.GOARCH) 243 if sizes == nil { 244 return fmt.Errorf("can't find sizes info for %s", runtime.GOARCH) 245 } 246 247 p.fset = token.NewFileSet() 248 mode := packages.NeedName | 249 packages.NeedFiles | 250 packages.NeedCompiledGoFiles | 251 packages.NeedImports | 252 packages.NeedTypes | 253 packages.NeedSyntax | 254 packages.NeedTypesInfo | 255 packages.NeedTypesSizes 256 cfg := packages.Config{ 257 Mode: mode, 258 Tests: true, 259 Fset: p.fset, 260 } 261 pkgs, err := loadPackages(&cfg, p.packages) 262 if err != nil { 263 log.Fatalf("load packages: %v", err) 264 } 265 sort.SliceStable(pkgs, func(i, j int) bool { 266 return pkgs[i].PkgPath < pkgs[j].PkgPath 267 }) 268 269 p.loadedPackages = pkgs 270 p.ctx = linter.NewContext(p.fset, sizes) 271 272 return nil 273} 274 275func (p *program) loadPlugin() error { 276 const pluginFilename = "gocritic-plugin.so" 277 if _, err := os.Stat(pluginFilename); os.IsNotExist(err) { 278 return nil 279 } 280 infoList, err := hotload.CheckersFromDylib(p.infoList, pluginFilename) 281 p.infoList = infoList 282 return err 283} 284 285type boundCheckerParams struct { 286 ints map[string]*int 287 bools map[string]*bool 288 strings map[string]*string 289} 290 291// bindCheckerParams registers command-line flags for every checker parameter. 292func (p *program) bindCheckerParams() error { 293 intParams := make(map[string]*int) 294 boolParams := make(map[string]*bool) 295 stringParams := make(map[string]*string) 296 297 for _, info := range p.infoList { 298 for pname, param := range info.Params { 299 key := p.checkerParamKey(info, pname) 300 switch v := param.Value.(type) { 301 case int: 302 intParams[key] = flag.Int(key, v, param.Usage) 303 case bool: 304 boolParams[key] = flag.Bool(key, v, param.Usage) 305 case string: 306 stringParams[key] = flag.String(key, v, param.Usage) 307 default: 308 panic("unreachable") // Checked in AddChecker 309 } 310 } 311 } 312 313 p.checkerParams.ints = intParams 314 p.checkerParams.bools = boolParams 315 p.checkerParams.strings = stringParams 316 317 return nil 318} 319 320func (p *program) checkerParamKey(info *linter.CheckerInfo, pname string) string { 321 return "@" + info.Name + "." + pname 322} 323 324// bindDefaultEnabledList calculates the default value for -enable param. 325func (p *program) bindDefaultEnabledList() error { 326 var enabled []string 327 for _, info := range p.infoList { 328 enable := !info.HasTag("experimental") && 329 !info.HasTag("opinionated") && 330 !info.HasTag("performance") && 331 !info.HasTag("security") 332 if enable { 333 enabled = append(enabled, info.Name) 334 } 335 } 336 p.filters.defaultCheckers = enabled 337 return nil 338} 339 340func (p *program) parseArgs() error { 341 flag.BoolVar(&p.filters.enableAll, "enableAll", false, 342 `identical to -enable with all checkers listed. If true, -enable is ignored`) 343 enable := flag.String("enable", strings.Join(p.filters.defaultCheckers, ","), 344 `comma-separated list of enabled checkers. Can include #tags`) 345 disable := flag.String("disable", "", 346 `comma-separated list of checkers to be disabled. Can include #tags`) 347 flag.IntVar(&p.exitCode, "exitCode", 1, 348 `exit code to be used when lint issues are found`) 349 flag.BoolVar(&p.checkTests, "checkTests", true, 350 `whether to check test files`) 351 flag.BoolVar(&p.shorterErrLocation, `shorterErrLocation`, true, 352 `whether to replace error location prefix with $GOROOT and $GOPATH`) 353 flag.BoolVar(&p.coloredOutput, `coloredOutput`, false, 354 `whether to use colored output`) 355 flag.BoolVar(&p.verbose, "v", false, 356 `whether to print output useful during linter debugging`) 357 358 flag.Parse() 359 360 p.packages = flag.Args() 361 p.filters.enable = strings.Split(*enable, ",") 362 p.filters.disable = strings.Split(*disable, ",") 363 364 if p.shorterErrLocation { 365 wd, err := os.Getwd() 366 if err != nil { 367 log.Printf("getwd: %v", err) 368 } 369 p.workDir = addTrailingSlash(wd) 370 p.gopath = addTrailingSlash(build.Default.GOPATH) 371 p.goroot = addTrailingSlash(build.Default.GOROOT) 372 } 373 374 return nil 375} 376 377func addTrailingSlash(s string) string { 378 if strings.HasSuffix(s, string(os.PathSeparator)) { 379 return s 380 } 381 return s + string(os.PathSeparator) 382} 383 384// assignCheckerParams initializes checker parameter values using 385// values that are coming from the command-line arguments. 386func (p *program) assignCheckerParams() error { 387 intParams := p.checkerParams.ints 388 boolParams := p.checkerParams.bools 389 stringParams := p.checkerParams.strings 390 391 for _, info := range p.infoList { 392 for pname, param := range info.Params { 393 key := p.checkerParamKey(info, pname) 394 switch param.Value.(type) { 395 case int: 396 info.Params[pname].Value = *intParams[key] 397 case bool: 398 info.Params[pname].Value = *boolParams[key] 399 case string: 400 info.Params[pname].Value = *stringParams[key] 401 default: 402 panic("unreachable") // Checked in AddChecker 403 } 404 } 405 } 406 407 return nil 408} 409 410var generatedFileCommentRE = regexp.MustCompile("Code generated .* DO NOT EDIT.") 411 412func (p *program) isGenerated(f *ast.File) bool { 413 return len(f.Comments) != 0 && 414 generatedFileCommentRE.MatchString(f.Comments[0].Text()) 415} 416 417func (p *program) getFilename(f *ast.File) string { 418 // See https://github.com/golang/go/issues/24498. 419 return filepath.Base(p.fset.Position(f.Pos()).Filename) 420} 421 422func (p *program) shortenLocation(loc string) string { 423 // If possible, construct relative path. 424 relLoc := loc 425 if p.workDir != "" { 426 relLoc = strings.Replace(loc, p.workDir, "./", 1) 427 } 428 429 switch { 430 case strings.HasPrefix(loc, p.gopath): 431 loc = strings.Replace(loc, p.gopath, "$GOPATH"+string(os.PathSeparator), 1) 432 case strings.HasPrefix(loc, p.goroot): 433 loc = strings.Replace(loc, p.goroot, "$GOROOT"+string(os.PathSeparator), 1) 434 } 435 436 // Return the representation that is shorter. 437 if len(relLoc) < len(loc) { 438 return relLoc 439 } 440 return loc 441} 442 443func printWarning(p *program, rule, loc, warn string) { 444 switch { 445 case p.coloredOutput: 446 log.Printf("%v: %v: %v\n", 447 aurora.Magenta(aurora.Bold(loc)), 448 aurora.Red(rule), 449 warn) 450 451 default: 452 log.Printf("%s: %s: %s\n", loc, rule, warn) 453 } 454} 455 456func loadPackages(cfg *packages.Config, patterns []string) ([]*packages.Package, error) { 457 pkgs, err := packages.Load(cfg, patterns...) 458 if err != nil { 459 return nil, err 460 } 461 462 result := pkgs[:0] 463 pkgload.VisitUnits(pkgs, func(u *pkgload.Unit) { 464 if u.ExternalTest != nil { 465 result = append(result, u.ExternalTest) 466 } 467 468 if u.Test != nil { 469 // Prefer tests to the base package, if present. 470 result = append(result, u.Test) 471 } else { 472 result = append(result, u.Base) 473 } 474 }) 475 return result, nil 476} 477