2023-08-09 12:09:20 +02:00

656 lines
17 KiB
Go

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
}
if settings != nil && config.IsGreaterThanOrEqualGo121(settings.Go) {
var enabledChecks []string
for _, check := range settings.EnabledChecks {
if check == "ruleguard" {
logger.Warnf("%s: check %q is disabled for go1.21 https://github.com/golangci/golangci-lint/issues/3933", goCriticName, "ruleguard")
continue
}
enabledChecks = append(enabledChecks, check)
}
settings.EnabledChecks = enabledChecks
}
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
}