gocritic: support of enable-all and disable-all options (#4335)

Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
This commit is contained in:
Anton Telyshev 2024-02-19 16:50:25 +03:00 committed by GitHub
parent b07bd18cfd
commit c65868c105
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 824 additions and 429 deletions

View file

@ -529,22 +529,32 @@ linters-settings:
ignore-strings: 'foo.+'
gocritic:
# Which checks should be enabled; can't be combined with 'disabled-checks'.
# See https://go-critic.github.io/overview#checks-overview.
# To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run`.
# By default, list of stable checks is used.
# Disable all checks.
# Default: false
disable-all: true
# Which checks should be enabled in addition to default checks; can't be combined with 'disabled-checks'.
# By default, list of stable checks is used (https://go-critic.github.io/overview#checks-overview):
# appendAssign, argOrder, assignOp, badCall, badCond, captLocal, caseOrder, codegenComment, commentFormatting,
# defaultCaseOrder, deprecatedComment, dupArg, dupBranchBody, dupCase, dupSubExpr, elseif, exitAfterDefer,
# flagDeref, flagName, ifElseChain, mapKey, newDeref, offBy1, regexpMust, singleCaseSwitch, sloppyLen,
# sloppyTypeAssert, switchTrue, typeSwitchVar, underef, unlambda, unslice, valSwap, wrapperFunc
# To see which checks are enabled run `GL_DEBUG=gocritic golangci-lint run --enable=gocritic`.
enabled-checks:
- nestingReduce
- unnamedResult
- ruleguard
- truncateCmp
# Enable all checks.
# Default: false
enable-all: true
# Which checks should be disabled; can't be combined with 'enabled-checks'.
# Default: []
disabled-checks:
- regexpMust
# Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks.
# Enable multiple checks by tags in addition to default checks.
# Run `GL_DEBUG=gocritic golangci-lint run --enable=gocritic` to see all tags and checks.
# See https://github.com/go-critic/go-critic#usage -> section "Tags".
# Default: []
enabled-tags:

View file

@ -460,7 +460,9 @@ type GoConstSettings struct {
type GoCriticSettings struct {
Go string `mapstructure:"-"`
DisableAll bool `mapstructure:"disable-all"`
EnabledChecks []string `mapstructure:"enabled-checks"`
EnableAll bool `mapstructure:"enable-all"`
DisabledChecks []string `mapstructure:"disabled-checks"`
EnabledTags []string `mapstructure:"enabled-tags"`
DisabledTags []string `mapstructure:"disabled-tags"`

View file

@ -15,6 +15,7 @@ import (
"github.com/go-critic/go-critic/checkers"
gocriticlinter "github.com/go-critic/go-critic/linter"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/tools/go/analysis"
"github.com/golangci/golangci-lint/pkg/config"
@ -36,8 +37,8 @@ func NewGoCritic(settings *config.GoCriticSettings, cfg *config.Config) *goanaly
var resIssues []goanalysis.Issue
wrapper := &goCriticWrapper{
cfg: cfg,
sizes: types.SizesFor("gc", runtime.GOARCH),
getConfigDir: cfg.GetConfigDir, // Config directory is filled after calling this constructor.
sizes: types.SizesFor("gc", runtime.GOARCH),
}
analyzer := &analysis.Analyzer{
@ -74,12 +75,13 @@ Dynamic rules are written declaratively with AST patterns, filters, report messa
}).
WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
return resIssues
}).WithLoadMode(goanalysis.LoadModeTypesInfo)
}).
WithLoadMode(goanalysis.LoadModeTypesInfo)
}
type goCriticWrapper struct {
settingsWrapper *goCriticSettingsWrapper
cfg *config.Config
getConfigDir func() string
sizes types.Sizes
once sync.Once
}
@ -92,15 +94,15 @@ func (w *goCriticWrapper) init(settings *config.GoCriticSettings, logger logutil
w.once.Do(func() {
err := checkers.InitEmbeddedRules()
if err != nil {
logger.Fatalf("%s: %v: setting an explicit GOROOT can fix this problem.", goCriticName, err)
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 {
settingsWrapper.InferEnabledChecks()
// Validate must be after InferEnabledChecks, not before.
// Because it uses gathered information about tags set and finally enabled checks.
if err := settingsWrapper.Validate(); err != nil {
logger.Fatalf("%s: invalid settings: %s", goCriticName, err)
}
@ -109,7 +111,7 @@ func (w *goCriticWrapper) init(settings *config.GoCriticSettings, logger logutil
func (w *goCriticWrapper) run(pass *analysis.Pass) ([]goanalysis.Issue, error) {
if w.settingsWrapper == nil {
return nil, fmt.Errorf("the settings wrapper is nil")
return nil, errors.New("the settings wrapper is nil")
}
linterCtx := gocriticlinter.NewContext(pass.Fset, w.sizes)
@ -123,7 +125,7 @@ func (w *goCriticWrapper) run(pass *analysis.Pass) ([]goanalysis.Issue, error) {
linterCtx.SetPackageInfo(pass.TypesInfo, pass.Pkg)
pkgIssues := runGocriticOnPackage(linterCtx, enabledCheckers, pass.Files)
pkgIssues := runGoCriticOnPackage(linterCtx, enabledCheckers, pass.Files)
issues := make([]goanalysis.Issue, 0, len(pkgIssues))
for i := range pkgIssues {
@ -134,15 +136,15 @@ func (w *goCriticWrapper) run(pass *analysis.Pass) ([]goanalysis.Issue, error) {
}
func (w *goCriticWrapper) buildEnabledCheckers(linterCtx *gocriticlinter.Context) ([]*gocriticlinter.Checker, error) {
allParams := w.settingsWrapper.getLowerCasedParams()
allLowerCasedParams := w.settingsWrapper.GetLowerCasedParams()
var enabledCheckers []*gocriticlinter.Checker
for _, info := range gocriticlinter.GetCheckersInfo() {
if !w.settingsWrapper.isCheckEnabled(info.Name) {
if !w.settingsWrapper.IsCheckEnabled(info.Name) {
continue
}
if err := w.configureCheckerInfo(info, allParams); err != nil {
if err := w.configureCheckerInfo(info, allLowerCasedParams); err != nil {
return nil, err
}
@ -156,20 +158,73 @@ func (w *goCriticWrapper) buildEnabledCheckers(linterCtx *gocriticlinter.Context
return enabledCheckers, nil
}
func runGocriticOnPackage(linterCtx *gocriticlinter.Context, checks []*gocriticlinter.Checker,
files []*ast.File) []result.Issue {
func (w *goCriticWrapper) configureCheckerInfo(
info *gocriticlinter.CheckerInfo,
allLowerCasedParams map[string]config.GoCriticCheckSettings,
) error {
params := allLowerCasedParams[strings.ToLower(info.Name)]
if params == nil { // no config for this checker
return nil
}
// To lowercase info param keys here because golangci-lint's config parser lowercases all strings.
infoParams := normalizeMap(info.Params)
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)
}
supportedKeys := maps.Keys(info.Params)
sort.Strings(supportedKeys)
return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s",
info.Name, k, supportedKeys)
}
return nil
}
// 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.getConfigDir())
default:
return p
}
}
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)
issues := runGoCriticOnFile(linterCtx, f, checks)
res = append(res, issues...)
}
return res
}
func runGocriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []*gocriticlinter.Checker) []result.Issue {
func runGoCriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []*gocriticlinter.Checker) []result.Issue {
var res []result.Issue
for _, c := range checks {
@ -200,416 +255,323 @@ func runGocriticOnFile(linterCtx *gocriticlinter.Context, f *ast.File, checks []
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
}
type goCriticChecks[T any] map[string]T
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)
}
supportedKeys := maps.Keys(info.Params)
sort.Strings(supportedKeys)
return fmt.Errorf("checker %s config param %s doesn't exist, all existing: %s",
info.Name, k, supportedKeys)
}
return nil
func (m goCriticChecks[T]) has(name string) bool {
_, ok := m[name]
return ok
}
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
allCheckers []*gocriticlinter.CheckerInfo
inferredEnabledChecks map[string]bool
allChecks goCriticChecks[struct{}]
allChecksByTag goCriticChecks[[]string]
allTagsSorted []string
inferredEnabledChecks goCriticChecks[struct{}]
// *LowerCased fields are used for GoCriticSettings.SettingsPerCheck validation only.
allChecksLowerCased goCriticChecks[struct{}]
inferredEnabledChecksLowerCased goCriticChecks[struct{}]
}
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
allChecks := make(goCriticChecks[struct{}], len(allCheckers))
allChecksLowerCased := make(goCriticChecks[struct{}], len(allCheckers))
allChecksByTag := make(goCriticChecks[[]string])
for _, checker := range allCheckers {
allChecks[checker.Name] = struct{}{}
allChecksLowerCased[strings.ToLower(checker.Name)] = struct{}{}
for _, tag := range checker.Tags {
allChecksByTag[tag] = append(allChecksByTag[tag], checker.Name)
}
}
allTagsSorted := maps.Keys(allChecksByTag)
sort.Strings(allTagsSorted)
return &goCriticSettingsWrapper{
GoCriticSettings: settings,
logger: logger,
allCheckers: allCheckers,
allCheckerMap: allCheckerMap,
inferredEnabledChecks: map[string]bool{},
GoCriticSettings: settings,
logger: logger,
allCheckers: allCheckers,
allChecks: allChecks,
allChecksLowerCased: allChecksLowerCased,
allChecksByTag: allChecksByTag,
allTagsSorted: allTagsSorted,
inferredEnabledChecks: make(goCriticChecks[struct{}]),
inferredEnabledChecksLowerCased: make(goCriticChecks[struct{}]),
}
}
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) IsCheckEnabled(name string) bool {
return s.inferredEnabledChecks.has(name)
}
func (s *goCriticSettingsWrapper) checkerTagsDebugf() {
if !isGoCriticDebug {
return
}
tagToCheckers := s.buildTagToCheckersMap()
allTags := maps.Keys(tagToCheckers)
sort.Strings(allTags)
goCriticDebugf("All gocritic existing tags and checks:")
for _, tag := range allTags {
debugChecksListf(tagToCheckers[tag], " tag %q", tag)
}
func (s *goCriticSettingsWrapper) GetLowerCasedParams() map[string]config.GoCriticCheckSettings {
return normalizeMap(s.SettingsPerCheck)
}
func (s *goCriticSettingsWrapper) disabledCheckersDebugf() {
if !isGoCriticDebug {
return
}
// InferEnabledChecks tries to be consistent with (lintersdb.EnabledSet).build.
func (s *goCriticSettingsWrapper) InferEnabledChecks() {
s.debugChecksInitialState()
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()
enabledByDefaultChecks, disabledByDefaultChecks := s.buildEnabledAndDisabledByDefaultChecks()
debugChecksListf(enabledByDefaultChecks, "Enabled by default")
disabledByDefaultChecks := s.getDefaultDisabledCheckersNames()
debugChecksListf(disabledByDefaultChecks, "Disabled by default")
enabledChecks := make([]string, 0, len(s.EnabledTags)+len(enabledByDefaultChecks))
enabledChecks := make(goCriticChecks[struct{}])
// EnabledTags
if len(s.EnabledTags) != 0 {
tagToCheckers := s.buildTagToCheckersMap()
for _, tag := range s.EnabledTags {
enabledChecks = append(enabledChecks, tagToCheckers[tag]...)
if s.EnableAll {
enabledChecks = make(goCriticChecks[struct{}], len(s.allCheckers))
for _, info := range s.allCheckers {
enabledChecks[info.Name] = struct{}{}
}
} else if !s.DisableAll {
// enable-all/disable-all revokes the default settings.
enabledChecks = make(goCriticChecks[struct{}], len(enabledByDefaultChecks))
for _, check := range enabledByDefaultChecks {
enabledChecks[check] = struct{}{}
}
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...)
if len(s.EnabledTags) != 0 {
enabledFromTags := s.expandTagsToChecks(s.EnabledTags)
debugChecksListf(enabledFromTags, "Enabled by config tags %s", sprintSortedStrings(s.EnabledTags))
for _, check := range enabledFromTags {
enabledChecks[check] = struct{}{}
}
}
// 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)
for _, check := range s.EnabledChecks {
if enabledChecks.has(check) {
s.logger.Warnf("%s: no need to enable check %q: it's already enabled", goCriticName, check)
continue
}
enabledChecks = append(enabledChecks, enabledCheck)
enabledChecks[check] = struct{}{}
}
}
if len(s.DisabledTags) != 0 {
disabledFromTags := s.expandTagsToChecks(s.DisabledTags)
debugChecksListf(disabledFromTags, "Disabled by config tags %s", sprintSortedStrings(s.DisabledTags))
for _, check := range disabledFromTags {
delete(enabledChecks, check)
}
}
// 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)
for _, check := range s.DisabledChecks {
if !enabledChecks.has(check) {
s.logger.Warnf("%s: no need to disable check %q: it's already disabled", goCriticName, check)
continue
}
delete(enabledChecksSet, disabledCheck)
}
enabledChecks = nil
for enabledCheck := range enabledChecksSet {
enabledChecks = append(enabledChecks, enabledCheck)
delete(enabledChecks, check)
}
}
s.inferredEnabledChecks = map[string]bool{}
for _, check := range enabledChecks {
s.inferredEnabledChecks[strings.ToLower(check)] = true
s.inferredEnabledChecks = enabledChecks
s.inferredEnabledChecksLowerCased = normalizeMap(s.inferredEnabledChecks)
s.debugChecksFinalState()
}
func (s *goCriticSettingsWrapper) buildEnabledAndDisabledByDefaultChecks() (enabled, disabled []string) {
for _, info := range s.allCheckers {
if enabledByDef := isEnabledByDefaultGoCriticChecker(info); enabledByDef {
enabled = append(enabled, info.Name)
} else {
disabled = append(disabled, info.Name)
}
}
return enabled, disabled
}
func (s *goCriticSettingsWrapper) expandTagsToChecks(tags []string) []string {
var checks []string
for _, tag := range tags {
checks = append(checks, s.allChecksByTag[tag]...)
}
return checks
}
func (s *goCriticSettingsWrapper) debugChecksInitialState() {
if !isGoCriticDebug {
return
}
goCriticDebugf("All gocritic existing tags and checks:")
for _, tag := range s.allTagsSorted {
debugChecksListf(s.allChecksByTag[tag], " tag %q", tag)
}
}
func (s *goCriticSettingsWrapper) debugChecksFinalState() {
if !isGoCriticDebug {
return
}
var enabledChecks []string
var disabledChecks []string
for _, checker := range s.allCheckers {
name := checker.Name
if s.inferredEnabledChecks.has(name) {
enabledChecks = append(enabledChecks, name)
} else {
disabledChecks = append(disabledChecks, name)
}
}
debugChecksListf(enabledChecks, "Final used")
s.disabledCheckersDebugf()
if len(disabledChecks) == 0 {
goCriticDebugf("All checks are enabled")
} else {
debugChecksListf(disabledChecks, "Final not used")
}
}
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")
// Validate tries to be consistent with (lintersdb.Validator).validateEnabledDisabledLintersConfig.
func (s *goCriticSettingsWrapper) Validate() error {
for _, v := range []func() error{
s.validateOptionsCombinations,
s.validateCheckerTags,
s.validateCheckerNames,
s.validateDisabledAndEnabledAtOneMoment,
s.validateAtLeastOneCheckerEnabled,
} {
if err := v(); err != nil {
return err
}
} else {
if err := validateStringsUniq(s.EnabledTags); err != nil {
return fmt.Errorf("validate enabled tags: %w", err)
}
return nil
}
func (s *goCriticSettingsWrapper) validateOptionsCombinations() error {
if s.EnableAll {
if s.DisableAll {
return errors.New("enable-all and disable-all options must not be combined")
}
tagToCheckers := s.buildTagToCheckersMap()
if len(s.EnabledTags) != 0 {
return errors.New("enable-all and enabled-tags options must not be combined")
}
for _, tag := range s.EnabledTags {
if _, ok := tagToCheckers[tag]; !ok {
return fmt.Errorf("gocritic [enabled]tag %q doesn't exist", tag)
}
if len(s.EnabledChecks) != 0 {
return errors.New("enable-all and enabled-checks options must not be combined")
}
}
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 s.DisableAll {
if len(s.DisabledTags) != 0 {
return errors.New("disable-all and disabled-tags options must not be combined")
}
}
if err := validateStringsUniq(s.EnabledChecks); err != nil {
return fmt.Errorf("validate enabled checks: %w", err)
}
if len(s.DisabledChecks) != 0 {
return errors.New("disable-all and disabled-checks options must not be combined")
}
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)
if len(s.EnabledTags) == 0 && len(s.EnabledChecks) == 0 {
return errors.New("all checks were disabled, but no one check was enabled: at least one must be enabled")
}
}
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)
func (s *goCriticSettingsWrapper) validateCheckerTags() error {
for _, tag := range s.EnabledTags {
if !s.allChecksByTag.has(tag) {
return fmt.Errorf("enabled tag %q doesn't exist, see %s's documentation", tag, goCriticName)
}
}
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)
for _, tag := range s.DisabledTags {
if !s.allChecksByTag.has(tag) {
return fmt.Errorf("disabled tag %q doesn't exist, see %s's documentation", tag, goCriticName)
}
}
return disabled
return nil
}
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))
if !s.allChecks.has(name) {
return fmt.Errorf("enabled check %q doesn't exist, see %s's documentation", name, goCriticName)
}
}
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))
if !s.allChecks.has(name) {
return fmt.Errorf("disabled check %q doesn't exist, see %s documentation", name, goCriticName)
}
}
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))
for name := range s.SettingsPerCheck {
lcName := strings.ToLower(name)
if !s.allChecksLowerCased.has(lcName) {
return fmt.Errorf("invalid check settings: check %q doesn't exist, see %s documentation", name, goCriticName)
}
if !s.isCheckEnabled(checkName) {
s.logger.Warnf("%s: settings were provided for not enabled check %q", goCriticName, checkName)
if !s.inferredEnabledChecksLowerCased.has(lcName) {
s.logger.Warnf("%s: settings were provided for disabled check %q", goCriticName, name)
}
}
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
func (s *goCriticSettingsWrapper) validateDisabledAndEnabledAtOneMoment() error {
for _, tag := range s.DisabledTags {
if slices.Contains(s.EnabledTags, tag) {
return fmt.Errorf("tag %q disabled and enabled at one moment", tag)
}
}
for _, check := range s.DisabledChecks {
if slices.Contains(s.EnabledChecks, check) {
return fmt.Errorf("check %q disabled and enabled at one moment", check)
}
}
return nil
}
func (s *goCriticSettingsWrapper) validateAtLeastOneCheckerEnabled() error {
if len(s.inferredEnabledChecks) == 0 {
return errors.New("eventually all checks were disabled: at least one must be enabled")
}
return nil
}
func normalizeMap[ValueT any](in map[string]ValueT) map[string]ValueT {
ret := make(map[string]ValueT, len(in))
for k, v := range in {
ret[strings.ToLower(k)] = v
}
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 := maps.Keys(allowedNames)
return sprintStrings(namesSlice)
}
func sprintStrings(ss []string) string {
sort.Strings(ss)
return fmt.Sprint(ss)
func isEnabledByDefaultGoCriticChecker(info *gocriticlinter.CheckerInfo) bool {
// https://github.com/go-critic/go-critic/blob/5b67cfd487ae9fe058b4b19321901b3131810f65/cmd/gocritic/check.go#L342-L345
return !info.HasTag(gocriticlinter.ExperimentalTag) &&
!info.HasTag(gocriticlinter.OpinionatedTag) &&
!info.HasTag(gocriticlinter.PerformanceTag) &&
!info.HasTag(gocriticlinter.SecurityTag)
}
func debugChecksListf(checks []string, format string, args ...any) {
@ -617,14 +579,10 @@ func debugChecksListf(checks []string, format string, args ...any) {
return
}
goCriticDebugf("%s checks (%d): %s", fmt.Sprintf(format, args...), len(checks), sprintStrings(checks))
goCriticDebugf("%s checks (%d): %s", fmt.Sprintf(format, args...), len(checks), sprintSortedStrings(checks))
}
func stringsSliceToSet(ss []string) map[string]bool {
ret := make(map[string]bool, len(ss))
for _, s := range ss {
ret[s] = true
}
return ret
func sprintSortedStrings(v []string) string {
sort.Strings(slices.Clone(v))
return fmt.Sprint(v)
}

View file

@ -1,56 +1,456 @@
package golinters
import (
"log"
"strings"
"testing"
"github.com/go-critic/go-critic/checkers"
gocriticlinter "github.com/go-critic/go-critic/linter"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/logutils"
)
func Test_intersectStringSlice(t *testing.T) {
s1 := []string{"diagnostic", "experimental", "opinionated"}
s2 := []string{"opinionated", "experimental"}
// https://go-critic.com/overview.html
func Test_goCriticSettingsWrapper_InferEnabledChecks(t *testing.T) {
err := checkers.InitEmbeddedRules()
require.NoError(t, err)
s3 := intersectStringSlice(s1, s2)
allCheckersInfo := gocriticlinter.GetCheckersInfo()
assert.ElementsMatch(t, []string{"experimental", "opinionated"}, s3)
allChecksByTag := make(map[string][]string)
allChecks := make([]string, 0, len(allCheckersInfo))
for _, checker := range allCheckersInfo {
allChecks = append(allChecks, checker.Name)
for _, tag := range checker.Tags {
allChecksByTag[tag] = append(allChecksByTag[tag], checker.Name)
}
}
enabledByDefaultChecks := make([]string, 0, len(allCheckersInfo))
for _, info := range allCheckersInfo {
if isEnabledByDefaultGoCriticChecker(info) {
enabledByDefaultChecks = append(enabledByDefaultChecks, info.Name)
}
}
t.Logf("enabled by default checks:\n%s", strings.Join(enabledByDefaultChecks, "\n"))
insert := func(in []string, toInsert ...string) []string {
return append(slices.Clone(in), toInsert...)
}
remove := func(in []string, toRemove ...string) []string {
result := slices.Clone(in)
for _, v := range toRemove {
if i := slices.Index(result, v); i != -1 {
result = slices.Delete(result, i, i+1)
}
}
return result
}
uniq := func(in []string) []string {
result := slices.Clone(in)
slices.Sort(result)
return slices.Compact(result)
}
cases := []struct {
name string
sett *config.GoCriticSettings
expectedEnabledChecks []string
}{
{
name: "no configuration",
sett: &config.GoCriticSettings{},
expectedEnabledChecks: enabledByDefaultChecks,
},
{
name: "enable checks",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"assignOp", "badCall", "emptyDecl"},
},
expectedEnabledChecks: insert(enabledByDefaultChecks, "emptyDecl"),
},
{
name: "disable checks",
sett: &config.GoCriticSettings{
DisabledChecks: []string{"assignOp", "emptyDecl"},
},
expectedEnabledChecks: remove(enabledByDefaultChecks, "assignOp"),
},
{
name: "enable tags",
sett: &config.GoCriticSettings{
EnabledTags: []string{"style", "experimental"},
},
expectedEnabledChecks: uniq(insert(insert(
enabledByDefaultChecks,
allChecksByTag["style"]...),
allChecksByTag["experimental"]...)),
},
{
name: "disable tags",
sett: &config.GoCriticSettings{
DisabledTags: []string{"diagnostic"},
},
expectedEnabledChecks: remove(enabledByDefaultChecks, allChecksByTag["diagnostic"]...),
},
{
name: "enable checks disable checks",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"badCall", "badLock"},
DisabledChecks: []string{"assignOp", "badSorting"},
},
expectedEnabledChecks: insert(remove(enabledByDefaultChecks, "assignOp"), "badLock"),
},
{
name: "enable checks enable tags",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"badCall", "badLock", "hugeParam"},
EnabledTags: []string{"diagnostic"},
},
expectedEnabledChecks: uniq(insert(insert(enabledByDefaultChecks,
allChecksByTag["diagnostic"]...),
"hugeParam")),
},
{
name: "enable checks disable tags",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"badCall", "badLock", "boolExprSimplify", "hugeParam"},
DisabledTags: []string{"style", "diagnostic"},
},
expectedEnabledChecks: insert(remove(remove(enabledByDefaultChecks,
allChecksByTag["style"]...),
allChecksByTag["diagnostic"]...),
"hugeParam"),
},
{
name: "enable all checks via tags",
sett: &config.GoCriticSettings{
EnabledTags: []string{"diagnostic", "experimental", "opinionated", "performance", "style"},
},
expectedEnabledChecks: allChecks,
},
{
name: "disable checks enable tags",
sett: &config.GoCriticSettings{
DisabledChecks: []string{"assignOp", "badCall", "badLock", "hugeParam"},
EnabledTags: []string{"style", "diagnostic"},
},
expectedEnabledChecks: remove(uniq(insert(insert(enabledByDefaultChecks,
allChecksByTag["style"]...),
allChecksByTag["diagnostic"]...)),
"assignOp", "badCall", "badLock"),
},
{
name: "disable checks disable tags",
sett: &config.GoCriticSettings{
DisabledChecks: []string{"badCall", "badLock", "codegenComment", "hugeParam"},
DisabledTags: []string{"style"},
},
expectedEnabledChecks: remove(remove(enabledByDefaultChecks,
allChecksByTag["style"]...),
"badCall", "codegenComment"),
},
{
name: "enable tags disable tags",
sett: &config.GoCriticSettings{
EnabledTags: []string{"experimental"},
DisabledTags: []string{"style"},
},
expectedEnabledChecks: remove(uniq(insert(enabledByDefaultChecks,
allChecksByTag["experimental"]...)),
allChecksByTag["style"]...),
},
{
name: "enable checks disable checks enable tags",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"badCall", "badLock", "boolExprSimplify", "indexAlloc", "hugeParam"},
DisabledChecks: []string{"deprecatedComment", "typeSwitchVar"},
EnabledTags: []string{"experimental"},
},
expectedEnabledChecks: remove(uniq(insert(insert(enabledByDefaultChecks,
allChecksByTag["experimental"]...),
"indexAlloc", "hugeParam")),
"deprecatedComment", "typeSwitchVar"),
},
{
name: "enable checks disable checks enable tags disable tags",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"badCall", "badCond", "badLock", "indexAlloc", "hugeParam"},
DisabledChecks: []string{"deprecatedComment", "typeSwitchVar"},
EnabledTags: []string{"experimental"},
DisabledTags: []string{"performance"},
},
expectedEnabledChecks: remove(remove(uniq(insert(insert(enabledByDefaultChecks,
allChecksByTag["experimental"]...),
"badCond")),
allChecksByTag["performance"]...),
"deprecatedComment", "typeSwitchVar"),
},
{
name: "enable single tag only",
sett: &config.GoCriticSettings{
DisableAll: true,
EnabledTags: []string{"experimental"},
},
expectedEnabledChecks: allChecksByTag["experimental"],
},
{
name: "enable two tags only",
sett: &config.GoCriticSettings{
DisableAll: true,
EnabledTags: []string{"experimental", "performance"},
},
expectedEnabledChecks: uniq(insert(allChecksByTag["experimental"], allChecksByTag["performance"]...)),
},
{
name: "disable single tag only",
sett: &config.GoCriticSettings{
EnableAll: true,
DisabledTags: []string{"style"},
},
expectedEnabledChecks: remove(allChecks, allChecksByTag["style"]...),
},
{
name: "disable two tags only",
sett: &config.GoCriticSettings{
EnableAll: true,
DisabledTags: []string{"style", "diagnostic"},
},
expectedEnabledChecks: remove(remove(allChecks, allChecksByTag["style"]...), allChecksByTag["diagnostic"]...),
},
{
name: "enable some checks only",
sett: &config.GoCriticSettings{
DisableAll: true,
EnabledChecks: []string{"deferInLoop", "dupImport", "ifElseChain", "mapKey"},
},
expectedEnabledChecks: []string{"deferInLoop", "dupImport", "ifElseChain", "mapKey"},
},
{
name: "disable some checks only",
sett: &config.GoCriticSettings{
EnableAll: true,
DisabledChecks: []string{"deferInLoop", "dupImport", "ifElseChain", "mapKey"},
},
expectedEnabledChecks: remove(allChecks, "deferInLoop", "dupImport", "ifElseChain", "mapKey"),
},
{
name: "enable single tag and some checks from another tag only",
sett: &config.GoCriticSettings{
DisableAll: true,
EnabledTags: []string{"experimental"},
EnabledChecks: []string{"importShadow"},
},
expectedEnabledChecks: insert(allChecksByTag["experimental"], "importShadow"),
},
{
name: "disable single tag and some checks from another tag only",
sett: &config.GoCriticSettings{
EnableAll: true,
DisabledTags: []string{"experimental"},
DisabledChecks: []string{"importShadow"},
},
expectedEnabledChecks: remove(remove(allChecks, allChecksByTag["experimental"]...), "importShadow"),
},
}
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
lg := logutils.NewStderrLog("Test_goCriticSettingsWrapper_InferEnabledChecks")
wr := newGoCriticSettingsWrapper(tt.sett, lg)
wr.InferEnabledChecks()
assert.ElementsMatch(t, tt.expectedEnabledChecks, maps.Keys(wr.inferredEnabledChecks))
assert.NoError(t, wr.Validate())
})
}
}
func Test_filterByDisableTags(t *testing.T) {
disabledTags := []string{"experimental", "opinionated"}
enabledChecks := []string{"appendAssign", "sortSlice", "caseOrder", "dupImport"}
func Test_goCriticSettingsWrapper_Validate(t *testing.T) {
cases := []struct {
name string
sett *config.GoCriticSettings
expectedErr bool
}{
{
name: "combine enable-all and disable-all",
sett: &config.GoCriticSettings{
EnableAll: true,
DisableAll: true,
},
expectedErr: true,
},
{
name: "combine enable-all and enabled-tags",
sett: &config.GoCriticSettings{
EnableAll: true,
EnabledTags: []string{"experimental"},
},
expectedErr: true,
},
{
name: "combine enable-all and enabled-checks",
sett: &config.GoCriticSettings{
EnableAll: true,
EnabledChecks: []string{"dupImport"},
},
expectedErr: true,
},
{
name: "combine disable-all and disabled-tags",
sett: &config.GoCriticSettings{
DisableAll: true,
DisabledTags: []string{"style"},
},
expectedErr: true,
},
{
name: "combine disable-all and disable-checks",
sett: &config.GoCriticSettings{
DisableAll: true,
DisabledChecks: []string{"appendAssign"},
},
expectedErr: true,
},
{
name: "disable-all and no one check enabled",
sett: &config.GoCriticSettings{
DisableAll: true,
},
expectedErr: true,
},
{
name: "unknown enabled tag",
sett: &config.GoCriticSettings{
EnabledTags: []string{"diagnostic", "go-proverbs"},
},
expectedErr: true,
},
{
name: "unknown disabled tag",
sett: &config.GoCriticSettings{
DisabledTags: []string{"style", "go-proverbs"},
},
expectedErr: true,
},
{
name: "unknown enabled check",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"appendAssign", "noExitAfterDefer", "underef"},
},
expectedErr: true,
},
{
name: "unknown disabled check",
sett: &config.GoCriticSettings{
DisabledChecks: []string{"dupSubExpr", "noExitAfterDefer", "returnAfterHttpError"},
},
expectedErr: true,
},
{
name: "settings for unknown check",
sett: &config.GoCriticSettings{
SettingsPerCheck: map[string]config.GoCriticCheckSettings{
"captLocall": {"paramsOnly": false},
"unnamedResult": {"checkExported": true},
},
},
expectedErr: true,
},
{
name: "settings for disabled check",
sett: &config.GoCriticSettings{
DisabledChecks: []string{"elseif"},
SettingsPerCheck: map[string]config.GoCriticCheckSettings{
"elseif": {"skipBalanced": true},
},
},
expectedErr: false, // Just logging.
},
{
name: "settings by lower-cased checker name",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"tooManyResultsChecker"},
SettingsPerCheck: map[string]config.GoCriticCheckSettings{
"toomanyresultschecker": {"maxResults": 3},
"unnamedResult": {"checkExported": true},
},
},
expectedErr: false,
},
{
name: "enabled and disabled at one moment check",
sett: &config.GoCriticSettings{
EnabledChecks: []string{"appendAssign", "codegenComment", "underef"},
DisabledChecks: []string{"elseif", "underef"},
},
expectedErr: true,
},
{
name: "enabled and disabled at one moment tag",
sett: &config.GoCriticSettings{
EnabledTags: []string{"performance", "style"},
DisabledTags: []string{"style", "diagnostic"},
},
expectedErr: true,
},
{
name: "disable all checks via tags",
sett: &config.GoCriticSettings{
DisabledTags: []string{"diagnostic", "experimental", "opinionated", "performance", "style"},
},
expectedErr: true,
},
{
name: "enable-all and disable all checks via tags",
sett: &config.GoCriticSettings{
EnableAll: true,
DisabledTags: []string{"diagnostic", "experimental", "opinionated", "performance", "style"},
},
expectedErr: true,
},
{
name: "valid configuration",
sett: &config.GoCriticSettings{
EnabledTags: []string{"performance"},
DisabledChecks: []string{"dupImport", "ifElseChain", "octalLiteral", "whyNoLint"},
SettingsPerCheck: map[string]config.GoCriticCheckSettings{
"hugeParam": {"sizeThreshold": 100},
"rangeValCopy": {"skipTestFuncs": true},
},
},
expectedErr: false,
},
}
settingsWrapper := newGoCriticSettingsWrapper(nil, &tLog{})
for _, tt := range cases {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
filterEnabledChecks := settingsWrapper.filterByDisableTags(enabledChecks, disabledTags)
lg := logutils.NewStderrLog("Test_goCriticSettingsWrapper_Validate")
wr := newGoCriticSettingsWrapper(tt.sett, lg)
assert.ElementsMatch(t, filterEnabledChecks, []string{"appendAssign", "caseOrder"})
wr.InferEnabledChecks()
err := wr.Validate()
if tt.expectedErr {
if assert.Error(t, err) {
t.Log(err)
}
} else {
assert.NoError(t, err)
}
})
}
}
type tLog struct{}
func (l *tLog) Fatalf(format string, args ...any) {
log.Printf(format, args...)
}
func (l *tLog) Panicf(format string, args ...any) {
log.Printf(format, args...)
}
func (l *tLog) Errorf(format string, args ...any) {
log.Printf(format, args...)
}
func (l *tLog) Warnf(format string, args ...any) {
log.Printf(format, args...)
}
func (l *tLog) Infof(format string, args ...any) {
log.Printf(format, args...)
}
func (l *tLog) Child(_ string) logutils.Log { return nil }
func (l *tLog) SetLevel(_ logutils.LogLevel) {}

