Skip to content

Commit b14d05c

Browse files
authored
dev: rewrite linters Manager (#4419)
1 parent 26f8088 commit b14d05c

27 files changed

+1749
-1825
lines changed

docs/src/docs/contributing/architecture.mdx

+25-17
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,18 @@ graph LR
2222

2323
</ResponsiveContainer>
2424

25+
## Init
26+
27+
The configuration is loaded from file and flags by `config.Loader` inside `PersistentPreRun` (or `PreRun`) of the commands that require configuration.
28+
29+
The linter database (`linterdb.Manager`) is fill based on the configuration:
30+
- The linters ("internals" and plugins) are built by `linterdb.LinterBuilder` and `linterdb.PluginBuilder` builders.
31+
- The configuration is validated by `linterdb.Validator`.
32+
2533
## Load Packages
2634

2735
Loading packages is listing all packages and their recursive dependencies for analysis.
28-
Also, depending on the enabled linters set some parsing of the source code can be performed
29-
at this step.
36+
Also, depending on the enabled linters set some parsing of the source code can be performed at this step.
3037

3138
Packages loading starts here:
3239

@@ -79,10 +86,10 @@ and outputs list of packages and requested information about them: filenames, ty
7986

8087
First, we need to find all enabled linters. All linters are registered here:
8188

82-
```go title=pkg/lint/lintersdb/manager.go
83-
func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
89+
```go title=pkg/lint/lintersdb/builder_linter.go
90+
func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
8491
// ...
85-
linters = append(linters,
92+
return []*linter.Config{
8693
// ...
8794
linter.NewConfig(golinters.NewBodyclose()).
8895
WithSince("v1.18.0").
@@ -97,26 +104,25 @@ func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
97104
WithPresets(linter.PresetBugs, linter.PresetMetaLinter).
98105
WithAlternativeNames("vet", "vetshadow").
99106
WithURL("https://pkg.go.dev/cmd/vet"),
107+
// ...
100108
}
101-
// ...
102109
}
103110
```
104111

105112
We filter requested in config and command-line linters in `EnabledSet`:
106113

107-
```go title=pkg/lint/lintersdb/enabled_set.go
108-
func (es EnabledSet) GetEnabledLintersMap() (map[string]*linter.Config, error)
114+
```go title=pkg/lint/lintersdb/manager.go
115+
func (m *Manager) GetEnabledLintersMap() (map[string]*linter.Config, error)
109116
```
110117

111118
We merge enabled linters into one `MetaLinter` to improve execution time if we can:
112119

113-
```go title=pkg/lint/lintersdb/enabled_set.go
114-
// GetOptimizedLinters returns enabled linters after optimization (merging) of multiple linters
115-
// into a fewer number of linters. E.g. some go/analysis linters can be optimized into
116-
// one metalinter for data reuse and speed up.
117-
func (es EnabledSet) GetOptimizedLinters() ([]*linter.Config, error) {
120+
```go titlepkg/lint/lintersdb/manager.go
121+
// GetOptimizedLinters returns enabled linters after optimization (merging) of multiple linters into a fewer number of linters.
122+
// E.g. some go/analysis linters can be optimized into one metalinter for data reuse and speed up.
123+
func (m *Manager) GetOptimizedLinters() ([]*linter.Config, error) {
118124
// ...
119-
es.combineGoAnalysisLinters(resultLintersSet)
125+
m.combineGoAnalysisLinters(resultLintersSet)
120126
// ...
121127
}
122128
```
@@ -133,9 +139,11 @@ type MetaLinter struct {
133139
Currently, all linters except `unused` can be merged into this meta linter.
134140
The `unused` isn't merged because it has high memory usage.
135141

136-
Linters execution starts in `runAnalyzers`. It's the most complex part of the `golangci-lint`.
137-
We use custom [go/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) runner there. It runs as much as it can in parallel. It lazy-loads as much as it can
138-
to reduce memory usage. Also, it sets all heavyweight data to `nil` as becomes unneeded to save memory.
142+
Linters execution starts in `runAnalyzers`.
143+
It's the most complex part of the `golangci-lint`.
144+
We use custom [go/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) runner there.
145+
It runs as much as it can in parallel. It lazy-loads as much as it can to reduce memory usage.
146+
Also, it sets all heavyweight data to `nil` as becomes unneeded to save memory.
139147

140148
We don't use existing [multichecker](https://pkg.go.dev/golang.org/x/tools/go/analysis/multichecker) because
141149
it doesn't use caching and doesn't have some important performance optimizations.

docs/src/docs/contributing/new-linters.mdx

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ After that:
2828
Look at other linters in this directory.
2929
Implement linter integration and check that test passes.
3030
3. Add the new struct for the linter (which you've implemented in `pkg/golinters/{yourlintername}.go`) to the
31-
list of all supported linters in [`pkg/lint/lintersdb/manager.go`](https://github.com/golangci/golangci-lint/blob/master/pkg/lint/lintersdb/manager.go)
32-
to the function `GetAllSupportedLinterConfigs`.
31+
list of all supported linters in [`pkg/lint/lintersdb/builder_linter.go`](https://github.com/golangci/golangci-lint/blob/master/pkg/lint/lintersdb/builder_linter.go)
32+
to the method `LinterBuilder.Build`.
3333
- Add `WithSince("next_version")`, where `next_version` must be replaced by the next minor version. (ex: v1.2.0 if the current version is v1.1.0)
3434
4. Find out what options do you need to configure for the linter.
3535
For example, `nakedret` has only 1 option: [`max-func-lines`](https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml).

pkg/commands/help.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func newHelpCommand(logger logutils.Log) *helpCommand {
4141
Args: cobra.NoArgs,
4242
ValidArgsFunction: cobra.NoFileCompletions,
4343
Run: c.execute,
44-
PreRun: c.preRun,
44+
PreRunE: c.preRunE,
4545
},
4646
)
4747

@@ -50,10 +50,18 @@ func newHelpCommand(logger logutils.Log) *helpCommand {
5050
return c
5151
}
5252

53-
func (c *helpCommand) preRun(_ *cobra.Command, _ []string) {
53+
func (c *helpCommand) preRunE(_ *cobra.Command, _ []string) error {
5454
// The command doesn't depend on the real configuration.
5555
// It just needs the list of all plugins and all presets.
56-
c.dbManager = lintersdb.NewManager(config.NewDefault(), c.log)
56+
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(),
57+
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
58+
if err != nil {
59+
return err
60+
}
61+
62+
c.dbManager = dbManager
63+
64+
return nil
5765
}
5866

5967
func (c *helpCommand) execute(_ *cobra.Command, _ []string) {

pkg/commands/linters.go

+9-6
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ type lintersCommand struct {
2727

2828
log logutils.Log
2929

30-
dbManager *lintersdb.Manager
31-
enabledLintersSet *lintersdb.EnabledSet
30+
dbManager *lintersdb.Manager
3231
}
3332

3433
func newLintersCommand(logger logutils.Log, cfg *config.Config) *lintersCommand {
@@ -65,15 +64,19 @@ func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error {
6564
return fmt.Errorf("can't load config: %w", err)
6665
}
6766

68-
c.dbManager = lintersdb.NewManager(c.cfg, c.log)
69-
c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager,
70-
lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg)
67+
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
68+
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
69+
if err != nil {
70+
return err
71+
}
72+
73+
c.dbManager = dbManager
7174

7275
return nil
7376
}
7477

7578
func (c *lintersCommand) execute(_ *cobra.Command, _ []string) error {
76-
enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap()
79+
enabledLintersMap, err := c.dbManager.GetEnabledLintersMap()
7780
if err != nil {
7881
return fmt.Errorf("can't get enabled linters: %w", err)
7982
}

pkg/commands/run.go

+12-9
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,7 @@ type runCommand struct {
8080

8181
buildInfo BuildInfo
8282

83-
dbManager *lintersdb.Manager
84-
enabledLintersSet *lintersdb.EnabledSet
83+
dbManager *lintersdb.Manager
8584

8685
log logutils.Log
8786
debugf logutils.DebugFunc
@@ -171,9 +170,13 @@ func (c *runCommand) persistentPostRunE(_ *cobra.Command, _ []string) error {
171170
}
172171

173172
func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error {
174-
c.dbManager = lintersdb.NewManager(c.cfg, c.log)
175-
c.enabledLintersSet = lintersdb.NewEnabledSet(c.dbManager,
176-
lintersdb.NewValidator(c.dbManager), c.log.Child(logutils.DebugKeyLintersDB), c.cfg)
173+
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
174+
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
175+
if err != nil {
176+
return err
177+
}
178+
179+
c.dbManager = dbManager
177180

178181
c.goenv = goutil.NewEnv(c.log.Child(logutils.DebugKeyGoEnv))
179182

@@ -340,12 +343,12 @@ func (c *runCommand) runAndPrint(ctx context.Context, args []string) error {
340343
func (c *runCommand) runAnalysis(ctx context.Context, args []string) ([]result.Issue, error) {
341344
c.cfg.Run.Args = args
342345

343-
lintersToRun, err := c.enabledLintersSet.GetOptimizedLinters()
346+
lintersToRun, err := c.dbManager.GetOptimizedLinters()
344347
if err != nil {
345348
return nil, err
346349
}
347350

348-
enabledLintersMap, err := c.enabledLintersSet.GetEnabledLintersMap()
351+
enabledLintersMap, err := c.dbManager.GetEnabledLintersMap()
349352
if err != nil {
350353
return nil, err
351354
}
@@ -361,8 +364,8 @@ func (c *runCommand) runAnalysis(ctx context.Context, args []string) ([]result.I
361364
}
362365
lintCtx.Log = c.log.Child(logutils.DebugKeyLintersContext)
363366

364-
runner, err := lint.NewRunner(c.cfg, c.log.Child(logutils.DebugKeyRunner),
365-
c.goenv, c.enabledLintersSet, c.lineCache, c.fileCache, c.dbManager, lintCtx.Packages)
367+
runner, err := lint.NewRunner(c.log.Child(logutils.DebugKeyRunner),
368+
c.cfg, c.goenv, c.lineCache, c.fileCache, c.dbManager, lintCtx.Packages)
366369
if err != nil {
367370
return nil, err
368371
}

pkg/commands/version.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ type versionInfo struct {
2525
}
2626

2727
type versionOptions struct {
28-
Format string `mapstructure:"format"`
29-
Debug bool `mapstructure:"debug"`
28+
Format string
29+
Debug bool
3030
}
3131

3232
type versionCommand struct {

pkg/config/config.go

+28-12
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package config
22

33
import (
4-
"errors"
5-
"fmt"
64
"os"
5+
"regexp"
76
"strings"
87

98
hcversion "github.com/hashicorp/go-version"
@@ -33,18 +32,16 @@ func (c *Config) GetConfigDir() string {
3332
}
3433

3534
func (c *Config) Validate() error {
36-
for i, rule := range c.Issues.ExcludeRules {
37-
if err := rule.Validate(); err != nil {
38-
return fmt.Errorf("error in exclude rule #%d: %w", i, err)
39-
}
35+
validators := []func() error{
36+
c.Issues.Validate,
37+
c.Severity.Validate,
38+
c.LintersSettings.Validate,
39+
c.Linters.Validate,
4040
}
4141

42-
if len(c.Severity.Rules) > 0 && c.Severity.Default == "" {
43-
return errors.New("can't set severity rule option: no default severity defined")
44-
}
45-
for i, rule := range c.Severity.Rules {
46-
if err := rule.Validate(); err != nil {
47-
return fmt.Errorf("error in severity rule #%d: %w", i, err)
42+
for _, v := range validators {
43+
if err := v(); err != nil {
44+
return err
4845
}
4946
}
5047

@@ -90,3 +87,22 @@ func detectGoVersion() string {
9087

9188
return "1.17"
9289
}
90+
91+
// Trims the Go version to keep only M.m.
92+
// Since Go 1.21 the version inside the go.mod can be a patched version (ex: 1.21.0).
93+
// The version can also include information which we want to remove (ex: 1.21alpha1)
94+
// https://go.dev/doc/toolchain#versions
95+
// This a problem with staticcheck and gocritic.
96+
func trimGoVersion(v string) string {
97+
if v == "" {
98+
return ""
99+
}
100+
101+
exp := regexp.MustCompile(`(\d\.\d+)(?:\.\d+|[a-z]+\d)`)
102+
103+
if exp.MatchString(v) {
104+
return exp.FindStringSubmatch(v)[1]
105+
}
106+
107+
return v
108+
}

pkg/config/config_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,52 @@ func TestIsGoGreaterThanOrEqual(t *testing.T) {
8484
})
8585
}
8686
}
87+
88+
func Test_trimGoVersion(t *testing.T) {
89+
testCases := []struct {
90+
desc string
91+
version string
92+
expected string
93+
}{
94+
{
95+
desc: "patched version",
96+
version: "1.22.0",
97+
expected: "1.22",
98+
},
99+
{
100+
desc: "minor version",
101+
version: "1.22",
102+
expected: "1.22",
103+
},
104+
{
105+
desc: "RC version",
106+
version: "1.22rc1",
107+
expected: "1.22",
108+
},
109+
{
110+
desc: "alpha version",
111+
version: "1.22alpha1",
112+
expected: "1.22",
113+
},
114+
{
115+
desc: "beta version",
116+
version: "1.22beta1",
117+
expected: "1.22",
118+
},
119+
{
120+
desc: "semver RC version",
121+
version: "1.22.0-rc1",
122+
expected: "1.22",
123+
},
124+
}
125+
126+
for _, test := range testCases {
127+
test := test
128+
t.Run(test.desc, func(t *testing.T) {
129+
t.Parallel()
130+
131+
version := trimGoVersion(test.version)
132+
assert.Equal(t, test.expected, version)
133+
})
134+
}
135+
}

pkg/config/issues.go

+10
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,16 @@ type Issues struct {
122122
NeedFix bool `mapstructure:"fix"`
123123
}
124124

125+
func (i *Issues) Validate() error {
126+
for i, rule := range i.ExcludeRules {
127+
if err := rule.Validate(); err != nil {
128+
return fmt.Errorf("error in exclude rule #%d: %w", i, err)
129+
}
130+
}
131+
132+
return nil
133+
}
134+
125135
type ExcludeRule struct {
126136
BaseRule `mapstructure:",squash"`
127137
}

0 commit comments

Comments
 (0)