Skip to content

Commit 25c2b07

Browse files
authored
plugin: allow to use settings for plugins (#3887)
1 parent 2dcd82f commit 25c2b07

File tree

4 files changed

+193
-122
lines changed

4 files changed

+193
-122
lines changed

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

+57-30
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
title: New linters
33
---
44

5-
## How to write a custom linter
5+
## How to write a linter
66

7-
Use `go/analysis` and take a look at [this tutorial](https://disaev.me/p/writing-useful-go-analysis-linter/): it shows how to write `go/analysis` linter
8-
from scratch and integrate it into `golangci-lint`.
7+
Use `go/analysis` and take a look at [this tutorial](https://disaev.me/p/writing-useful-go-analysis-linter/):
8+
it shows how to write `go/analysis` linter from scratch and integrate it into `golangci-lint`.
99

1010
## How to add a public linter to `golangci-lint`
1111

@@ -16,8 +16,14 @@ After that:
1616

1717
1. Implement functional tests for the linter:
1818
- Add one file into directory [`test/testdata`](https://github.com/golangci/golangci-lint/tree/master/test/testdata).
19-
- Run `T=yourlintername.go make test_linters` to ensure that test fails.
20-
- Run `go run ./cmd/golangci-lint/ run --no-config --disable-all --enable=yourlintername ./test/testdata/yourlintername.go`
19+
- Run the test to ensure that test fails:
20+
```bash
21+
T=yourlintername.go make test_linters
22+
```
23+
- Run:
24+
```bash
25+
go run ./cmd/golangci-lint/ run --no-config --disable-all --enable=yourlintername ./test/testdata/yourlintername.go
26+
```
2127
2. Add a new file `pkg/golinters/{yourlintername}.go`.
2228
Look at other linters in this directory.
2329
Implement linter integration and check that test passes.
@@ -33,58 +39,79 @@ After that:
3339
if you think that this project needs not default values.
3440
- [config struct](https://github.com/golangci/golangci-lint/blob/master/pkg/config/config.go) -
3541
don't forget about `mapstructure` tag for proper configuration files parsing by [pflag](https://github.com/spf13/pflag).
36-
5. Take a look at the example of [Pull Request with new linter support](https://github.com/golangci/golangci-lint/pulls?q=is%3Apr+is%3Amerged+label%3A%22linter%3A+new%22).
42+
5. Take a look at the example of [pull requests with new linter support](https://github.com/golangci/golangci-lint/pulls?q=is%3Apr+is%3Amerged+label%3A%22linter%3A+new%22).
3743

3844
## How to add a private linter to `golangci-lint`
3945

4046
Some people and organizations may choose to have custom-made linters run as a part of `golangci-lint`.
4147
Typically, these linters can't be open-sourced or too specific.
48+
4249
Such linters can be added through Go's plugin library.
4350

4451
For a private linter (which acts as a plugin) to work properly,
45-
the plugin as well as the golangci-lint binary needs to be built for the same environment. `CGO_ENABLED` is another requirement.
52+
the plugin as well as the golangci-lint binary **needs to be built for the same environment**.
53+
54+
`CGO_ENABLED` is another requirement.
55+
4656
This means that `golangci-lint` needs to be built for whatever machine you intend to run it on
4757
(cloning the golangci-lint repository and running a `CGO_ENABLED=1 make build` should do the trick for your machine).
4858

4959
### Configure a Plugin
5060

51-
If you already have a linter plugin available, you can follow these steps to define it's usage in a projects
52-
`.golangci.yml` file. An example linter can be found at [here](https://github.com/golangci/example-plugin-linter). If you're looking for
53-
instructions on how to configure your own custom linter, they can be found further down.
61+
If you already have a linter plugin available, you can follow these steps to define its usage in a projects `.golangci.yml` file.
62+
63+
An example linter can be found at [here](https://github.com/golangci/example-plugin-linter).
64+
65+
If you're looking for instructions on how to configure your own custom linter, they can be found further down.
5466

5567
1. If the project you want to lint does not have one already, copy the [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) to the root directory.
5668
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
57-
58-
```yaml
59-
linters-settings:
60-
custom:
61-
example:
62-
path: /example.so
63-
description: The description of the linter
64-
original-url: github.com/golangci/example-linter
65-
```
69+
```yaml
70+
linters-settings:
71+
custom:
72+
example:
73+
path: /example.so
74+
description: The description of the linter
75+
original-url: github.com/golangci/example-linter
76+
settings: # Settings are optional.
77+
one: Foo
78+
two:
79+
- name: Bar
80+
three:
81+
name: Bar
82+
```
6683
6784
That is all the configuration that is required to run a custom linter in your project.
85+
6886
Custom linters are disabled by default, and are not enabled when `linters.enable-all` is specified.
6987
They can be enabled by adding them the `linters.enable` list, or providing the enabled option on the command line (`golangci-lint run -Eexample`).
7088

89+
The configuration inside the `settings` field of linter have some limitations (there are NOT related to the plugin system itself):
90+
we use Viper to handle the configuration but Viper put all the keys in lowercase, and `.` cannot be used inside a key.
91+
7192
### Create a Plugin
7293

73-
Your linter must implement one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
74-
Your project should also use `go.mod`. All versions of libraries that overlap `golangci-lint` (including replaced
75-
libraries) MUST be set to the same version as `golangci-lint`. You can see the versions by running `go version -m golangci-lint`.
94+
Your linter must provide one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
95+
96+
Your project should also use `go.mod`.
7697

77-
You'll also need to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define a
78-
variable of name `AnalyzerPlugin`. The `AnalyzerPlugin` instance MUST implement the following interface:
98+
All versions of libraries that overlap `golangci-lint` (including replaced libraries) MUST be set to the same version as `golangci-lint`.
99+
You can see the versions by running `go version -m golangci-lint`.
79100

101+
You'll also need to create a Go file like `plugin/example.go`.
102+
103+
This file MUST be in the package `main`, and MUST define an exposed function called `New` with the following signature:
80104
```go
81-
type AnalyzerPlugin interface {
82-
GetAnalyzers() []*analysis.Analyzer
105+
func New(conf any) ([]*analysis.Analyzer, error) {
106+
// ...
83107
}
84108
```
85109

86-
The type of `AnalyzerPlugin` is not important, but is by convention `type analyzerPlugin struct {}`. See
87-
[plugin/example.go](https://github.com/golangci/example-plugin-linter/blob/master/plugin/example.go) for more info.
110+
See [plugin/example.go](https://github.com/golangci/example-plugin-linter/blob/master/plugin/example.go) for more info.
111+
112+
To build the plugin, from the root project directory, run:
113+
```bash
114+
go build -buildmode=plugin plugin/example.go
115+
```
88116

89-
To build the plugin, from the root project directory, run `go build -buildmode=plugin plugin/example.go`. This will create a plugin `*.so`
90-
file that can be copied into your project or another well known location for usage in golangci-lint.
117+
This will create a plugin `*.so` file that can be copied into your project or another well known location for usage in `golangci-lint`.

pkg/config/linters_settings.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,9 @@ type CustomLinterSettings struct {
828828
Path string
829829
// Description describes the purpose of the private linter.
830830
Description string
831-
// The URL containing the source code for the private linter.
831+
// OriginalURL The URL containing the source code for the private linter.
832832
OriginalURL string `mapstructure:"original-url"`
833+
834+
// Settings plugin settings only work with linterdb.PluginConstructor symbol.
835+
Settings any
833836
}

pkg/lint/lintersdb/custom_linters.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package lintersdb
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"plugin"
7+
8+
"github.com/hashicorp/go-multierror"
9+
"github.com/spf13/viper"
10+
"golang.org/x/tools/go/analysis"
11+
12+
"github.com/golangci/golangci-lint/pkg/config"
13+
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
14+
"github.com/golangci/golangci-lint/pkg/lint/linter"
15+
"github.com/golangci/golangci-lint/pkg/logutils"
16+
"github.com/golangci/golangci-lint/pkg/report"
17+
)
18+
19+
type AnalyzerPlugin interface {
20+
GetAnalyzers() []*analysis.Analyzer
21+
}
22+
23+
// WithCustomLinters loads private linters that are specified in the golangci config file.
24+
func (m *Manager) WithCustomLinters() *Manager {
25+
if m.log == nil {
26+
m.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &report.Data{})
27+
}
28+
29+
if m.cfg == nil {
30+
return m
31+
}
32+
33+
for name, settings := range m.cfg.LintersSettings.Custom {
34+
lc, err := m.loadCustomLinterConfig(name, settings)
35+
36+
if err != nil {
37+
m.log.Errorf("Unable to load custom analyzer %s:%s, %v", name, settings.Path, err)
38+
} else {
39+
m.nameToLCs[name] = append(m.nameToLCs[name], lc)
40+
}
41+
}
42+
43+
return m
44+
}
45+
46+
// loadCustomLinterConfig loads the configuration of private linters.
47+
// Private linters are dynamically loaded from .so plugin files.
48+
func (m *Manager) loadCustomLinterConfig(name string, settings config.CustomLinterSettings) (*linter.Config, error) {
49+
analyzers, err := m.getAnalyzerPlugin(settings.Path, settings.Settings)
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
m.log.Infof("Loaded %s: %s", settings.Path, name)
55+
56+
customLinter := goanalysis.NewLinter(name, settings.Description, analyzers, nil).
57+
WithLoadMode(goanalysis.LoadModeTypesInfo)
58+
59+
linterConfig := linter.NewConfig(customLinter).
60+
WithEnabledByDefault().
61+
WithLoadForGoAnalysis().
62+
WithURL(settings.OriginalURL)
63+
64+
return linterConfig, nil
65+
}
66+
67+
// getAnalyzerPlugin loads a private linter as specified in the config file,
68+
// loads the plugin from a .so file,
69+
// and returns the 'AnalyzerPlugin' interface implemented by the private plugin.
70+
// An error is returned if the private linter cannot be loaded
71+
// or the linter does not implement the AnalyzerPlugin interface.
72+
func (m *Manager) getAnalyzerPlugin(path string, settings any) ([]*analysis.Analyzer, error) {
73+
if !filepath.IsAbs(path) {
74+
// resolve non-absolute paths relative to config file's directory
75+
configFilePath := viper.ConfigFileUsed()
76+
absConfigFilePath, err := filepath.Abs(configFilePath)
77+
if err != nil {
78+
return nil, fmt.Errorf("could not get absolute representation of config file path %q: %v", configFilePath, err)
79+
}
80+
path = filepath.Join(filepath.Dir(absConfigFilePath), path)
81+
}
82+
83+
plug, err := plugin.Open(path)
84+
if err != nil {
85+
return nil, err
86+
}
87+
88+
analyzers, err := m.lookupPlugin(plug, settings)
89+
if err != nil {
90+
return nil, fmt.Errorf("lookup plugin %s: %w", path, err)
91+
}
92+
93+
return analyzers, nil
94+
}
95+
96+
func (m *Manager) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
97+
symbol, err := plug.Lookup("New")
98+
if err != nil {
99+
analyzers, errP := m.lookupAnalyzerPlugin(plug)
100+
if errP != nil {
101+
// TODO(ldez): use `errors.Join` when we will upgrade to go1.20.
102+
return nil, multierror.Append(err, errP)
103+
}
104+
105+
return analyzers, nil
106+
}
107+
108+
// The type func cannot be used here, must be the explicit signature.
109+
constructor, ok := symbol.(func(any) ([]*analysis.Analyzer, error))
110+
if !ok {
111+
return nil, fmt.Errorf("plugin does not abide by 'New' function: %T", symbol)
112+
}
113+
114+
return constructor(settings)
115+
}
116+
117+
func (m *Manager) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
118+
symbol, err := plug.Lookup("AnalyzerPlugin")
119+
if err != nil {
120+
return nil, err
121+
}
122+
123+
m.log.Warnf("plugin: 'AnalyzerPlugin' plugins are deprecated, please use the new plugin signature: " +
124+
"https://golangci-lint.run/contributing/new-linters/#create-a-plugin")
125+
126+
analyzerPlugin, ok := symbol.(AnalyzerPlugin)
127+
if !ok {
128+
return nil, fmt.Errorf("plugin does not abide by 'AnalyzerPlugin' interface: %T", symbol)
129+
}
130+
131+
return analyzerPlugin.GetAnalyzers(), nil
132+
}

pkg/lint/lintersdb/manager.go

-91
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
package lintersdb
22

33
import (
4-
"fmt"
5-
"path/filepath"
6-
"plugin"
7-
8-
"github.com/spf13/viper"
9-
"golang.org/x/tools/go/analysis"
10-
114
"github.com/golangci/golangci-lint/pkg/config"
125
"github.com/golangci/golangci-lint/pkg/golinters"
13-
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
146
"github.com/golangci/golangci-lint/pkg/lint/linter"
157
"github.com/golangci/golangci-lint/pkg/logutils"
16-
"github.com/golangci/golangci-lint/pkg/report"
178
)
189

1910
type Manager struct {
@@ -37,28 +28,6 @@ func NewManager(cfg *config.Config, log logutils.Log) *Manager {
3728
return m
3829
}
3930

40-
// WithCustomLinters loads private linters that are specified in the golangci config file.
41-
func (m *Manager) WithCustomLinters() *Manager {
42-
if m.log == nil {
43-
m.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &report.Data{})
44-
}
45-
if m.cfg != nil {
46-
for name, settings := range m.cfg.LintersSettings.Custom {
47-
lc, err := m.loadCustomLinterConfig(name, settings)
48-
49-
if err != nil {
50-
m.log.Errorf("Unable to load custom analyzer %s:%s, %v",
51-
name,
52-
settings.Path,
53-
err)
54-
} else {
55-
m.nameToLCs[name] = append(m.nameToLCs[name], lc)
56-
}
57-
}
58-
}
59-
return m
60-
}
61-
6231
func (Manager) AllPresets() []string {
6332
return []string{
6433
linter.PresetBugs,
@@ -950,63 +919,3 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {
950919

951920
return ret
952921
}
953-
954-
// loadCustomLinterConfig loads the configuration of private linters.
955-
// Private linters are dynamically loaded from .so plugin files.
956-
func (m Manager) loadCustomLinterConfig(name string, settings config.CustomLinterSettings) (*linter.Config, error) {
957-
analyzer, err := m.getAnalyzerPlugin(settings.Path)
958-
if err != nil {
959-
return nil, err
960-
}
961-
m.log.Infof("Loaded %s: %s", settings.Path, name)
962-
customLinter := goanalysis.NewLinter(
963-
name,
964-
settings.Description,
965-
analyzer.GetAnalyzers(),
966-
nil).WithLoadMode(goanalysis.LoadModeTypesInfo)
967-
968-
linterConfig := linter.NewConfig(customLinter).
969-
WithEnabledByDefault().
970-
WithLoadForGoAnalysis().
971-
WithURL(settings.OriginalURL)
972-
973-
return linterConfig, nil
974-
}
975-
976-
type AnalyzerPlugin interface {
977-
GetAnalyzers() []*analysis.Analyzer
978-
}
979-
980-
// getAnalyzerPlugin loads a private linter as specified in the config file,
981-
// loads the plugin from a .so file, and returns the 'AnalyzerPlugin' interface
982-
// implemented by the private plugin.
983-
// An error is returned if the private linter cannot be loaded or the linter
984-
// does not implement the AnalyzerPlugin interface.
985-
func (m Manager) getAnalyzerPlugin(path string) (AnalyzerPlugin, error) {
986-
if !filepath.IsAbs(path) {
987-
// resolve non-absolute paths relative to config file's directory
988-
configFilePath := viper.ConfigFileUsed()
989-
absConfigFilePath, err := filepath.Abs(configFilePath)
990-
if err != nil {
991-
return nil, fmt.Errorf("could not get absolute representation of config file path %q: %v", configFilePath, err)
992-
}
993-
path = filepath.Join(filepath.Dir(absConfigFilePath), path)
994-
}
995-
996-
plug, err := plugin.Open(path)
997-
if err != nil {
998-
return nil, err
999-
}
1000-
1001-
symbol, err := plug.Lookup("AnalyzerPlugin")
1002-
if err != nil {
1003-
return nil, err
1004-
}
1005-
1006-
analyzerPlugin, ok := symbol.(AnalyzerPlugin)
1007-
if !ok {
1008-
return nil, fmt.Errorf("plugin %s does not abide by 'AnalyzerPlugin' interface", path)
1009-
}
1010-
1011-
return analyzerPlugin, nil
1012-
}

0 commit comments

Comments
 (0)