package golinters import ( "errors" "fmt" "go/ast" "go/types" "path/filepath" "reflect" "runtime" "sort" "strings" "sync" "github.com/go-critic/go-critic/checkers" gocriticlinter "github.com/go-critic/go-critic/linter" "golang.org/x/tools/go/analysis" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/golinters/goanalysis" "github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/result" ) const goCriticName = "gocritic" var ( goCriticDebugf = logutils.Debug(logutils.DebugKeyGoCritic) isGoCriticDebug = logutils.HaveDebugTag(logutils.DebugKeyGoCritic) ) func NewGoCritic(settings *config.GoCriticSettings, cfg *config.Config) *goanalysis.Linter { var mu sync.Mutex var resIssues []goanalysis.Issue wrapper := &goCriticWrapper{ cfg: cfg, sizes: types.SizesFor("gc", runtime.GOARCH), } analyzer := &analysis.Analyzer{ Name: goCriticName, Doc: goanalysis.TheOnlyanalyzerDoc, Run: func(pass *analysis.Pass) (any, error) { issues, err := wrapper.run(pass) if err != nil { return nil, err } if len(issues) == 0 { return nil, nil } mu.Lock() resIssues = append(resIssues, issues...) mu.Unlock() return nil, nil }, } return goanalysis.NewLinter( goCriticName, `Provides diagnostics that check for bugs, performance and style issues. Extensible without recompilation through dynamic rules. Dynamic rules are written declaratively with AST patterns, filters, report message and optional suggestion.`, []*analysis.Analyzer{analyzer}, nil, ). WithContextSetter(func(context *linter.Context) { wrapper.init(settings, context.Log) }). WithIssuesReporter(func(*linter.Context) []goanalysis.Issue { return resIssues }).WithLoadMode(goanalysis.LoadModeTypesInfo) } type goCriticWrapper struct { settingsWrapper *goCriticSettingsWrapper cfg *config.Config sizes types.Sizes once sync.Once } func (w *goCriticWrapper) init(settings *config.GoCriticSettings, logger logutils.Log) { if settings == nil { return } w.once.Do(func() { err := checkers.InitEmbeddedRules() if err != nil { logger.Fatalf("%s: %v: setting an explicit GOROOT can fix this problem.", goCriticName, err) } }) settingsWrapper := newGoCriticSettingsWrapper(settings, logger) settingsWrapper.inferEnabledChecks() if err := settingsWrapper.validate(); err != nil { logger.Fatalf("%s: invalid settings: %s", goCriticName, err) } w.settingsWrapper = settingsWrapper } func (w *goCriticWrapper) run(pass *analysis.Pass) ([]goanalysis.Issue, error) { if w.settingsWrapper == nil { return nil, fmt.Errorf("the settings wrapper is nil") } linterCtx := gocriticlinter.NewContext(pass.Fset, w.sizes) linterCtx.SetGoVersion(w.settingsWrapper.Go) enabledCheckers, err := w.buildEnabledCheckers(linterCtx) if err != nil { return nil, err } linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg) pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files) issues := make([]goanalysis.Issue, 0, len(pkgIssues)) for i := range pkgIssues { issues = append(issues, goanalysis.NewIssue(&pkgIssues[i], pass)) } return issues, nil } func (w *goCriticWrapper) buildEnabledCheckers(linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) { allParams := w.settingsWrapper.getLowerCasedParams() var enabledCheckers []*gocriticlinter.Checker for _, info := range gocriticlinter.GetCheckersInfo() { if !w.settingsWrapper.isCheckEnabled(info.Name) { continue } if err := w.configureCheckerInfo(info, allParams); err != nil { return nil, err } c, err := gocriticlinter.NewChecker(linterCtx, info) if err != nil { return nil, err } enabledCheckers = append(enabledCheckers, c) } return enabledCheckers, nil } func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checks []*gocriticlinter.Checker, files []*ast.File) []result.Issue { var res []result.Issue for _, f := range files { filename := filepath.Base(linterCtx.FileSet.Position(f.Pos()).Filename) linterCtx.SetFileInfo(filename, f) issues := runGocriticOnFile(linterCtx, f, checks) res = append(res, issues...) } return res } func runGocriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []*gocriticlinter.Checker) []result.Issue { var res []result.Issue for _, c := range checks { // All checkers are expected to use *lint.Context // as read-only structure, so no copying is required. for _, warn := range c.Check(f) { pos := linterCtx.FileSet.Position(warn.Pos) issue := result.Issue{ Pos: pos, Text: fmt.Sprintf("%s: %s", c.Info.Name, warn.Text), FromLinter: goCriticName, } if warn.HasQuickFix() { issue.Replacement = &result.Replacement{ Inline: &result.InlineFix{ StartCol: pos.Column - 1, Length: int(warn.Suggestion.To - warn.Suggestion.From), NewString: string(warn.Suggestion.Replacement), }, } } res = append(res, issue) } } return res } func (w *goCriticWrapper) configureCheckerInfo(info *gocriticlinter.CheckerInfo, allParams map[string]config.GoCriticCheckSettings) error { params := allParams[strings.ToLower(info.Name)] if params == nil { // no config for this checker return nil } infoParams := normalizeCheckerInfoParams(info) for k, p := range params { v, ok := infoParams[k] if ok { v.Value = w.normalizeCheckerParamsValue(p) continue } // param `k` isn't supported if len(info.Params) == 0 { return fmt.Errorf("checker %s config param %s doesn't exist: checker doesn't have params", info.Name, k) } var supportedKeys []string for sk := range info.Params { supportedKeys = append(supportedKeys, sk) } sort.Strings(supportedKeys) return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s", info.Name, k, supportedKeys) } return nil } func normalizeCheckerInfoParams(info *gocriticlinter.CheckerInfo) gocriticlinter.CheckerParams { // lowercase info param keys here because golangci-lint's config parser lowercases all strings ret := gocriticlinter.CheckerParams{} for k, v := range info.Params { ret[strings.ToLower(k)] = v } return ret } // normalizeCheckerParamsValue normalizes value types. // go-critic asserts that CheckerParam.Value has some specific types, // but the file parsers (TOML, YAML, JSON) don't create the same representation for raw type. // then we have to convert value types into the expected value types. // Maybe in the future, this kind of conversion will be done in go-critic itself. func (w *goCriticWrapper) normalizeCheckerParamsValue(p any) any { rv := reflect.ValueOf(p) switch rv.Type().Kind() { case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: return int(rv.Int()) case reflect.Bool: return rv.Bool() case reflect.String: // Perform variable substitution. return strings.ReplaceAll(rv.String(), "${configDir}", w.cfg.GetConfigDir()) default: return p } } // TODO(ldez): rewrite and simplify goCriticSettingsWrapper. type goCriticSettingsWrapper struct { *config.GoCriticSettings logger logutils.Log allCheckers []*gocriticlinter.CheckerInfo allCheckerMap map[string]*gocriticlinter.CheckerInfo inferredEnabledChecks map[string]bool } func newGoCriticSettingsWrapper(settings *config.GoCriticSettings, logger logutils.Log) *goCriticSettingsWrapper { allCheckers := gocriticlinter.GetCheckersInfo() allCheckerMap := make(map[string]*gocriticlinter.CheckerInfo) for _, checkInfo := range allCheckers { allCheckerMap[checkInfo.Name] = checkInfo } return &goCriticSettingsWrapper{ GoCriticSettings: settings, logger: logger, allCheckers: allCheckers, allCheckerMap: allCheckerMap, inferredEnabledChecks: map[string]bool{}, } } func (s *goCriticSettingsWrapper) buildTagToCheckersMap() map[string][]string { tagToCheckers := map[string][]string{} for _, checker := range s.allCheckers { for _, tag := range checker.Tags { tagToCheckers[tag] = append(tagToCheckers[tag], checker.Name) } } return tagToCheckers } func (s *goCriticSettingsWrapper) checkerTagsDebugf() { if !isGoCriticDebug { return } tagToCheckers := s.buildTagToCheckersMap() allTags := make([]string, 0, len(tagToCheckers)) for tag := range tagToCheckers { allTags = append(allTags, tag) } sort.Strings(allTags) goCriticDebugf("All gocritic existing tags and checks:") for _, tag := range allTags { debugChecksListf(tagToCheckers[tag], " tag %q", tag) } } func (s *goCriticSettingsWrapper) disabledCheckersDebugf() { if !isGoCriticDebug { return } var disabledCheckers []string for _, checker := range s.allCheckers { if s.inferredEnabledChecks[strings.ToLower(checker.Name)] { continue } disabledCheckers = append(disabledCheckers, checker.Name) } if len(disabledCheckers) == 0 { goCriticDebugf("All checks are enabled") } else { debugChecksListf(disabledCheckers, "Final not used") } } func (s *goCriticSettingsWrapper) inferEnabledChecks() { s.checkerTagsDebugf() enabledByDefaultChecks := s.getDefaultEnabledCheckersNames() debugChecksListf(enabledByDefaultChecks, "Enabled by default") disabledByDefaultChecks := s.getDefaultDisabledCheckersNames() debugChecksListf(disabledByDefaultChecks, "Disabled by default") enabledChecks := make([]string, 0, len(s.EnabledTags)+len(enabledByDefaultChecks)) // EnabledTags if len(s.EnabledTags) != 0 { tagToCheckers := s.buildTagToCheckersMap() for _, tag := range s.EnabledTags { enabledChecks = append(enabledChecks, tagToCheckers[tag]...) } debugChecksListf(enabledChecks, "Enabled by config tags %s", sprintStrings(s.EnabledTags)) } if !(len(s.EnabledTags) == 0 && len(s.EnabledChecks) != 0) { // don't use default checks only if we have no enabled tags and enable some checks manually enabledChecks = append(enabledChecks, enabledByDefaultChecks...) } // DisabledTags if len(s.DisabledTags) != 0 { enabledChecks = s.filterByDisableTags(enabledChecks, s.DisabledTags) } // EnabledChecks if len(s.EnabledChecks) != 0 { debugChecksListf(s.EnabledChecks, "Enabled by config") alreadyEnabledChecksSet := stringsSliceToSet(enabledChecks) for _, enabledCheck := range s.EnabledChecks { if alreadyEnabledChecksSet[enabledCheck] { s.logger.Warnf("%s: no need to enable check %q: it's already enabled", goCriticName, enabledCheck) continue } enabledChecks = append(enabledChecks, enabledCheck) } } // DisabledChecks if len(s.DisabledChecks) != 0 { debugChecksListf(s.DisabledChecks, "Disabled by config") enabledChecksSet := stringsSliceToSet(enabledChecks) for _, disabledCheck := range s.DisabledChecks { if !enabledChecksSet[disabledCheck] { s.logger.Warnf("%s: check %q was explicitly disabled via config. However, as this check "+ "is disabled by default, there is no need to explicitly disable it via config.", goCriticName, disabledCheck) continue } delete(enabledChecksSet, disabledCheck) } enabledChecks = nil for enabledCheck := range enabledChecksSet { enabledChecks = append(enabledChecks, enabledCheck) } } s.inferredEnabledChecks = map[string]bool{} for _, check := range enabledChecks { s.inferredEnabledChecks[strings.ToLower(check)] = true } debugChecksListf(enabledChecks, "Final used") s.disabledCheckersDebugf() } func (s *goCriticSettingsWrapper) validate() error { if len(s.EnabledTags) == 0 { if len(s.EnabledChecks) != 0 && len(s.DisabledChecks) != 0 { return errors.New("both enabled and disabled check aren't allowed for gocritic") } } else { if err := validateStringsUniq(s.EnabledTags); err != nil { return fmt.Errorf("validate enabled tags: %w", err) } tagToCheckers := s.buildTagToCheckersMap() for _, tag := range s.EnabledTags { if _, ok := tagToCheckers[tag]; !ok { return fmt.Errorf("gocritic [enabled]tag %q doesn't exist", tag) } } } if len(s.DisabledTags) > 0 { tagToCheckers := s.buildTagToCheckersMap() for _, tag := range s.EnabledTags { if _, ok := tagToCheckers[tag]; !ok { return fmt.Errorf("gocritic [disabled]tag %q doesn't exist", tag) } } } if err := validateStringsUniq(s.EnabledChecks); err != nil { return fmt.Errorf("validate enabled checks: %w", err) } if err := validateStringsUniq(s.DisabledChecks); err != nil { return fmt.Errorf("validate disabled checks: %w", err) } if err := s.validateCheckerNames(); err != nil { return fmt.Errorf("validation failed: %w", err) } return nil } func (s *goCriticSettingsWrapper) isCheckEnabled(name string) bool { return s.inferredEnabledChecks[strings.ToLower(name)] } // getAllCheckerNames returns a map containing all checker names supported by gocritic. func (s *goCriticSettingsWrapper) getAllCheckerNames() map[string]bool { allCheckerNames := make(map[string]bool, len(s.allCheckers)) for _, checker := range s.allCheckers { allCheckerNames[strings.ToLower(checker.Name)] = true } return allCheckerNames } func (s *goCriticSettingsWrapper) getDefaultEnabledCheckersNames() []string { var enabled []string for _, info := range s.allCheckers { enable := s.isEnabledByDefaultCheck(info) if enable { enabled = append(enabled, info.Name) } } return enabled } func (s *goCriticSettingsWrapper) getDefaultDisabledCheckersNames() []string { var disabled []string for _, info := range s.allCheckers { enable := s.isEnabledByDefaultCheck(info) if !enable { disabled = append(disabled, info.Name) } } return disabled } func (s *goCriticSettingsWrapper) validateCheckerNames() error { allowedNames := s.getAllCheckerNames() for _, name := range s.EnabledChecks { if !allowedNames[strings.ToLower(name)] { return fmt.Errorf("enabled checker %s doesn't exist, all existing checkers: %s", name, sprintAllowedCheckerNames(allowedNames)) } } for _, name := range s.DisabledChecks { if !allowedNames[strings.ToLower(name)] { return fmt.Errorf("disabled checker %s doesn't exist, all existing checkers: %s", name, sprintAllowedCheckerNames(allowedNames)) } } for checkName := range s.SettingsPerCheck { if _, ok := allowedNames[checkName]; !ok { return fmt.Errorf("invalid setting, checker %s doesn't exist, all existing checkers: %s", checkName, sprintAllowedCheckerNames(allowedNames)) } if !s.isCheckEnabled(checkName) { s.logger.Warnf("%s: settings were provided for not enabled check %q", goCriticName, checkName) } } return nil } func (s *goCriticSettingsWrapper) getLowerCasedParams() map[string]config.GoCriticCheckSettings { ret := make(map[string]config.GoCriticCheckSettings, len(s.SettingsPerCheck)) for checker, params := range s.SettingsPerCheck { ret[strings.ToLower(checker)] = params } return ret } func (s *goCriticSettingsWrapper) filterByDisableTags(enabledChecks, disableTags []string) []string { enabledChecksSet := stringsSliceToSet(enabledChecks) for _, enabledCheck := range enabledChecks { checkInfo, checkInfoExists := s.allCheckerMap[enabledCheck] if !checkInfoExists { s.logger.Warnf("%s: check %q was not exists via filtering disabled tags", goCriticName, enabledCheck) continue } hitTags := intersectStringSlice(checkInfo.Tags, disableTags) if len(hitTags) != 0 { delete(enabledChecksSet, enabledCheck) } } debugChecksListf(enabledChecks, "Disabled by config tags %s", sprintStrings(disableTags)) enabledChecks = nil for enabledCheck := range enabledChecksSet { enabledChecks = append(enabledChecks, enabledCheck) } return enabledChecks } func (s *goCriticSettingsWrapper) isEnabledByDefaultCheck(info *gocriticlinter.CheckerInfo) bool { return !info.HasTag("experimental") && !info.HasTag("opinionated") && !info.HasTag("performance") } func validateStringsUniq(ss []string) error { set := map[string]bool{} for _, s := range ss { _, ok := set[s] if ok { return fmt.Errorf("%q occurs multiple times in list", s) } set[s] = true } return nil } func intersectStringSlice(s1, s2 []string) []string { s1Map := make(map[string]struct{}, len(s1)) for _, s := range s1 { s1Map[s] = struct{}{} } results := make([]string, 0) for _, s := range s2 { if _, exists := s1Map[s]; exists { results = append(results, s) } } return results } func sprintAllowedCheckerNames(allowedNames map[string]bool) string { namesSlice := make([]string, 0, len(allowedNames)) for name := range allowedNames { namesSlice = append(namesSlice, name) } return sprintStrings(namesSlice) } func sprintStrings(ss []string) string { sort.Strings(ss) return fmt.Sprint(ss) } func debugChecksListf(checks []string, format string, args ...any) { if !isGoCriticDebug { return } goCriticDebugf("%s checks (%d): %s", fmt.Sprintf(format, args...), len(checks), sprintStrings(checks)) } func stringsSliceToSet(ss []string) map[string]bool { ret := make(map[string]bool, len(ss)) for _, s := range ss { ret[s] = true } return ret }