github.com/chenfeining/golangci-lint@v1.0.2-0.20230730162517-14c6c67868df/pkg/golinters/gocritic.go (about) 1 package golinters 2 3 import ( 4 "errors" 5 "fmt" 6 "go/ast" 7 "go/types" 8 "path/filepath" 9 "reflect" 10 "runtime" 11 "sort" 12 "strings" 13 "sync" 14 15 "github.com/go-critic/go-critic/checkers" 16 gocriticlinter "github.com/go-critic/go-critic/linter" 17 "golang.org/x/tools/go/analysis" 18 19 "github.com/chenfeining/golangci-lint/pkg/config" 20 "github.com/chenfeining/golangci-lint/pkg/golinters/goanalysis" 21 "github.com/chenfeining/golangci-lint/pkg/lint/linter" 22 "github.com/chenfeining/golangci-lint/pkg/logutils" 23 "github.com/chenfeining/golangci-lint/pkg/result" 24 ) 25 26 const goCriticName = "gocritic" 27 28 var ( 29 goCriticDebugf = logutils.Debug(logutils.DebugKeyGoCritic) 30 isGoCriticDebug = logutils.HaveDebugTag(logutils.DebugKeyGoCritic) 31 ) 32 33 func NewGoCritic(settings *config.GoCriticSettings, cfg *config.Config) *goanalysis.Linter { 34 var mu sync.Mutex 35 var resIssues []goanalysis.Issue 36 37 wrapper := &goCriticWrapper{ 38 cfg: cfg, 39 sizes: types.SizesFor("gc", runtime.GOARCH), 40 } 41 42 analyzer := &analysis.Analyzer{ 43 Name: goCriticName, 44 Doc: goanalysis.TheOnlyanalyzerDoc, 45 Run: func(pass *analysis.Pass) (any, error) { 46 issues, err := wrapper.run(pass) 47 if err != nil { 48 return nil, err 49 } 50 51 if len(issues) == 0 { 52 return nil, nil 53 } 54 55 mu.Lock() 56 resIssues = append(resIssues, issues...) 57 mu.Unlock() 58 59 return nil, nil 60 }, 61 } 62 63 return goanalysis.NewLinter( 64 goCriticName, 65 `Provides diagnostics that check for bugs, performance and style issues. 66 Extensible without recompilation through dynamic rules. 67 Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion.`, 68 []*analysis.Analyzer{analyzer}, 69 nil, 70 ). 71 WithContextSetter(func(context *linter.Context) { 72 wrapper.init(settings, context.Log) 73 }). 74 WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { 75 return resIssues 76 }).WithLoadMode(goanalysis.LoadModeTypesInfo) 77 } 78 79 type goCriticWrapper struct { 80 settingsWrapper *goCriticSettingsWrapper 81 cfg *config.Config 82 sizes types.Sizes 83 once sync.Once 84 } 85 86 func (w *goCriticWrapper) init(settings *config.GoCriticSettings, logger logutils.Log) { 87 if settings == nil { 88 return 89 } 90 91 w.once.Do(func() { 92 err := checkers.InitEmbeddedRules() 93 if err != nil { 94 logger.Fatalf("%s: %v: setting an explicit GOROOT can fix this problem.", goCriticName, err) 95 } 96 }) 97 98 settingsWrapper := newGoCriticSettingsWrapper(settings, logger) 99 100 settingsWrapper.inferEnabledChecks() 101 102 if err := settingsWrapper.validate(); err != nil { 103 logger.Fatalf("%s: invalid settings: %s", goCriticName, err) 104 } 105 106 w.settingsWrapper = settingsWrapper 107 } 108 109 func (w *goCriticWrapper) run(pass *analysis.Pass) ([]goanalysis.Issue, error) { 110 if w.settingsWrapper == nil { 111 return nil, fmt.Errorf("the settings wrapper is nil") 112 } 113 114 linterCtx := gocriticlinter.NewContext(pass.Fset, w.sizes) 115 116 linterCtx.SetGoVersion(w.settingsWrapper.Go) 117 118 enabledCheckers, err := w.buildEnabledCheckers(linterCtx) 119 if err != nil { 120 return nil, err 121 } 122 123 linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg) 124 125 pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files) 126 127 issues := make([]goanalysis.Issue, 0, len(pkgIssues)) 128 for i := range pkgIssues { 129 issues = append(issues, goanalysis.NewIssue(&pkgIssues[i], pass)) 130 } 131 132 return issues, nil 133 } 134 135 func (w *goCriticWrapper) buildEnabledCheckers(linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) { 136 allParams := w.settingsWrapper.getLowerCasedParams() 137 138 var enabledCheckers []*gocriticlinter.Checker 139 for _, info := range gocriticlinter.GetCheckersInfo() { 140 if !w.settingsWrapper.isCheckEnabled(info.Name) { 141 continue 142 } 143 144 if err := w.configureCheckerInfo(info, allParams); err != nil { 145 return nil, err 146 } 147 148 c, err := gocriticlinter.NewChecker(linterCtx, info) 149 if err != nil { 150 return nil, err 151 } 152 enabledCheckers = append(enabledCheckers, c) 153 } 154 155 return enabledCheckers, nil 156 } 157 158 func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checks []*gocriticlinter.Checker, 159 files []*ast.File) []result.Issue { 160 var res []result.Issue 161 for _, f := range files { 162 filename := filepath.Base(linterCtx.FileSet.Position(f.Pos()).Filename) 163 linterCtx.SetFileInfo(filename, f) 164 165 issues := runGocriticOnFile(linterCtx, f, checks) 166 res = append(res, issues...) 167 } 168 return res 169 } 170 171 func runGocriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []*gocriticlinter.Checker) []result.Issue { 172 var res []result.Issue 173 174 for _, c := range checks { 175 // All checkers are expected to use *lint.Context 176 // as read-only structure, so no copying is required. 177 for _, warn := range c.Check(f) { 178 pos := linterCtx.FileSet.Position(warn.Pos) 179 issue := result.Issue{ 180 Pos: pos, 181 Text: fmt.Sprintf("%s: %s", c.Info.Name, warn.Text), 182 FromLinter: goCriticName, 183 } 184 185 if warn.HasQuickFix() { 186 issue.Replacement = &result.Replacement{ 187 Inline: &result.InlineFix{ 188 StartCol: pos.Column - 1, 189 Length: int(warn.Suggestion.To - warn.Suggestion.From), 190 NewString: string(warn.Suggestion.Replacement), 191 }, 192 } 193 } 194 195 res = append(res, issue) 196 } 197 } 198 199 return res 200 } 201 202 func (w *goCriticWrapper) configureCheckerInfo(info *gocriticlinter.CheckerInfo, allParams map[string]config.GoCriticCheckSettings) error { 203 params := allParams[strings.ToLower(info.Name)] 204 if params == nil { // no config for this checker 205 return nil 206 } 207 208 infoParams := normalizeCheckerInfoParams(info) 209 for k, p := range params { 210 v, ok := infoParams[k] 211 if ok { 212 v.Value = w.normalizeCheckerParamsValue(p) 213 continue 214 } 215 216 // param `k` isn't supported 217 if len(info.Params) == 0 { 218 return fmt.Errorf("checker %s config param %s doesn't exist: checker doesn't have params", 219 info.Name, k) 220 } 221 222 var supportedKeys []string 223 for sk := range info.Params { 224 supportedKeys = append(supportedKeys, sk) 225 } 226 sort.Strings(supportedKeys) 227 228 return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s", 229 info.Name, k, supportedKeys) 230 } 231 232 return nil 233 } 234 235 func normalizeCheckerInfoParams(info *gocriticlinter.CheckerInfo) gocriticlinter.CheckerParams { 236 // lowercase info param keys here because golangci-lint's config parser lowercases all strings 237 ret := gocriticlinter.CheckerParams{} 238 for k, v := range info.Params { 239 ret[strings.ToLower(k)] = v 240 } 241 242 return ret 243 } 244 245 // normalizeCheckerParamsValue normalizes value types. 246 // go-critic asserts that CheckerParam.Value has some specific types, 247 // but the file parsers (TOML, YAML, JSON) don't create the same representation for raw type. 248 // then we have to convert value types into the expected value types. 249 // Maybe in the future, this kind of conversion will be done in go-critic itself. 250 func (w *goCriticWrapper) normalizeCheckerParamsValue(p any) any { 251 rv := reflect.ValueOf(p) 252 switch rv.Type().Kind() { 253 case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: 254 return int(rv.Int()) 255 case reflect.Bool: 256 return rv.Bool() 257 case reflect.String: 258 // Perform variable substitution. 259 return strings.ReplaceAll(rv.String(), "${configDir}", w.cfg.GetConfigDir()) 260 default: 261 return p 262 } 263 } 264 265 // TODO(ldez): rewrite and simplify goCriticSettingsWrapper. 266 267 type goCriticSettingsWrapper struct { 268 *config.GoCriticSettings 269 270 logger logutils.Log 271 272 allCheckers []*gocriticlinter.CheckerInfo 273 allCheckerMap map[string]*gocriticlinter.CheckerInfo 274 275 inferredEnabledChecks map[string]bool 276 } 277 278 func newGoCriticSettingsWrapper(settings *config.GoCriticSettings, logger logutils.Log) *goCriticSettingsWrapper { 279 allCheckers := gocriticlinter.GetCheckersInfo() 280 281 allCheckerMap := make(map[string]*gocriticlinter.CheckerInfo) 282 for _, checkInfo := range allCheckers { 283 allCheckerMap[checkInfo.Name] = checkInfo 284 } 285 286 return &goCriticSettingsWrapper{ 287 GoCriticSettings: settings, 288 logger: logger, 289 allCheckers: allCheckers, 290 allCheckerMap: allCheckerMap, 291 inferredEnabledChecks: map[string]bool{}, 292 } 293 } 294 295 func (s *goCriticSettingsWrapper) buildTagToCheckersMap() map[string][]string { 296 tagToCheckers := map[string][]string{} 297 298 for _, checker := range s.allCheckers { 299 for _, tag := range checker.Tags { 300 tagToCheckers[tag] = append(tagToCheckers[tag], checker.Name) 301 } 302 } 303 304 return tagToCheckers 305 } 306 307 func (s *goCriticSettingsWrapper) checkerTagsDebugf() { 308 if !isGoCriticDebug { 309 return 310 } 311 312 tagToCheckers := s.buildTagToCheckersMap() 313 314 allTags := make([]string, 0, len(tagToCheckers)) 315 for tag := range tagToCheckers { 316 allTags = append(allTags, tag) 317 } 318 319 sort.Strings(allTags) 320 321 goCriticDebugf("All gocritic existing tags and checks:") 322 for _, tag := range allTags { 323 debugChecksListf(tagToCheckers[tag], " tag %q", tag) 324 } 325 } 326 327 func (s *goCriticSettingsWrapper) disabledCheckersDebugf() { 328 if !isGoCriticDebug { 329 return 330 } 331 332 var disabledCheckers []string 333 for _, checker := range s.allCheckers { 334 if s.inferredEnabledChecks[strings.ToLower(checker.Name)] { 335 continue 336 } 337 338 disabledCheckers = append(disabledCheckers, checker.Name) 339 } 340 341 if len(disabledCheckers) == 0 { 342 goCriticDebugf("All checks are enabled") 343 } else { 344 debugChecksListf(disabledCheckers, "Final not used") 345 } 346 } 347 348 func (s *goCriticSettingsWrapper) inferEnabledChecks() { 349 s.checkerTagsDebugf() 350 351 enabledByDefaultChecks := s.getDefaultEnabledCheckersNames() 352 debugChecksListf(enabledByDefaultChecks, "Enabled by default") 353 354 disabledByDefaultChecks := s.getDefaultDisabledCheckersNames() 355 debugChecksListf(disabledByDefaultChecks, "Disabled by default") 356 357 enabledChecks := make([]string, 0, len(s.EnabledTags)+len(enabledByDefaultChecks)) 358 359 // EnabledTags 360 if len(s.EnabledTags) != 0 { 361 tagToCheckers := s.buildTagToCheckersMap() 362 for _, tag := range s.EnabledTags { 363 enabledChecks = append(enabledChecks, tagToCheckers[tag]...) 364 } 365 366 debugChecksListf(enabledChecks, "Enabled by config tags %s", sprintStrings(s.EnabledTags)) 367 } 368 369 if !(len(s.EnabledTags) == 0 && len(s.EnabledChecks) != 0) { 370 // don't use default checks only if we have no enabled tags and enable some checks manually 371 enabledChecks = append(enabledChecks, enabledByDefaultChecks...) 372 } 373 374 // DisabledTags 375 if len(s.DisabledTags) != 0 { 376 enabledChecks = s.filterByDisableTags(enabledChecks, s.DisabledTags) 377 } 378 379 // EnabledChecks 380 if len(s.EnabledChecks) != 0 { 381 debugChecksListf(s.EnabledChecks, "Enabled by config") 382 383 alreadyEnabledChecksSet := stringsSliceToSet(enabledChecks) 384 for _, enabledCheck := range s.EnabledChecks { 385 if alreadyEnabledChecksSet[enabledCheck] { 386 s.logger.Warnf("%s: no need to enable check %q: it's already enabled", goCriticName, enabledCheck) 387 continue 388 } 389 enabledChecks = append(enabledChecks, enabledCheck) 390 } 391 } 392 393 // DisabledChecks 394 if len(s.DisabledChecks) != 0 { 395 debugChecksListf(s.DisabledChecks, "Disabled by config") 396 397 enabledChecksSet := stringsSliceToSet(enabledChecks) 398 for _, disabledCheck := range s.DisabledChecks { 399 if !enabledChecksSet[disabledCheck] { 400 s.logger.Warnf("%s: check %q was explicitly disabled via config. However, as this check "+ 401 "is disabled by default, there is no need to explicitly disable it via config.", goCriticName, disabledCheck) 402 continue 403 } 404 delete(enabledChecksSet, disabledCheck) 405 } 406 407 enabledChecks = nil 408 for enabledCheck := range enabledChecksSet { 409 enabledChecks = append(enabledChecks, enabledCheck) 410 } 411 } 412 413 s.inferredEnabledChecks = map[string]bool{} 414 for _, check := range enabledChecks { 415 s.inferredEnabledChecks[strings.ToLower(check)] = true 416 } 417 418 debugChecksListf(enabledChecks, "Final used") 419 420 s.disabledCheckersDebugf() 421 } 422 423 func (s *goCriticSettingsWrapper) validate() error { 424 if len(s.EnabledTags) == 0 { 425 if len(s.EnabledChecks) != 0 && len(s.DisabledChecks) != 0 { 426 return errors.New("both enabled and disabled check aren't allowed for gocritic") 427 } 428 } else { 429 if err := validateStringsUniq(s.EnabledTags); err != nil { 430 return fmt.Errorf("validate enabled tags: %w", err) 431 } 432 433 tagToCheckers := s.buildTagToCheckersMap() 434 435 for _, tag := range s.EnabledTags { 436 if _, ok := tagToCheckers[tag]; !ok { 437 return fmt.Errorf("gocritic [enabled]tag %q doesn't exist", tag) 438 } 439 } 440 } 441 442 if len(s.DisabledTags) > 0 { 443 tagToCheckers := s.buildTagToCheckersMap() 444 for _, tag := range s.EnabledTags { 445 if _, ok := tagToCheckers[tag]; !ok { 446 return fmt.Errorf("gocritic [disabled]tag %q doesn't exist", tag) 447 } 448 } 449 } 450 451 if err := validateStringsUniq(s.EnabledChecks); err != nil { 452 return fmt.Errorf("validate enabled checks: %w", err) 453 } 454 455 if err := validateStringsUniq(s.DisabledChecks); err != nil { 456 return fmt.Errorf("validate disabled checks: %w", err) 457 } 458 459 if err := s.validateCheckerNames(); err != nil { 460 return fmt.Errorf("validation failed: %w", err) 461 } 462 463 return nil 464 } 465 466 func (s *goCriticSettingsWrapper) isCheckEnabled(name string) bool { 467 return s.inferredEnabledChecks[strings.ToLower(name)] 468 } 469 470 // getAllCheckerNames returns a map containing all checker names supported by gocritic. 471 func (s *goCriticSettingsWrapper) getAllCheckerNames() map[string]bool { 472 allCheckerNames := make(map[string]bool, len(s.allCheckers)) 473 474 for _, checker := range s.allCheckers { 475 allCheckerNames[strings.ToLower(checker.Name)] = true 476 } 477 478 return allCheckerNames 479 } 480 481 func (s *goCriticSettingsWrapper) getDefaultEnabledCheckersNames() []string { 482 var enabled []string 483 484 for _, info := range s.allCheckers { 485 enable := s.isEnabledByDefaultCheck(info) 486 if enable { 487 enabled = append(enabled, info.Name) 488 } 489 } 490 491 return enabled 492 } 493 494 func (s *goCriticSettingsWrapper) getDefaultDisabledCheckersNames() []string { 495 var disabled []string 496 497 for _, info := range s.allCheckers { 498 enable := s.isEnabledByDefaultCheck(info) 499 if !enable { 500 disabled = append(disabled, info.Name) 501 } 502 } 503 504 return disabled 505 } 506 507 func (s *goCriticSettingsWrapper) validateCheckerNames() error { 508 allowedNames := s.getAllCheckerNames() 509 510 for _, name := range s.EnabledChecks { 511 if !allowedNames[strings.ToLower(name)] { 512 return fmt.Errorf("enabled checker %s doesn't exist, all existing checkers: %s", 513 name, sprintAllowedCheckerNames(allowedNames)) 514 } 515 } 516 517 for _, name := range s.DisabledChecks { 518 if !allowedNames[strings.ToLower(name)] { 519 return fmt.Errorf("disabled checker %s doesn't exist, all existing checkers: %s", 520 name, sprintAllowedCheckerNames(allowedNames)) 521 } 522 } 523 524 for checkName := range s.SettingsPerCheck { 525 if _, ok := allowedNames[checkName]; !ok { 526 return fmt.Errorf("invalid setting, checker %s doesn't exist, all existing checkers: %s", 527 checkName, sprintAllowedCheckerNames(allowedNames)) 528 } 529 530 if !s.isCheckEnabled(checkName) { 531 s.logger.Warnf("%s: settings were provided for not enabled check %q", goCriticName, checkName) 532 } 533 } 534 535 return nil 536 } 537 538 func (s *goCriticSettingsWrapper) getLowerCasedParams() map[string]config.GoCriticCheckSettings { 539 ret := make(map[string]config.GoCriticCheckSettings, len(s.SettingsPerCheck)) 540 541 for checker, params := range s.SettingsPerCheck { 542 ret[strings.ToLower(checker)] = params 543 } 544 545 return ret 546 } 547 548 func (s *goCriticSettingsWrapper) filterByDisableTags(enabledChecks, disableTags []string) []string { 549 enabledChecksSet := stringsSliceToSet(enabledChecks) 550 551 for _, enabledCheck := range enabledChecks { 552 checkInfo, checkInfoExists := s.allCheckerMap[enabledCheck] 553 if !checkInfoExists { 554 s.logger.Warnf("%s: check %q was not exists via filtering disabled tags", goCriticName, enabledCheck) 555 continue 556 } 557 558 hitTags := intersectStringSlice(checkInfo.Tags, disableTags) 559 if len(hitTags) != 0 { 560 delete(enabledChecksSet, enabledCheck) 561 } 562 } 563 564 debugChecksListf(enabledChecks, "Disabled by config tags %s", sprintStrings(disableTags)) 565 566 enabledChecks = nil 567 for enabledCheck := range enabledChecksSet { 568 enabledChecks = append(enabledChecks, enabledCheck) 569 } 570 571 return enabledChecks 572 } 573 574 func (s *goCriticSettingsWrapper) isEnabledByDefaultCheck(info *gocriticlinter.CheckerInfo) bool { 575 return !info.HasTag("experimental") && 576 !info.HasTag("opinionated") && 577 !info.HasTag("performance") 578 } 579 580 func validateStringsUniq(ss []string) error { 581 set := map[string]bool{} 582 583 for _, s := range ss { 584 _, ok := set[s] 585 if ok { 586 return fmt.Errorf("%q occurs multiple times in list", s) 587 } 588 set[s] = true 589 } 590 591 return nil 592 } 593 594 func intersectStringSlice(s1, s2 []string) []string { 595 s1Map := make(map[string]struct{}, len(s1)) 596 597 for _, s := range s1 { 598 s1Map[s] = struct{}{} 599 } 600 601 results := make([]string, 0) 602 for _, s := range s2 { 603 if _, exists := s1Map[s]; exists { 604 results = append(results, s) 605 } 606 } 607 608 return results 609 } 610 611 func sprintAllowedCheckerNames(allowedNames map[string]bool) string { 612 namesSlice := make([]string, 0, len(allowedNames)) 613 614 for name := range allowedNames { 615 namesSlice = append(namesSlice, name) 616 } 617 618 return sprintStrings(namesSlice) 619 } 620 621 func sprintStrings(ss []string) string { 622 sort.Strings(ss) 623 return fmt.Sprint(ss) 624 } 625 626 func debugChecksListf(checks []string, format string, args ...any) { 627 if !isGoCriticDebug { 628 return 629 } 630 631 goCriticDebugf("%s checks (%d): %s", fmt.Sprintf(format, args...), len(checks), sprintStrings(checks)) 632 } 633 634 func stringsSliceToSet(ss []string) map[string]bool { 635 ret := make(map[string]bool, len(ss)) 636 for _, s := range ss { 637 ret[s] = true 638 } 639 640 return ret 641 }