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  }