Skip to content

Commit

Permalink
forbidigo: better support for configuring complex rules
Browse files Browse the repository at this point in the history
forbidigo 1.4.0 introduced a new configuration mechanism with multiple fields
per rule. It still supports a single string, but allowing structs as
alternative to such strings makes the configuration easier to read and write.

A new global flag is needed to enable the more advanced analysis.
  • Loading branch information
pohly committed Feb 27, 2023
1 parent f0dbc75 commit 8a2f7e7
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 21 deletions.
25 changes: 21 additions & 4 deletions .golangci.reference.yml
Expand Up @@ -361,14 +361,31 @@ linters-settings:
# Forbid the following identifiers (list of regexp).
# Default: ["^(fmt\\.Print(|f|ln)|print|println)$"]
forbid:
# Builtin function:
- ^print.*$
- 'fmt\.Print.*'
# Optionally put comments at the end of the regex, surrounded by `(# )?`
# Escape any special characters.
# Optional message that gets included in error reports.
- p: ^fmt\.Print.*$
msg: Do not commit print statements.
# Alternatively, put messages at the end of the regex, surrounded by `(# )?`
# Escape any special characters. Those messages get included in error reports.
- 'fmt\.Print.*(# Do not commit print statements\.)?'
# Forbid spew Dump, whether it is called as function or method.
# Depends on analyze-types below.
- ^spew\.(ConfigState\.)?Dump$
# The package name might be ambiguous.
# The full import path can be used as additional criteria.
# Depends on analyze-types below.
- p: ^v1.Dump$
pkg: ^example.com/pkg/api/v1$
# Exclude godoc examples from forbidigo checks.
# Default: true
exclude_godoc_examples: false
exclude-godoc-examples: false
# Instead of matching the literal source code, use type information to
# replace expressions with strings that contain the package name and (for
# methods and fields) the type name. This makes it possible to handle
# import renaming and forbid struct fields and methods.
# Default: false
analyze-types: true

funlen:
# Checks the number of lines in a function.
Expand Down
7 changes: 3 additions & 4 deletions go.mod
Expand Up @@ -14,7 +14,7 @@ require (
github.com/OpenPeeDeeP/depguard v1.1.1
github.com/alexkohler/prealloc v1.0.0
github.com/alingse/asasalint v0.0.11
github.com/ashanbrown/forbidigo v1.4.0
github.com/ashanbrown/forbidigo v1.5.1
github.com/ashanbrown/makezero v1.1.1
github.com/bkielbasa/cyclop v1.2.0
github.com/blizzy78/varnamelen v0.8.0
Expand Down Expand Up @@ -148,12 +148,11 @@ require (
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/mapstructure v1.5.0
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
Expand Down Expand Up @@ -186,6 +185,6 @@ require (
golang.org/x/text v0.6.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v2 v2.4.0
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
)
4 changes: 2 additions & 2 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions pkg/config/linters_settings.go
@@ -1,8 +1,11 @@
package config

import (
"encoding"
"errors"
"runtime"

"gopkg.in/yaml.v2"
)

var defaultLintersSettings = LintersSettings{
Expand Down Expand Up @@ -308,10 +311,46 @@ type ExhaustructSettings struct {
}

type ForbidigoSettings struct {
Forbid []string `mapstructure:"forbid"`
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
Forbid []ForbidigoPattern `mapstructure:"forbid"`
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
AnalyzeTypes bool `mapstructure:"analyze-types"`
}

// ForbidigoPattern corresponds to forbidigo.pattern and adds
// mapstructure support. The YAML field names must match what
// forbidigo expects.
type ForbidigoPattern struct {
// patternString gets populated when the config contains a string as
// entry in ForbidigoSettings.Forbid[] because ForbidigoPattern
// implements encoding.TextUnmarshaler and the reader uses the
// mapstructure.TextUnmarshallerHookFunc as decoder hook.
//
// If the entry is a map, then the other fields are set as usual
// by mapstructure.
patternString string

Pattern string `yaml:"p" mapstructure:"p"`
Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"`
Msg string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"`
}

func (p *ForbidigoPattern) UnmarshalText(text []byte) error {
// Validation happens when instantiating forbidigo.
p.patternString = string(text)
return nil
}

// String is intentionally not called MarshalText because that led to infinite
// recursion when yaml.Marshal called MarshalText.
func (p *ForbidigoPattern) String() ([]byte, error) {
if p.patternString != "" {
return []byte(p.patternString), nil
}
return yaml.Marshal(p)
}

var _ encoding.TextUnmarshaler = &ForbidigoPattern{}

type FunlenSettings struct {
Lines int
Statements int
Expand Down
10 changes: 9 additions & 1 deletion pkg/config/reader.go
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/mitchellh/go-homedir"
"github.com/mitchellh/mapstructure"
"github.com/spf13/viper"

"github.com/golangci/golangci-lint/pkg/exitcodes"
Expand Down Expand Up @@ -83,7 +84,14 @@ func (r *FileReader) parseConfig() error {
}
r.cfg.cfgDir = usedConfigDir

if err := viper.Unmarshal(r.cfg); err != nil {
if err := viper.Unmarshal(r.cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),

// Needed for forbidigo.
mapstructure.TextUnmarshallerHookFunc(),
))); err != nil {
return fmt.Errorf("can't unmarshal config by viper: %s", err)
}

Expand Down
35 changes: 31 additions & 4 deletions pkg/golinters/forbidigo.go
Expand Up @@ -10,12 +10,12 @@ import (
"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 forbidigoName = "forbidigo"

//nolint:dupl
func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter {
var mu sync.Mutex
var resIssues []goanalysis.Issue
Expand All @@ -40,14 +40,20 @@ func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter {
},
}

loadMode := goanalysis.LoadModeSyntax
if settings != nil && settings.AnalyzeTypes {
// Need more information for the analysis.
loadMode = goanalysis.LoadModeTypesInfo
}

return goanalysis.NewLinter(
forbidigoName,
"Forbids identifiers",
[]*analysis.Analyzer{analyzer},
nil,
).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
return resIssues
}).WithLoadMode(goanalysis.LoadModeSyntax)
}).WithLoadMode(loadMode)
}