View file

@ -1 +1,14 @@
This directory contains ruleguard files that are used in functional tests.
This directory contains ruleguard files that are used in functional tests:
```bash
T=gocritic.go make test_linters
```
```bash
T=gocritic.go make test_fix
```
Helpful:
- https://go-critic.com/overview.html
- https://github.com/go-critic/go-critic/blob/master/checkers/rules/rules.go

View file

@ -1,23 +0,0 @@
//go:build ruleguard
package ruleguard
import "github.com/quasilyte/go-ruleguard/dsl"
// Suppose that we want to report the duplicated left and right operands of binary operations.
//
// But if the operand has some side effects, this rule can cause false positives:
// `f() && f()` can make sense (although it's not the best piece of code).
//
// This is where *filters* come to the rescue.
func DupSubExpr(m dsl.Matcher) {
// All filters are written as a Where() argument.
// In our case, we need to assert that $x is "pure".
// It can be achieved by checking the m["x"] member Pure field.
m.Match(`$x || $x`,
`$x && $x`,
`$x | $x`,
`$x & $x`).
Where(m["x"].Pure).
Report(`suspicious identical LHS and RHS`)
}

View file

@ -0,0 +1,12 @@
//go:build ruleguard
package ruleguard
import "github.com/quasilyte/go-ruleguard/dsl"
func preferWriteString(m dsl.Matcher) {
m.Match(`$w.Write([]byte($s))`).
Where(m["w"].Type.Implements("io.StringWriter")).
Suggest("$w.WriteString($s)").
Report(`$w.WriteString($s) should be preferred to the $$`)
}

