Skip to content

Commit bd4c77e

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 defde13 commit bd4c77e

File tree

10 files changed

+135
-17
lines changed

10 files changed

+135
-17
lines changed

.golangci.reference.yml

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

370387
funlen:
371388
# Checks the number of lines in a function.

go.mod

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

pkg/config/linters_settings.go

+40-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package config
22

33
import (
4+
"encoding"
45
"runtime"
56

67
"github.com/pkg/errors"
8+
"gopkg.in/yaml.v2"
79
)
810

911
var defaultLintersSettings = LintersSettings{
@@ -307,10 +309,46 @@ type ExhaustructSettings struct {
307309
}
308310

309311
type ForbidigoSettings struct {
310-
Forbid []string `mapstructure:"forbid"`
311-
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
312+
Forbid []ForbidigoPattern `mapstructure:"forbid"`
313+
ExcludeGodocExamples bool `mapstructure:"exclude-godoc-examples"`
314+
AnalyzeTypes bool `mapstructure:"analyze-types"`
315+
}
316+
317+
// ForbidigoPattern corresponds to forbidigo.pattern and adds
318+
// mapstructure support. The YAML field names must match what
319+
// forbidigo expects.
320+
type ForbidigoPattern struct {
321+
// patternString gets populated when the config contains a string as
322+
// entry in ForbidigoSettings.Forbid[] because ForbidigoPattern
323+
// implements encoding.TextUnmarshaler and the reader uses the
324+
// mapstructure.TextUnmarshallerHookFunc as decoder hook.
325+
//
326+
// If the entry is a map, then the other fields are set as usual
327+
// by mapstructure.
328+
patternString string
329+
330+
Pattern string `yaml:"p" mapstructure:"p"`
331+
Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"`
332+
Msg string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"`
333+
}
334+
335+
func (p *ForbidigoPattern) UnmarshalText(text []byte) error {
336+
// Validation happens when instantiating forbidigo.
337+
p.patternString = string(text)
338+
return nil
312339
}
313340

341+
// String is intentionally not called MarshalText because that led to infinite
342+
// recursion when yaml.Marshal called MarshalText.
343+
func (p *ForbidigoPattern) String() ([]byte, error) {
344+
if p.patternString != "" {
345+
return []byte(p.patternString), nil
346+
}
347+
return yaml.Marshal(p)
348+
}
349+
350+
var _ encoding.TextUnmarshaler = &ForbidigoPattern{}
351+
314352
type FunlenSettings struct {
315353
Lines int
316354
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

+28-4
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ 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

1617
const forbidigoName = "forbidigo"
1718

18-
//nolint:dupl
1919
func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter {
2020
var mu sync.Mutex
2121
var resIssues []goanalysis.Issue
@@ -40,14 +40,20 @@ func NewForbidigo(settings *config.ForbidigoSettings) *goanalysis.Linter {
4040
},
4141
}
4242

43+
loadMode := goanalysis.LoadModeSyntax
44+
if settings != nil && settings.AnalyzeTypes {
45+
// Need more information for the analysis.
46+
loadMode = goanalysis.LoadModeTypesInfo
47+
}
48+
4349
return goanalysis.NewLinter(
4450
forbidigoName,
4551
"Forbids identifiers",
4652
[]*analysis.Analyzer{analyzer},
4753
nil,
4854
).WithIssuesReporter(func(*linter.Context) []goanalysis.Issue {
4955
return resIssues
50-
}).WithLoadMode(goanalysis.LoadModeSyntax)
56+
}).WithLoadMode(loadMode)
5157
}
5258

5359
func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]goanalysis.Issue, error) {
@@ -57,14 +63,32 @@ func runForbidigo(pass *analysis.Pass, settings *config.ForbidigoSettings) ([]go
5763
forbidigo.OptionIgnorePermitDirectives(true),
5864
}
5965

60-
forbid, err := forbidigo.NewLinter(settings.Forbid, options...)
66+
// Convert patterns back to strings because that is what NewLinter
67+
// accepts.
68+
var patterns []string
69+
for _, pattern := range settings.Forbid {
70+
buffer, err := pattern.String()
71+
if err != nil {
72+
return nil, err
73+
}
74+
patterns = append(patterns, string(buffer))
75+
}
76+
77+
forbid, err := forbidigo.NewLinter(patterns, options...)
6178
if err != nil {
6279
return nil, errors.Wrapf(err, "failed to create linter %q", forbidigoName)
6380
}
6481

6582
var issues []goanalysis.Issue
6683
for _, file := range pass.Files {
67-
hints, err := forbid.RunWithConfig(forbidigo.RunConfig{Fset: pass.Fset}, file)
84+
runConfig := forbidigo.RunConfig{
85+
Fset: pass.Fset,
86+
DebugLog: logutils.Debug(logutils.DebugKeyForbidigo),
87+
}
88+
if settings != nil && settings.AnalyzeTypes {
89+
runConfig.TypesInfo = pass.TypesInfo
90+
}
91+
hints, err := forbid.RunWithConfig(runConfig, file)
6892
if err != nil {
6993
return nil, errors.Wrapf(err, "forbidigo linter failed on file %q", file.Name.String())
7094
}

pkg/lint/lintersdb/manager.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -414,10 +414,17 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
414414
WithLoadForGoAnalysis().
415415
WithURL("https://github.com/kyoh86/exportloopref"),
416416

417-
linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
418-
WithSince("v1.34.0").
419-
WithPresets(linter.PresetStyle).
420-
WithURL("https://github.com/ashanbrown/forbidigo"),
417+
func() *linter.Config {
418+
l := linter.NewConfig(golinters.NewForbidigo(forbidigoCfg)).
419+
WithSince("v1.34.0").
420+
WithPresets(linter.PresetStyle).
421+
WithURL("https://github.com/ashanbrown/forbidigo")
422+
if forbidigoCfg != nil && forbidigoCfg.AnalyzeTypes {
423+
// Need more information for the analysis.
424+
l = l.WithLoadForGoAnalysis()
425+
}
426+
return l
427+
}(),
421428

422429
linter.NewConfig(golinters.NewForceTypeAssert()).
423430
WithSince("v1.38.0").

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)