Skip to content

Commit be3c688

Browse files
dbraleyjirfag
andcommitted
I473 (#841)
Support custom linters integration by plugins Co-authored-by: Isaev Denis <[email protected]>
1 parent d3e36a9 commit be3c688

File tree

11 files changed

+223
-13
lines changed

11 files changed

+223
-13
lines changed

.golangci.example.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,18 @@ linters-settings:
230230
# Force newlines in end of case at this limit (0 = never).
231231
force-case-trailing-whitespace: 0
232232

233+
# The custom section can be used to define linter plugins to be loaded at runtime. See README doc
234+
# for more info.
235+
custom:
236+
# Each custom linter should have a unique name.
237+
example:
238+
# The path to the plugin *.so. Can be absolute or local. Required for each custom linter
239+
path: /path/to/example.so
240+
# The description of the linter. Optional, just for documentation purposes.
241+
description: This is an example usage of a plugin linter.
242+
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
243+
original-url: github.com/golangci/example-linter
244+
233245
linters:
234246
enable:
235247
- megacheck

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,8 @@ go.sum: go.mod
108108

109109
vendor: go.mod go.sum
110110
go mod vendor
111-
.PHONY: vendor
111+
112+
unexport GOFLAGS
113+
vendor_free_build: FORCE
114+
go build -o golangci-lint ./cmd/golangci-lint
115+
.PHONY: vendor_free_build vendor

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,18 @@ linters-settings:
834834
# Force newlines in end of case at this limit (0 = never).
835835
force-case-trailing-whitespace: 0
836836
837+
# The custom section can be used to define linter plugins to be loaded at runtime. See README doc
838+
# for more info.
839+
custom:
840+
# Each custom linter should have a unique name.
841+
example:
842+
# The path to the plugin *.so. Can be absolute or local. Required for each custom linter
843+
path: /path/to/example.so
844+
# The description of the linter. Optional, just for documentation purposes.
845+
description: This is an example usage of a plugin linter.
846+
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
847+
original-url: github.com/golangci/example-linter
848+
837849
linters:
838850
enable:
839851
- megacheck
@@ -1026,6 +1038,58 @@ service:
10261038
- echo "here I can run custom commands, but no preparation needed for this repo"
10271039
```
10281040
1041+
## Custom Linters
1042+
Some people and organizations may choose to have custom made linters run as a part of golangci-lint. That functionality
1043+
is supported through go's plugin library.
1044+
1045+
### Create a Copy of `golangci-lint` that Can Run with Plugins
1046+
In order to use plugins, you'll need a golangci-lint executable that can run them. The normal version of this project
1047+
is built with the vendors option, which breaks plugins that have overlapping dependencies.
1048+
1049+
1. Download [golangci-lint](https://github.com/golangci/golangci-lint) source code
1050+
2. From the projects root directory, run `make vendor_free_build`
1051+
3. Copy the `golangci-lint` executable that was created to your path, project, or other location
1052+
1053+
### Configure Your Project for Linting
1054+
If you already have a linter plugin available, you can follow these steps to define it's usage in a projects
1055+
`.golangci.yml` file. An example linter can be found at [here](https://github.com/golangci/example-plugin-linter). If you're looking for
1056+
instructions on how to configure your own custom linter, they can be found further down.
1057+
1058+
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.
1059+
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
1060+
```
1061+
linters-settings:
1062+
custom:
1063+
example:
1064+
path: /example.so
1065+
description: The description of the linter
1066+
original-url: github.com/golangci/example-linter
1067+
```
1068+
1069+
That is all the configuration that is required to run a custom linter in your project. Custom linters are enabled by default,
1070+
but abide by the same rules as other linters. If the disable all option is specified either on command line or in
1071+
`.golang.yml` files `linters:disable-all: true`, custom linters will be disabled; they can be re-enabled by adding them
1072+
to the `linters:enable` list, or providing the enabled option on the command line, `golangci-lint run -Eexample`.
1073+
1074+
### To Create Your Own Custom Linter
1075+
1076+
Your linter must implement one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
1077+
Your project should also use `go.mod`. All versions of libraries that overlap `golangci-lint` (including replaced
1078+
libraries) MUST be set to the same version as `golangci-lint`. You can see the versions by running `go version -m golangci-lint`.
1079+
1080+
You'll also need to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define a
1081+
variable of name `AnalyzerPlugin`. The `AnalyzerPlugin` instance MUST implement the following interface:
1082+
```
1083+
type AnalyzerPlugin interface {
1084+
GetAnalyzers() []*analysis.Analyzer
1085+
}
1086+
```
1087+
The type of `AnalyzerPlugin` is not important, but is by convention `type analyzerPlugin struct {}`. See
1088+
[plugin/example.go](https://github.com/golangci/example-plugin-linter/plugin/example.go) for more info.
1089+
1090+
To build the plugin, from the root project directory, run `go build -buildmode=plugin plugin/example.go`. This will create a plugin `*.so`
1091+
file that can be copied into your project or another well known location for usage in golangci-lint.
1092+
10291093
## False Positives
10301094
10311095
False positives are inevitable, but we did our best to reduce their count. For example, we have a default enabled set of [exclude patterns](#command-line-options). If a false positive occurred you have the following choices:

README.tmpl.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,58 @@ than the default and have more strict settings:
455455
{{.GolangciYaml}}
456456
```
457457

458+
## Custom Linters
459+
Some people and organizations may choose to have custom made linters run as a part of golangci-lint. That functionality
460+
is supported through go's plugin library.
461+
462+
### Create a Copy of `golangci-lint` that Can Run with Plugins
463+
In order to use plugins, you'll need a golangci-lint executable that can run them. The normal version of this project
464+
is built with the vendors option, which breaks plugins that have overlapping dependencies.
465+
466+
1. Download [golangci-lint](https://github.com/golangci/golangci-lint) source code
467+
2. From the projects root directory, run `make vendor_free_build`
468+
3. Copy the `golangci-lint` executable that was created to your path, project, or other location
469+
470+
### Configure Your Project for Linting
471+
If you already have a linter plugin available, you can follow these steps to define it's usage in a projects
472+
`.golangci.yml` file. An example linter can be found at [here](https://github.com/golangci/example-plugin-linter). If you're looking for
473+
instructions on how to configure your own custom linter, they can be found further down.
474+
475+
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.
476+
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
477+
```
478+
linters-settings:
479+
custom:
480+
example:
481+
path: /example.so
482+
description: The description of the linter
483+
original-url: github.com/golangci/example-linter
484+
```
485+
486+
That is all the configuration that is required to run a custom linter in your project. Custom linters are enabled by default,
487+
but abide by the same rules as other linters. If the disable all option is specified either on command line or in
488+
`.golang.yml` files `linters:disable-all: true`, custom linters will be disabled; they can be re-enabled by adding them
489+
to the `linters:enable` list, or providing the enabled option on the command line, `golangci-lint run -Eexample`.
490+
491+
### To Create Your Own Custom Linter
492+
493+
Your linter must implement one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
494+
Your project should also use `go.mod`. All versions of libraries that overlap `golangci-lint` (including replaced
495+
libraries) MUST be set to the same version as `golangci-lint`. You can see the versions by running `go version -m golangci-lint`.
496+
497+
You'll also need to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define a
498+
variable of name `AnalyzerPlugin`. The `AnalyzerPlugin` instance MUST implement the following interface:
499+
```
500+
type AnalyzerPlugin interface {
501+
GetAnalyzers() []*analysis.Analyzer
502+
}
503+
```
504+
The type of `AnalyzerPlugin` is not important, but is by convention `type analyzerPlugin struct {}`. See
505+
[plugin/example.go](https://github.com/golangci/example-plugin-linter/plugin/example.go) for more info.
506+
507+
To build the plugin, from the root project directory, run `go build -buildmode=plugin plugin/example.go`. This will create a plugin `*.so`
508+
file that can be copied into your project or another well known location for usage in golangci-lint.
509+
458510
## False Positives
459511

460512
False positives are inevitable, but we did our best to reduce their count. For example, we have a default enabled set of [exclude patterns](#command-line-options). If a false positive occurred you have the following choices:

pkg/commands/executor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func NewExecutor(version, commit, date string) *Executor {
6060
version: version,
6161
commit: commit,
6262
date: date,
63-
DBManager: lintersdb.NewManager(nil),
63+
DBManager: lintersdb.NewManager(nil, nil),
6464
debugf: logutils.Debug("exec"),
6565
}
6666

@@ -112,7 +112,7 @@ func NewExecutor(version, commit, date string) *Executor {
112112
}
113113

114114
// recreate after getting config
115-
e.DBManager = lintersdb.NewManager(e.cfg)
115+
e.DBManager = lintersdb.NewManager(e.cfg, e.log).WithCustomLinters()
116116

117117
e.cfg.LintersSettings.Gocritic.InferEnabledChecks(e.log)
118118
if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil {

pkg/config/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ type LintersSettings struct {
190190
Godox GodoxSettings
191191
Dogsled DogsledSettings
192192
Gocognit GocognitSettings
193+
194+
Custom map[string]CustomLinterSettings
193195
}
194196

195197
type GovetSettings struct {
@@ -301,6 +303,12 @@ var defaultLintersSettings = LintersSettings{
301303
},
302304
}
303305

306+
type CustomLinterSettings struct {
307+
Path string
308+
Description string
309+
OriginalURL string `mapstructure:"original-url"`
310+
}
311+
304312
type Linters struct {
305313
Enable []string
306314
Disable []string

pkg/lint/lintersdb/enabled_set_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ func TestGetEnabledLintersSet(t *testing.T) {
9191
},
9292
}
9393

94-
m := NewManager(nil)
94+
m := NewManager(nil, nil)
9595
es := NewEnabledSet(m, NewValidator(m), nil, nil)
9696
for _, c := range cases {
9797
c := c

pkg/lint/lintersdb/manager.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
11
package lintersdb
22

33
import (
4+
"fmt"
45
"os"
6+
"plugin"
7+
8+
"golang.org/x/tools/go/analysis"
59

610
"github.com/golangci/golangci-lint/pkg/config"
711
"github.com/golangci/golangci-lint/pkg/golinters"
12+
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
813
"github.com/golangci/golangci-lint/pkg/lint/linter"
14+
"github.com/golangci/golangci-lint/pkg/logutils"
15+
"github.com/golangci/golangci-lint/pkg/report"
916
)
1017

1118
type Manager struct {
1219
nameToLCs map[string][]*linter.Config
1320
cfg *config.Config
21+
log logutils.Log
1422
}
1523

16-
func NewManager(cfg *config.Config) *Manager {
17-
m := &Manager{cfg: cfg}
24+
func NewManager(cfg *config.Config, log logutils.Log) *Manager {
25+
m := &Manager{cfg: cfg, log: log}
1826
nameToLCs := make(map[string][]*linter.Config)
1927
for _, lc := range m.GetAllSupportedLinterConfigs() {
2028
for _, name := range lc.AllNames() {
@@ -26,6 +34,27 @@ func NewManager(cfg *config.Config) *Manager {
2634
return m
2735
}
2836

37+
func (m *Manager) WithCustomLinters() *Manager {
38+
if m.log == nil {
39+
m.log = report.NewLogWrapper(logutils.NewStderrLog(""), &report.Data{})
40+
}
41+
if m.cfg != nil {
42+
for name, settings := range m.cfg.LintersSettings.Custom {
43+
lc, err := m.loadCustomLinterConfig(name, settings)
44+
45+
if err != nil {
46+
m.log.Errorf("Unable to load custom analyzer %s:%s, %v",
47+
name,
48+
settings.Path,
49+
err)
50+
} else {
51+
m.nameToLCs[name] = append(m.nameToLCs[name], lc)
52+
}
53+
}
54+
}
55+
return m
56+
}
57+
2958
func (Manager) AllPresets() []string {
3059
return []string{linter.PresetBugs, linter.PresetComplexity, linter.PresetFormatting,
3160
linter.PresetPerformance, linter.PresetStyle, linter.PresetUnused}
@@ -267,3 +296,44 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {
267296

268297
return ret
269298
}
299+
300+
func (m Manager) loadCustomLinterConfig(name string, settings config.CustomLinterSettings) (*linter.Config, error) {
301+
analyzer, err := m.getAnalyzerPlugin(settings.Path)
302+
if err != nil {
303+
return nil, err
304+
}
305+
m.log.Infof("Loaded %s: %s", settings.Path, name)
306+
customLinter := goanalysis.NewLinter(
307+
name,
308+
settings.Description,
309+
analyzer.GetAnalyzers(),
310+
nil).WithLoadMode(goanalysis.LoadModeTypesInfo)
311+
linterConfig := linter.NewConfig(customLinter)
312+
linterConfig.EnabledByDefault = true
313+
linterConfig.IsSlow = false
314+
linterConfig.WithURL(settings.OriginalURL)
315+
return linterConfig, nil
316+
}
317+
318+
type AnalyzerPlugin interface {
319+
GetAnalyzers() []*analysis.Analyzer
320+
}
321+
322+
func (m Manager) getAnalyzerPlugin(path string) (AnalyzerPlugin, error) {
323+
plug, err := plugin.Open(path)
324+
if err != nil {
325+
return nil, err
326+
}
327+
328+
symbol, err := plug.Lookup("AnalyzerPlugin")
329+
if err != nil {
330+
return nil, err
331+
}
332+
333+
analyzerPlugin, ok := symbol.(AnalyzerPlugin)
334+
if !ok {
335+
return nil, fmt.Errorf("plugin %s does not abide by 'AnalyzerPlugin' interface", path)
336+
}
337+
338+
return analyzerPlugin, nil
339+
}

pkg/result/processors/nolint_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func newNolint2FileIssue(line int) result.Issue {
3131
}
3232

3333
func newTestNolintProcessor(log logutils.Log) *Nolint {
34-
return NewNolint(log, lintersdb.NewManager(nil))
34+
return NewNolint(log, lintersdb.NewManager(nil, nil))
3535
}
3636

3737
func getMockLog() *logutils.MockLog {

scripts/gen_readme/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func buildTemplateContext() (map[string]interface{}, error) {
114114

115115
func getLintersListMarkdown(enabled bool) string {
116116
var neededLcs []*linter.Config
117-
lcs := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs()
117+
lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
118118
for _, lc := range lcs {
119119
if lc.EnabledByDefault == enabled {
120120
neededLcs = append(neededLcs, lc)
@@ -139,7 +139,7 @@ func getLintersListMarkdown(enabled bool) string {
139139
func getThanksList() string {
140140
var lines []string
141141
addedAuthors := map[string]bool{}
142-
for _, lc := range lintersdb.NewManager(nil).GetAllSupportedLinterConfigs() {
142+
for _, lc := range lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() {
143143
if lc.OriginalURL == "" {
144144
continue
145145
}

test/enabled_linters_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func inSlice(s []string, v string) bool {
2121
}
2222

2323
func getEnabledByDefaultFastLintersExcept(except ...string) []string {
24-
m := lintersdb.NewManager(nil)
24+
m := lintersdb.NewManager(nil, nil)
2525
ebdl := m.GetAllEnabledByDefaultLinters()
2626
ret := []string{}
2727
for _, lc := range ebdl {
@@ -38,7 +38,7 @@ func getEnabledByDefaultFastLintersExcept(except ...string) []string {
3838
}
3939

4040
func getAllFastLintersWith(with ...string) []string {
41-
linters := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs()
41+
linters := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
4242
ret := append([]string{}, with...)
4343
for _, lc := range linters {
4444
if lc.IsSlowLinter() {
@@ -51,7 +51,7 @@ func getAllFastLintersWith(with ...string) []string {
5151
}
5252

5353
func getEnabledByDefaultLinters() []string {
54-
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters()
54+
ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
5555
ret := []string{}
5656
for _, lc := range ebdl {
5757
ret = append(ret, lc.Name())
@@ -61,7 +61,7 @@ func getEnabledByDefaultLinters() []string {
6161
}
6262

6363
func getEnabledByDefaultFastLintersWith(with ...string) []string {
64-
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters()
64+
ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
6565
ret := append([]string{}, with...)
6666
for _, lc := range ebdl {
6767
if lc.IsSlowLinter() {

0 commit comments

Comments
 (0)