func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]goanalysis.Issue, error) {
Expand All @@ -56,15 +62,36 @@ func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]go
// disable "//permit" directives so only "//nolint" directives matters within golangci-lint
forbidigo.OptionIgnorePermitDirectives(true),
}
if settings != nil && settings.AnalyzeTypes {
options = append(options, forbidigo.OptionAnalyzeTypes(true))
}

forbid, err := forbidigo.NewLinter(settings.Forbid, options...)
// Convert patterns back to strings because that is what NewLinter
// accepts.
var patterns []string
for _, pattern := range settings.Forbid {
buffer, err := pattern.String()
if err != nil {
return nil, err
}
patterns = append(patterns, string(buffer))
}

forbid, err := forbidigo.NewLinter(patterns, options...)
if err != nil {
return nil, fmt.Errorf("failed to create linter %q: %w", forbidigoName, err)
}

var issues []goanalysis.Issue
for _, file := range pass.Files {
hints, err := forbid.RunWithConfig(forbidigo.RunConfig{Fset: pass.Fset}, file)
runConfig := forbidigo.RunConfig{
Fset: pass.Fset,
DebugLog: logutils.Debug(logutils.DebugKeyForbidigo),
}
if settings != nil && settings.AnalyzeTypes {
runConfig.TypesInfo = pass.TypesInfo
}
hints, err := forbid.RunWithConfig(runConfig, file)
if err != nil {
return nil, fmt.Errorf("forbidigo linter failed on file %q: %w", file.Name.String(), err)
}
Expand Down
15 changes: 11 additions & 4 deletions pkg/lint/lintersdb/manager.go
Expand Up @@ -414,10 +414,17 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
WithLoadForGoAnalysis().
WithURL("https://github.com/kyoh86/exportloopref"),

linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
WithSince("v1.34.0").
WithPresets(linter.PresetStyle).
WithURL("https://github.com/ashanbrown/forbidigo"),
func() *linter.Config {
l := linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
WithSince("v1.34.0").
WithPresets(linter.PresetStyle).
WithURL("https://github.com/ashanbrown/forbidigo")
if forbidigoCfg != nil && forbidigoCfg.AnalyzeTypes {
// Need more information for the analysis.
l = l.WithLoadForGoAnalysis()
}
return l
}(),

linter.NewConfig(golinters.NewForceTypeAssert()).
WithSince("v1.38.0").
Expand Down
1 change: 1 addition & 0 deletions pkg/logutils/logutils.go
Expand Up @@ -22,6 +22,7 @@ const (
DebugKeyExcludeRules = "exclude_rules"
DebugKeyExec = "exec"
DebugKeyFilenameUnadjuster = "filename_unadjuster"
DebugKeyForbidigo = "forbidigo"
DebugKeyGoEnv = "goenv"
DebugKeyLinter = "linter"
DebugKeyLintersContext = "linters_context"
Expand Down
8 changes: 8 additions & 0 deletions test/testdata/configs/forbidigo_struct.yml
@@ -0,0 +1,8 @@
linters-settings:
forbidigo:
analyze-types: true
forbid:
- p: fmt\.Print.*
pkg: ^fmt$
- p: time.Sleep
msg: no sleeping!
2 changes: 2 additions & 0 deletions test/testdata/forbidigo_example.go
Expand Up @@ -4,10 +4,12 @@ package testdata

import (
"fmt"
fmt2 "fmt"
"time"
)

func Forbidigo() {
fmt.Printf("too noisy!!!") // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`"
fmt2.Printf("too noisy!!!") // Not detected because analyze-types is false by default for backward compatbility.
time.Sleep(time.Nanosecond) // want "no sleeping!"
}
13 changes: 13 additions & 0 deletions test/testdata/forbidigo_struct_config.go
@@ -0,0 +1,13 @@
//golangcitest:args -Eforbidigo
//golangcitest:config_path testdata/configs/forbidigo_struct.yml
package testdata

import (
fmt2 "fmt"
"time"
)

func Forbidigo() {
fmt2.Printf("too noisy!!!") // want "use of `fmt2\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`"
time.Sleep(time.Nanosecond) // want "no sleeping!"
}

0 comments on commit 8a2f7e7

Please sign in to comment.