Skip to content

Commit f20d983

Browse files
committed
forbidigo: better support for configuring complex rules
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.
1 parent 50901e4 commit f20d983

File tree

10 files changed

+125
-11
lines changed

10 files changed

+125
-11
lines changed

.golangci.reference.yml

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

373390
funlen:
374391
# Checks the number of lines in a function.

go.mod

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ require (
148148
github.com/mattn/go-isatty v0.0.17 // indirect
149149
github.com/mattn/go-runewidth v0.0.9 // indirect
150150
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
151-
github.com/mitchellh/mapstructure v1.5.0 // indirect
151+
github.com/mitchellh/mapstructure v1.5.0
152152
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
153153
github.com/olekukonko/tablewriter v0.0.5 // indirect
154154
github.com/pelletier/go-toml v1.9.5 // indirect
@@ -185,6 +185,6 @@ require (
185185
golang.org/x/text v0.6.0 // indirect
186186
google.golang.org/protobuf v1.28.0 // indirect
187187
gopkg.in/ini.v1 v1.67.0 // indirect
188-
gopkg.in/yaml.v2 v2.4.0 // indirect
188+
gopkg.in/yaml.v2 v2.4.0
189189
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
190190
)

pkg/config/linters_settings.go

+41-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.v2"
69
)
710

811
var defaultLintersSettings = LintersSettings{
@@ -308,10 +311,46 @@ type ExhaustructSettings struct {
308311
}
309312

310313
type ForbidigoSettings struct {
311-
Forbid []string `mapstructure:"forbid"`
312-
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
314+
Forbid []ForbidigoPattern `mapstructure:"forbid"`
315+
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
316+
AnalyzeTypes bool `mapstructure:"analyze-types"`
317+
}
318+
319+
// ForbidigoPattern corresponds to forbidigo.pattern and adds
320+
// mapstructure support. The YAML field names must match what
321+
// forbidigo expects.
322+
type ForbidigoPattern struct {
323+
// patternString gets populated when the config contains a string as
324+
// entry in ForbidigoSettings.Forbid[] because ForbidigoPattern
325+
// implements encoding.TextUnmarshaler and the reader uses the
326+
// mapstructure.TextUnmarshallerHookFunc as decoder hook.
327+
//
328+
// If the entry is a map, then the other fields are set as usual
329+
// by mapstructure.
330+
patternString string
331+
332+
Pattern string `yaml:"p" mapstructure:"p"`
333+
Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"`
334+
Msg string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"`
335+
}
336+
337+
func (p *ForbidigoPattern) UnmarshalText(text []byte) error {
338+
// Validation happens when instantiating forbidigo.
339+
p.patternString = string(text)
340+
return nil
341+
}
342+
343+
// String is intentionally not called MarshalText because that led to infinite
344+
// recursion when yaml.Marshal called MarshalText.
345+
func (p *ForbidigoPattern) String() ([]byte, error) {
346+
if p.patternString != "" {
347+
return []byte(p.patternString), nil
348+
}
349+
return yaml.Marshal(p)
313350
}
314351

352+
var _ encoding.TextUnmarshaler = &ForbidigoPattern{}
353+
315354
type FunlenSettings struct {
316355
Lines int
317356
Statements int

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

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

86-
if err := viper.Unmarshal(r.cfg); err != nil {
87+
if err := viper.Unmarshal(r.cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
88+
// Default hooks (https://github.com/spf13/viper/blob/518241257478c557633ab36e474dfcaeb9a3c623/viper.go#L135-L138).
89+
mapstructure.StringToTimeDurationHookFunc(),
90+
mapstructure.StringToSliceHookFunc(","),
91+
92+
// Needed for forbidigo.
93+
mapstructure.TextUnmarshallerHookFunc(),
94+
))); err != nil {
8795
return fmt.Errorf("can't unmarshal config by viper: %s", err)
8896
}
8997

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.String()
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
@@ -417,6 +417,10 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
417417
linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
418418
WithSince("v1.34.0").
419419
WithPresets(linter.PresetStyle).
420+
// Strictly speaking,
421+
// the additional information is only needed when forbidigoCfg.AnalyzeTypes is chosen by the user.
422+
// But we don't know that here in all cases (sometimes config is not loaded),
423+
// so we have to assume that it is needed to be on the safe side.
420424
WithLoadForGoAnalysis().
421425
WithURL("https://github.com/ashanbrown/forbidigo"),
422426

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)