View file

@ -6,7 +6,7 @@ import (
"github.com/quasilyte/go-ruleguard/dsl"
)
func RangeExprVal(m dsl.Matcher) {
func rangeExprCopy(m dsl.Matcher) {
m.Match(`for _, $_ := range $x { $*_ }`, `for _, $_ = range $x { $*_ }`).
Where(m["x"].Addressable && m["x"].Type.Size >= 512).
Report(`$x copy can be avoided with &$x`).

View file

@ -4,7 +4,7 @@ package ruleguard
import "github.com/quasilyte/go-ruleguard/dsl"
func StringsSimplify(m dsl.Matcher) {
func stringsSimplify(m dsl.Matcher) {
// Some issues have simple fixes that can be expressed as
// a replacement pattern. Rules can use Suggest() function
// to add a quickfix action for such issues.

View file

@ -4,5 +4,4 @@ linters-settings:
- ruleguard
settings:
ruleguard:
rules: 'ruleguard/rangeExprCopy.go,ruleguard/strings_simplify.go'
rules: 'ruleguard/rangeExprCopy.go,ruleguard/stringsSimplify.go'

View file

@ -1,20 +1,20 @@
linters-settings:
gocritic:
disabled-checks:
- appendAssign
- switchTrue
enabled-checks:
- rangeValCopy
- flagDeref
- wrapperFunc
- hugeParam
- ruleguard
settings:
rangeValCopy:
sizeThreshold: 2
hugeParam:
sizeThreshold: 24
ruleguard:
debug: dupSubExpr
failOn: dsl,import
# comma-separated paths to ruleguard files.
# Comma-separated paths to ruleguard files.
# The ${configDir} is substituted by the directory containing the golangci-lint config file.
# Note about the directory structure for functional tests:
# The ruleguard files used in functional tests cannot be under the 'testdata' directory.
# This is because they import the 'github.com/quasilyte/go-ruleguard/dsl' package,
# which needs to be added to go.mod. The testdata directory is ignored by go mod.
rules: '${configDir}/../../ruleguard/strings_simplify.go,${configDir}/../../ruleguard/dup.go'
rules: '${configDir}/../../ruleguard/preferWriteString.go,${configDir}/../../ruleguard/stringsSimplify.go'

View file

@ -4,6 +4,7 @@ package testdata
import (
"flag"
"io"
"log"
"strings"
)
@ -11,38 +12,61 @@ import (
var _ = *flag.Bool("global1", false, "") // want `flagDeref: immediate deref in \*flag.Bool\(.global1., false, ..\) is most likely an error; consider using flag\.BoolVar`
type size1 struct {
a bool
a [12]bool
}
type size2 struct {
size1
b bool
b [12]bool
}
func gocriticRangeValCopySize1(ss []size1) {
for _, s := range ss {
log.Print(s)
func gocriticAppendAssign() {
var positives, negatives []int
positives = append(negatives, 1)
negatives = append(negatives, -1)
log.Print(positives, negatives)
}
func gocriticDupSubExpr(x bool) {
if x && x { // want "dupSubExpr: suspicious identical LHS and RHS.*"
log.Print("x is true")
}
}
func gocriticRangeValCopySize2(ss []size2) {
for _, s := range ss { // want "rangeValCopy: each iteration copies 2 bytes.*"
log.Print(s)
func gocriticHugeParamSize1(ss size1) {
log.Print(ss)
}
func gocriticHugeParamSize2(ss size2) { // want "hugeParam: ss is heavy \\(24 bytes\\); consider passing it by pointer"
log.Print(ss)
}
func gocriticHugeParamSize2Ptr(ss *size2) {
log.Print(*ss)
}
func gocriticSwitchTrue() {
switch true {
case false:
log.Print("false")
default:
log.Print("default")
}
}
func goCriticPreferStringWriter(w interface {
io.Writer
io.StringWriter
}) {
w.Write([]byte("test")) // want "ruleguard: w\\.WriteString\\(\"test\"\\) should be preferred.*"
}
func gocriticStringSimplify() {
s := "Most of the time, travellers worry about their luggage."
s = strings.Replace(s, ",", "", -1) // want "ruleguard: this Replace call can be simplified.*"
log.Print(s)
}
func gocriticDup(x bool) {
if x && x { // want "ruleguard: suspicious identical LHS and RHS.*"
log.Print("x is true")
}
}
func gocriticRuleWrapperFunc() {
strings.Replace("abcabc", "a", "d", -1) // want "ruleguard: this Replace call can be simplified.*"
}