Skip to content

Commit afd0ba5

Browse files
authored
forbidigo: better support for configuring complex rules (#3612)
1 parent 8fde463 commit afd0ba5

File tree

10 files changed

+125
-10
lines changed

10 files changed

+125
-10
lines changed

.golangci.reference.yml

+21-4
Original file line numberDiff line numberDiff line change
@@ -351,14 +351,31 @@ linters-settings:
351351
# Forbid the following identifiers (list of regexp).
352352
# Default: ["^(fmt\\.Print(|f|ln)|print|println)$"]
353353
forbid:
354+
# Builtin function:
354355
- ^print.*$
355-
- 'fmt\.Print.*'
356-
# Optionally put comments at the end of the regex, surrounded by `(# )?`
357-
# Escape any special characters.
356+
# Optional message that gets included in error reports.
357+
- p: ^fmt\.Print.*$
358+
msg: Do not commit print statements.
359+
# Alternatively, put messages at the end of the regex, surrounded by `(# )?`
360+
# Escape any special characters. Those messages get included in error reports.
358361
- 'fmt\.Print.*(# Do not commit print statements\.)?'
362+
# Forbid spew Dump, whether it is called as function or method.
363+
# Depends on analyze-types below.
364+
- ^spew\.(ConfigState\.)?Dump$
365+
# The package name might be ambiguous.
366+
# The full import path can be used as additional criteria.
367+
# Depends on analyze-types below.
368+
- p: ^v1.Dump$
369+
pkg: ^example.com/pkg/api/v1$
359370
# Exclude godoc examples from forbidigo checks.
360371
# Default: true
361-
exclude_godoc_examples: false
372+
exclude-godoc-examples: false
373+
# Instead of matching the literal source code,
374+
# use type information to replace expressions with strings that contain the package name
375+
# and (for methods and fields) the type name.
376+
# This makes it possible to handle import renaming and forbid struct fields and methods.
377+
# Default: false
378+
analyze-types: true
362379

363380
funlen:
364381
# Checks the number of lines in a function.

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ require (
7171
github.com/mgechev/revive v1.3.2
7272
github.com/mitchellh/go-homedir v1.1.0
7373
github.com/mitchellh/go-ps v1.0.0
74+
github.com/mitchellh/mapstructure v1.5.0
7475
github.com/moricho/tparallel v0.3.1
7576
github.com/nakabonne/nestif v0.3.1
7677
github.com/nishanths/exhaustive v0.10.0
@@ -153,7 +154,6 @@ require (
153154
github.com/mattn/go-isatty v0.0.17 // indirect
154155
github.com/mattn/go-runewidth v0.0.9 // indirect
155156
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
156-
github.com/mitchellh/mapstructure v1.5.0 // indirect
157157
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
158158
github.com/olekukonko/tablewriter v0.0.5 // indirect
159159
github.com/pelletier/go-toml v1.9.5 // indirect

pkg/config/linters_settings.go

+42-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package config
22

33
import (
4+
"encoding"
45
"errors"
56
"runtime"
7+
8+
"gopkg.in/yaml.v3"
69
)
710

811
var defaultLintersSettings = LintersSettings{
@@ -330,8 +333,45 @@ type ExhaustructSettings struct {
330333
}
331334

332335
type ForbidigoSettings struct {
333-
Forbid []string `mapstructure:"forbid"`
334-
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
336+
Forbid []ForbidigoPattern `mapstructure:"forbid"`
337+
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
338+
AnalyzeTypes bool `mapstructure:"analyze-types"`
339+
}
340+
341+
var _ encoding.TextUnmarshaler = &ForbidigoPattern{}
342+
343+
// ForbidigoPattern corresponds to forbidigo.pattern and adds mapstructure support.
344+
// The YAML field names must match what forbidigo expects.
345+
type ForbidigoPattern struct {
346+
// patternString gets populated when the config contains a string as entry in ForbidigoSettings.Forbid[]
347+
// because ForbidigoPattern implements encoding.TextUnmarshaler
348+
// and the reader uses the mapstructure.TextUnmarshallerHookFunc as decoder hook.
349+
//
350+
// If the entry is a map, then the other fields are set as usual by mapstructure.
351+
patternString string
352+
353+
Pattern string `yaml:"p" mapstructure:"p"`
354+
Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"`
355+
Msg string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"`
356+
}
357+
358+
func (p *ForbidigoPattern) UnmarshalText(text []byte) error {
359+
// Validation happens when instantiating forbidigo.
360+
p.patternString = string(text)
361+
return nil
362+
}
363+
364+
// MarshalString converts the pattern into a string as needed by forbidigo.NewLinter.
365+
//
366+
// MarshalString is intentionally not called MarshalText,
367+
// although it has the same signature
368+
// because implementing encoding.TextMarshaler led to infinite recursion when yaml.Marshal called MarshalText.
369+
func (p *ForbidigoPattern) MarshalString() ([]byte, error) {
370+
if p.patternString != "" {
371+
return []byte(p.patternString), nil
372+
}
373+
374+
return yaml.Marshal(p)
335375
}
336376

337377
type FunlenSettings struct {

pkg/config/reader.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/mitchellh/go-homedir"
11+
"github.com/mitchellh/mapstructure"
1112
"github.com/spf13/viper"
1213
"golang.org/x/exp/slices"
1314

@@ -91,7 +92,14 @@ func (r *FileReader) parseConfig() error {
9192
}
9293
r.cfg.cfgDir = usedConfigDir
9394

94-
if err := viper.Unmarshal(r.cfg); err != nil {
95+
if err := viper.Unmarshal(r.cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
96+
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
97+
mapstructure.StringToTimeDurationHookFunc(),
98+
mapstructure.StringToSliceHookFunc(","),
99+
100+
// Needed for forbidigo.
101+
mapstructure.TextUnmarshallerHookFunc(),
102+
))); err != nil {
95103
return fmt.Errorf("can't unmarshal config by viper: %s", err)
96104
}
97105

pkg/golinters/forbidigo.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/golangci/golangci-lint/pkg/config"
1111
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
1212
"github.com/golangci/golangci-lint/pkg/lint/linter"
13+
"github.com/golangci/golangci-lint/pkg/logutils"
1314
"github.com/golangci/golangci-lint/pkg/result"
1415
)
1516

@@ -40,6 +41,9 @@ func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter {
4041
},
4142
}
4243

44+
// Without AnalyzeTypes, LoadModeSyntax is enough.
45+
// But we cannot make this depend on the settings and have to mirror the mode chosen in GetAllSupportedLinterConfigs,
46+
// therefore we have to use LoadModeTypesInfo in all cases.
4347
return goanalysis.NewLinter(
4448
forbidigoName,
4549
"Forbids identifiers",
@@ -55,16 +59,34 @@ func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]go
5559
forbidigo.OptionExcludeGodocExamples(settings.ExcludeGodocExamples),
5660
// disable "//permit" directives so only "//nolint" directives matters within golangci-lint
5761
forbidigo.OptionIgnorePermitDirectives(true),
62+
forbidigo.OptionAnalyzeTypes(settings.AnalyzeTypes),
5863
}
5964

60-
forbid, err := forbidigo.NewLinter(settings.Forbid, options...)
65+
// Convert patterns back to strings because that is what NewLinter accepts.
66+
var patterns []string
67+
for _, pattern := range settings.Forbid {
68+
buffer, err := pattern.MarshalString()
69+
if err != nil {
70+
return nil, err
71+
}
72+
patterns = append(patterns, string(buffer))
73+
}
74+
75+
forbid, err := forbidigo.NewLinter(patterns, options...)
6176
if err != nil {
6277
return nil, fmt.Errorf("failed to create linter %q: %w", forbidigoName, err)
6378
}
6479

6580
var issues []goanalysis.Issue
6681
for _, file := range pass.Files {
67-
hints, err := forbid.RunWithConfig(forbidigo.RunConfig{Fset: pass.Fset}, file)
82+
runConfig := forbidigo.RunConfig{
83+
Fset: pass.Fset,
84+
DebugLog: logutils.Debug(logutils.DebugKeyForbidigo),
85+
}
86+
if settings != nil && settings.AnalyzeTypes {
87+
runConfig.TypesInfo = pass.TypesInfo
88+
}
89+
hints, err := forbid.RunWithConfig(runConfig, file)
6890
if err != nil {
6991
return nil, fmt.Errorf("forbidigo linter failed on file %q: %w", file.Name.String(), err)
7092
}

pkg/lint/lintersdb/manager.go

+4
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,10 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
427427
linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
428428
WithSince("v1.34.0").
429429
WithPresets(linter.PresetStyle).
430+
// Strictly speaking,
431+
// the additional information is only needed when forbidigoCfg.AnalyzeTypes is chosen by the user.
432+
// But we don't know that here in all cases (sometimes config is not loaded),
433+
// so we have to assume that it is needed to be on the safe side.
430434
WithLoadForGoAnalysis().
431435
WithURL("https://github.com/ashanbrown/forbidigo"),
432436

pkg/logutils/logutils.go

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const (
2222
DebugKeyExcludeRules = "exclude_rules"
2323
DebugKeyExec = "exec"
2424
DebugKeyFilenameUnadjuster = "filename_unadjuster"
25+
DebugKeyForbidigo = "forbidigo"
2526
DebugKeyGoEnv = "goenv"
2627
DebugKeyLinter = "linter"
2728
DebugKeyLintersContext = "linters_context"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
linters-settings:
2+
forbidigo:
3+
analyze-types: true
4+
forbid:
5+
- p: fmt\.Print.*
6+
pkg: ^fmt$
7+
- p: time.Sleep
8+
msg: no sleeping!

test/testdata/forbidigo_example.go

+2
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ package testdata
44

55
import (
66
"fmt"
7+
fmt2 "fmt"
78
"time"
89
)
910

1011
func Forbidigo() {
1112
fmt.Printf("too noisy!!!") // want "use of `fmt\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`"
13+
fmt2.Printf("too noisy!!!") // Not detected because analyze-types is false by default for backward compatbility.
1214
time.Sleep(time.Nanosecond) // want "no sleeping!"
1315
}
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//golangcitest:args -Eforbidigo
2+
//golangcitest:config_path testdata/configs/forbidigo_struct.yml
3+
package testdata
4+
5+
import (
6+
fmt2 "fmt"
7+
"time"
8+
)
9+
10+
func Forbidigo() {
11+
fmt2.Printf("too noisy!!!") // want "use of `fmt2\\.Printf` forbidden by pattern `fmt\\\\.Print\\.\\*`"
12+
time.Sleep(time.Nanosecond) // want "no sleeping!"
13+
}

0 commit comments

Comments
 (0)