github.com/nozzle/golangci-lint@v1.49.0-nz3/pkg/golinters/gocritic.go (about) 1 package golinters 2 3 import ( 4 "fmt" 5 "go/ast" 6 "go/types" 7 "path/filepath" 8 "reflect" 9 "runtime" 10 "sort" 11 "strings" 12 "sync" 13 14 "github.com/go-critic/go-critic/checkers" 15 gocriticlinter "github.com/go-critic/go-critic/framework/linter" 16 "github.com/pkg/errors" 17 "golang.org/x/tools/go/analysis" 18 19 "github.com/golangci/golangci-lint/pkg/config" 20 "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" 21 "github.com/golangci/golangci-lint/pkg/lint/linter" 22 "github.com/golangci/golangci-lint/pkg/logutils" 23 "github.com/golangci/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) (interface{}, 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 enabledCheckers, err := w.buildEnabledCheckers(linterCtx) 117 if err != nil { 118 return nil, err 119 } 120 121 linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg) 122 123 pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files) 124 125 issues := make([]goanalysis.Issue, 0, len(pkgIssues)) 126 for i := range pkgIssues { 127 issues = append(issues, goanalysis.NewIssue(&pkgIssues[i], pass)) 128 } 129 130 return issues, nil 131 } 132 133 func (w *goCriticWrapper) buildEnabledCheckers(linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) { 134 allParams := w.settingsWrapper.getLowerCasedParams() 135 136 var enabledCheckers []*gocriticlinter.Checker 137 for _, info := range gocriticlinter.GetCheckersInfo() { 138 if !w.settingsWrapper.isCheckEnabled(info.Name) { 139 continue 140 } 141 142 if err := w.configureCheckerInfo(info, allParams); err != nil { 143 return nil, err 144 } 145 146 c, err := gocriticlinter.NewChecker(linterCtx, info) 147 if err != nil { 148 return nil, err 149 } 150 enabledCheckers = append(enabledCheckers, c) 151 } 152 153 return enabledCheckers, nil 154 } 155 156 func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checks []*gocriticlinter.Checker, 157 files []*ast.File) []result.Issue { 158 var res []result.Issue 159 for _, f := range files { 160 filename := filepath.Base(linterCtx.FileSet.Position(f.Pos()).Filename) 161 linterCtx.SetFileInfo(filename, f) 162 163 issues := runGocriticOnFile(linterCtx, f, checks) 164 res = append(res, issues...) 165 } 166 return res 167 } 168 169 func runGocriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []*gocriticlinter.Checker) []result.Issue { 170 var res []result.Issue 171 172 for _, c := range checks { 173 // All checkers are expected to use *lint.Context 174 // as read-only structure, so no copying is required. 175 for _, warn := range c.Check(f) { 176 pos := linterCtx.FileSet.Position(warn.Node.Pos()) 177 issue := result.Issue{ 178 Pos: pos, 179 Text: fmt.Sprintf("%s: %s", c.Info.Name, warn.Text), 180 FromLinter: goCriticName, 181 } 182 183 if warn.HasQuickFix() { 184 issue.Replacement = &result.Replacement{ 185 Inline: &result.InlineFix{ 186 StartCol: pos.Column - 1, 187 Length: int(warn.Node.End() - warn.Node.Pos()), 188 NewString: string(warn.Suggestion.Replacement), 189 }, 190 } 191 } 192 193 res = append(res, issue) 194 } 195 } 196 197 return res 198 } 199 200 func (w *goCriticWrapper) configureCheckerInfo(info *gocriticlinter.CheckerInfo, allParams map[string]config.GoCriticCheckSettings) error { 201 params := allParams[strings.ToLower(info.Name)] 202 if params == nil { // no config for this checker 203 return nil 204 } 205 206 infoParams := normalizeCheckerInfoParams(info) 207 for k, p := range params { 208 v, ok := infoParams[k] 209 if ok { 210 v.Value = w.normalizeCheckerParamsValue(p) 211 continue 212 } 213 214 // param `k` isn't supported 215 if len(info.Params) == 0 { 216 return fmt.Errorf("checker %s config param %s doesn't exist: checker doesn't have params", 217 info.Name, k) 218 } 219 220 var supportedKeys []string 221 for sk := range info.Params { 222 supportedKeys = append(supportedKeys, sk) 223 } 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 interface{}) interface{} { 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 := make([]string, 0, len(tagToCheckers)) 313 for tag := range tagToCheckers { 314 allTags = append(allTags, tag) 315 } 316 317 sort.Strings(allTags) 318 319 goCriticDebugf("All gocritic existing tags and checks:") 320 for _, tag := range allTags { 321 debugChecksListf(tagToCheckers[tag], " tag %q", tag) 322 } 323 } 324 325 func (s *goCriticSettingsWrapper) disabledCheckersDebugf() { 326 if !isGoCriticDebug { 327 return 328 } 329 330 var disabledCheckers []string 331 for _, checker := range s.allCheckers { 332 if s.inferredEnabledChecks[strings.ToLower(checker.Name)] { 333 continue 334 } 335 336 disabledCheckers = append(disabledCheckers, checker.Name) 337 } 338 339 if len(disabledCheckers) == 0 { 340 goCriticDebugf("All checks are enabled") 341 } else { 342 debugChecksListf(disabledCheckers, "Final not used") 343 } 344 } 345 346 func (s *goCriticSettingsWrapper) inferEnabledChecks() { 347 s.checkerTagsDebugf() 348 349 enabledByDefaultChecks := s.getDefaultEnabledCheckersNames() 350 debugChecksListf(enabledByDefaultChecks, "Enabled by default") 351 352 disabledByDefaultChecks := s.getDefaultDisabledCheckersNames() 353 debugChecksListf(disabledByDefaultChecks, "Disabled by default") 354 355 enabledChecks := make([]string, 0, len(s.EnabledTags)+len(enabledByDefaultChecks)) 356 357 // EnabledTags 358 if len(s.EnabledTags) != 0 { 359 tagToCheckers := s.buildTagToCheckersMap() 360 for _, tag := range s.EnabledTags { 361 enabledChecks = append(enabledChecks, tagToCheckers[tag]...) 362 } 363 364 debugChecksListf(enabledChecks, "Enabled by config tags %s", sprintStrings(s.EnabledTags)) 365 } 366 367 if !(len(s.EnabledTags) == 0 && len(s.EnabledChecks) != 0) { 368 // don't use default checks only if we have no enabled tags and enable some checks manually 369 enabledChecks = append(enabledChecks, enabledByDefaultChecks...) 370 } 371 372 // DisabledTags 373 if len(s.DisabledTags) != 0 { 374 enabledChecks = s.filterByDisableTags(enabledChecks, s.DisabledTags) 375 } 376 377 // EnabledChecks 378 if len(s.EnabledChecks) != 0 { 379 debugChecksListf(s.EnabledChecks, "Enabled by config") 380 381 alreadyEnabledChecksSet := stringsSliceToSet(enabledChecks) 382 for _, enabledCheck := range s.EnabledChecks { 383 if alreadyEnabledChecksSet[enabledCheck] { 384 s.logger.Warnf("%s: no need to enable check %q: it's already enabled", goCriticName, enabledCheck) 385 continue 386 } 387 enabledChecks = append(enabledChecks, enabledCheck) 388 } 389 } 390 391 // DisabledChecks 392 if len(s.DisabledChecks) != 0 { 393 debugChecksListf(s.DisabledChecks, "Disabled by config") 394 395 enabledChecksSet := stringsSliceToSet(enabledChecks) 396 for _, disabledCheck := range s.DisabledChecks { 397 if !enabledChecksSet[disabledCheck] { 398 s.logger.Warnf("%s: check %q was explicitly disabled via config. However, as this check "+ 399 "is disabled by default, there is no need to explicitly disable it via config.", goCriticName, disabledCheck) 400 continue 401 } 402 delete(enabledChecksSet, disabledCheck) 403 } 404 405 enabledChecks = nil 406 for enabledCheck := range enabledChecksSet { 407 enabledChecks = append(enabledChecks, enabledCheck) 408 } 409 } 410 411 s.inferredEnabledChecks = map[string]bool{} 412 for _, check := range enabledChecks { 413 s.inferredEnabledChecks[strings.ToLower(check)] = true 414 } 415 416 debugChecksListf(enabledChecks, "Final used") 417 418 s.disabledCheckersDebugf() 419 } 420 421 func (s *goCriticSettingsWrapper) validate() error { 422 if len(s.EnabledTags) == 0 { 423 if len(s.EnabledChecks) != 0 && len(s.DisabledChecks) != 0 { 424 return errors.New("both enabled and disabled check aren't allowed for gocritic") 425 } 426 } else { 427 if err := validateStringsUniq(s.EnabledTags); err != nil { 428 return errors.Wrap(err, "validate enabled tags") 429 } 430 431 tagToCheckers := s.buildTagToCheckersMap() 432 433 for _, tag := range s.EnabledTags { 434 if _, ok := tagToCheckers[tag]; !ok { 435 return fmt.Errorf("gocritic [enabled]tag %q doesn't exist", tag) 436 } 437 } 438 } 439 440 if len(s.DisabledTags) > 0 { 441 tagToCheckers := s.buildTagToCheckersMap() 442 for _, tag := range s.EnabledTags { 443 if _, ok := tagToCheckers[tag]; !ok { 444 return fmt.Errorf("gocritic [disabled]tag %q doesn't exist", tag) 445 } 446 } 447 } 448 449 if err := validateStringsUniq(s.EnabledChecks); err != nil { 450 return errors.Wrap(err, "validate enabled checks") 451 } 452 453 if err := validateStringsUniq(s.DisabledChecks); err != nil { 454 return errors.Wrap(err, "validate disabled checks") 455 } 456 457 if err := s.validateCheckerNames(); err != nil { 458 return errors.Wrap(err, "validation failed") 459 } 460 461 return nil 462 } 463 464 func (s *goCriticSettingsWrapper) isCheckEnabled(name string) bool { 465 return s.inferredEnabledChecks[strings.ToLower(name)] 466 } 467 468 // getAllCheckerNames returns a map containing all checker names supported by gocritic. 469 func (s *goCriticSettingsWrapper) getAllCheckerNames() map[string]bool { 470 allCheckerNames := make(map[string]bool, len(s.allCheckers)) 471 472 for _, checker := range s.allCheckers { 473 allCheckerNames[strings.ToLower(checker.Name)] = true 474 } 475 476 return allCheckerNames 477 } 478 479 func (s *goCriticSettingsWrapper) getDefaultEnabledCheckersNames() []string { 480 var enabled []string 481 482 for _, info := range s.allCheckers { 483 enable := s.isEnabledByDefaultCheck(info) 484 if enable { 485 enabled = append(enabled, info.Name) 486 } 487 } 488 489 return enabled 490 } 491 492 func (s *goCriticSettingsWrapper) getDefaultDisabledCheckersNames() []string { 493 var disabled []string 494 495 for _, info := range s.allCheckers { 496 enable := s.isEnabledByDefaultCheck(info) 497 if !enable { 498 disabled = append(disabled, info.Name) 499 } 500 } 501 502 return disabled 503 } 504 505 func (s *goCriticSettingsWrapper) validateCheckerNames() error { 506 allowedNames := s.getAllCheckerNames() 507 508 for _, name := range s.EnabledChecks { 509 if !allowedNames[strings.ToLower(name)] { 510 return fmt.Errorf("enabled checker %s doesn't exist, all existing checkers: %s", 511 name, sprintAllowedCheckerNames(allowedNames)) 512 } 513 } 514 515 for _, name := range s.DisabledChecks { 516 if !allowedNames[strings.ToLower(name)] { 517 return fmt.Errorf("disabled checker %s doesn't exist, all existing checkers: %s", 518 name, sprintAllowedCheckerNames(allowedNames)) 519 } 520 } 521 522 for checkName := range s.SettingsPerCheck { 523 if _, ok := allowedNames[checkName]; !ok { 524 return fmt.Errorf("invalid setting, checker %s doesn't exist, all existing checkers: %s", 525 checkName, sprintAllowedCheckerNames(allowedNames)) 526 } 527 528 if !s.isCheckEnabled(checkName) { 529 s.logger.Warnf("%s: settings were provided for not enabled check %q", goCriticName, checkName) 530 } 531 } 532 533 return nil 534 } 535 536 func (s *goCriticSettingsWrapper) getLowerCasedParams() map[string]config.GoCriticCheckSettings { 537 ret := make(map[string]config.GoCriticCheckSettings, len(s.SettingsPerCheck)) 538 539 for checker, params := range s.SettingsPerCheck { 540 ret[strings.ToLower(checker)] = params 541 } 542 543 return ret 544 } 545 546 func (s *goCriticSettingsWrapper) filterByDisableTags(enabledChecks, disableTags []string) []string { 547 enabledChecksSet := stringsSliceToSet(enabledChecks) 548 549 for _, enabledCheck := range enabledChecks { 550 checkInfo, checkInfoExists := s.allCheckerMap[enabledCheck] 551 if !checkInfoExists { 552 s.logger.Warnf("%s: check %q was not exists via filtering disabled tags", goCriticName, enabledCheck) 553 continue 554 } 555 556 hitTags := intersectStringSlice(checkInfo.Tags, disableTags) 557 if len(hitTags) != 0 { 558 delete(enabledChecksSet, enabledCheck) 559 } 560 } 561 562 debugChecksListf(enabledChecks, "Disabled by config tags %s", sprintStrings(disableTags)) 563 564 enabledChecks = nil 565 for enabledCheck := range enabledChecksSet { 566 enabledChecks = append(enabledChecks, enabledCheck) 567 } 568 569 return enabledChecks 570 } 571 572 func (s *goCriticSettingsWrapper) isEnabledByDefaultCheck(info *gocriticlinter.CheckerInfo) bool { 573 return !info.HasTag("experimental") && 574 !info.HasTag("opinionated") && 575 !info.HasTag("performance") 576 } 577 578 func validateStringsUniq(ss []string) error { 579 set := map[string]bool{} 580 581 for _, s := range ss { 582 _, ok := set[s] 583 if ok { 584 return fmt.Errorf("%q occurs multiple times in list", s) 585 } 586 set[s] = true 587 } 588 589 return nil 590 } 591 592 func intersectStringSlice(s1, s2 []string) []string { 593 s1Map := make(map[string]struct{}, len(s1)) 594 595 for _, s := range s1 { 596 s1Map[s] = struct{}{} 597 } 598 599 results := make([]string, 0) 600 for _, s := range s2 { 601 if _, exists := s1Map[s]; exists { 602 results = append(results, s) 603 } 604 } 605 606 return results 607 } 608 609 func sprintAllowedCheckerNames(allowedNames map[string]bool) string { 610 namesSlice := make([]string, 0, len(allowedNames)) 611 612 for name := range allowedNames { 613 namesSlice = append(namesSlice, name) 614 } 615 616 return sprintStrings(namesSlice) 617 } 618 619 func sprintStrings(ss []string) string { 620 sort.Strings(ss) 621 return fmt.Sprint(ss) 622 } 623 624 func debugChecksListf(checks []string, format string, args ...interface{}) { 625 if !isGoCriticDebug { 626 return 627 } 628 629 goCriticDebugf("%s checks (%d): %s", fmt.Sprintf(format, args...), len(checks), sprintStrings(checks)) 630 } 631 632 func stringsSliceToSet(ss []string) map[string]bool { 633 ret := make(map[string]bool, len(ss)) 634 for _, s := range ss { 635 ret[s] = true 636 } 637 638 return ret 639 }