Skip to content

Commit 04b16cf

Browse files
pohlyldez
authored andcommitted
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 0b8ebea commit 04b16cf

File tree

10 files changed

+129
-11
lines changed

10 files changed

+129
-11
lines changed

.golangci.reference.yml

Lines changed: 21 additions & 4 deletions
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, use type information to
374+
# replace expressions with strings that contain the package name and (for
375+
# methods and fields) the type name. This makes it possible to handle
376+
# 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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ require (
153153
github.com/mattn/go-isatty v0.0.17 // indirect
154154
github.com/mattn/go-runewidth v0.0.9 // indirect
155155
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
156-
github.com/mitchellh/mapstructure v1.5.0 // indirect
156+
github.com/mitchellh/mapstructure v1.5.0
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
@@ -190,6 +190,6 @@ require (
190190
golang.org/x/text v0.9.0 // indirect
191191
google.golang.org/protobuf v1.28.0 // indirect
192192
gopkg.in/ini.v1 v1.67.0 // indirect
193-
gopkg.in/yaml.v2 v2.4.0 // indirect
193+
gopkg.in/yaml.v2 v2.4.0
194194
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
195195
)

pkg/config/linters_settings.go

Lines changed: 45 additions & 2 deletions
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{
@@ -330,10 +333,50 @@ 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+
// ForbidigoPattern corresponds to forbidigo.pattern and adds
342+
// mapstructure support. The YAML field names must match what
343+
// forbidigo expects.
344+
type ForbidigoPattern struct {
345+
// patternString gets populated when the config contains a string as
346+
// entry in ForbidigoSettings.Forbid[] because ForbidigoPattern
347+
// implements encoding.TextUnmarshaler and the reader uses the
348+
// mapstructure.TextUnmarshallerHookFunc as decoder hook.
349+
//
350+
// If the entry is a map, then the other fields are set as usual
351+
// by mapstructure.
352+
patternString string
353+
354+
Pattern string `yaml:"p" mapstructure:"p"`
355+
Package string `yaml:"pkg,omitempty" mapstructure:"pkg,omitempty"`
356+
Msg string `yaml:"msg,omitempty" mapstructure:"msg,omitempty"`
357+
}
358+
359+
func (p *ForbidigoPattern) UnmarshalText(text []byte) error {
360+
// Validation happens when instantiating forbidigo.
361+
p.patternString = string(text)
362+
return nil
335363
}
336364

365+
// MarshalString converts the pattern into a string as needed by
366+
// forbidigo.NewLinter.
367+
//
368+
// MarshalString is intentionally not called MarshalText although it has the
369+
// same signature because implementing encoding.TextMarshaler led to infinite
370+
// recursion when yaml.Marshal called MarshalText.
371+
func (p *ForbidigoPattern) MarshalString() ([]byte, error) {
372+
if p.patternString != "" {
373+
return []byte(p.patternString), nil
374+
}
375+
return yaml.Marshal(p)
376+
}
377+
378+
var _ encoding.TextUnmarshaler = &ForbidigoPattern{}
379+
337380
type FunlenSettings struct {
338381
Lines int
339382
Statements int

pkg/config/reader.go

Lines changed: 9 additions & 1 deletion
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

Lines changed: 24 additions & 2 deletions
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

Lines changed: 4 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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"
Lines changed: 8 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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
}
Lines changed: 13 additions & 0 deletions
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)