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