Support custom linters integration by plugins

Co-authored-by: Isaev Denis <idenx@yandex.com>
This commit is contained in:
David Braley 2020-01-08 05:51:55 -05:00 committed by Isaev Denis
parent d3e36a97cd
commit be3c688da4
11 changed files with 223 additions and 13 deletions

View File

@ -230,6 +230,18 @@ linters-settings:
# Force newlines in end of case at this limit (0 = never). # Force newlines in end of case at this limit (0 = never).
force-case-trailing-whitespace: 0 force-case-trailing-whitespace: 0
# The custom section can be used to define linter plugins to be loaded at runtime. See README doc
# for more info.
custom:
# Each custom linter should have a unique name.
example:
# The path to the plugin *.so. Can be absolute or local. Required for each custom linter
path: /path/to/example.so
# The description of the linter. Optional, just for documentation purposes.
description: This is an example usage of a plugin linter.
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
original-url: github.com/golangci/example-linter
linters: linters:
enable: enable:
- megacheck - megacheck

View File

@ -108,4 +108,8 @@ go.sum: go.mod
vendor: go.mod go.sum vendor: go.mod go.sum
go mod vendor go mod vendor
.PHONY: vendor
unexport GOFLAGS
vendor_free_build: FORCE
go build -o golangci-lint ./cmd/golangci-lint
.PHONY: vendor_free_build vendor

View File

@ -834,6 +834,18 @@ linters-settings:
# Force newlines in end of case at this limit (0 = never). # Force newlines in end of case at this limit (0 = never).
force-case-trailing-whitespace: 0 force-case-trailing-whitespace: 0
# The custom section can be used to define linter plugins to be loaded at runtime. See README doc
# for more info.
custom:
# Each custom linter should have a unique name.
example:
# The path to the plugin *.so. Can be absolute or local. Required for each custom linter
path: /path/to/example.so
# The description of the linter. Optional, just for documentation purposes.
description: This is an example usage of a plugin linter.
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
original-url: github.com/golangci/example-linter
linters: linters:
enable: enable:
- megacheck - megacheck
@ -1026,6 +1038,58 @@ service:
- echo "here I can run custom commands, but no preparation needed for this repo" - echo "here I can run custom commands, but no preparation needed for this repo"
``` ```
## Custom Linters
Some people and organizations may choose to have custom made linters run as a part of golangci-lint. That functionality
is supported through go's plugin library.
### Create a Copy of `golangci-lint` that Can Run with Plugins
In order to use plugins, you'll need a golangci-lint executable that can run them. The normal version of this project
is built with the vendors option, which breaks plugins that have overlapping dependencies.
1. Download [golangci-lint](https://github.com/golangci/golangci-lint) source code
2. From the projects root directory, run `make vendor_free_build`
3. Copy the `golangci-lint` executable that was created to your path, project, or other location
### Configure Your Project for Linting
If you already have a linter plugin available, you can follow these steps to define it's usage in a projects
`.golangci.yml` file. An example linter can be found at [here](https://github.com/golangci/example-plugin-linter). If you're looking for
instructions on how to configure your own custom linter, they can be found further down.
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.
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
```
linters-settings:
custom:
example:
path: /example.so
description: The description of the linter
original-url: github.com/golangci/example-linter
```
That is all the configuration that is required to run a custom linter in your project. Custom linters are enabled by default,
but abide by the same rules as other linters. If the disable all option is specified either on command line or in
`.golang.yml` files `linters:disable-all: true`, custom linters will be disabled; they can be re-enabled by adding them
to the `linters:enable` list, or providing the enabled option on the command line, `golangci-lint run -Eexample`.
### To Create Your Own Custom Linter
Your linter must implement one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
Your project should also use `go.mod`. All versions of libraries that overlap `golangci-lint` (including replaced
libraries) MUST be set to the same version as `golangci-lint`. You can see the versions by running `go version -m golangci-lint`.
You'll also need to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define a
variable of name `AnalyzerPlugin`. The `AnalyzerPlugin` instance MUST implement the following interface:
```
type AnalyzerPlugin interface {
GetAnalyzers() []*analysis.Analyzer
}
```
The type of `AnalyzerPlugin` is not important, but is by convention `type analyzerPlugin struct {}`. See
[plugin/example.go](https://github.com/golangci/example-plugin-linter/plugin/example.go) for more info.
To build the plugin, from the root project directory, run `go build -buildmode=plugin plugin/example.go`. This will create a plugin `*.so`
file that can be copied into your project or another well known location for usage in golangci-lint.
## False Positives ## False Positives
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: 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:

View File

@ -455,6 +455,58 @@ than the default and have more strict settings:
{{.GolangciYaml}} {{.GolangciYaml}}
``` ```
## Custom Linters
Some people and organizations may choose to have custom made linters run as a part of golangci-lint. That functionality
is supported through go's plugin library.
### Create a Copy of `golangci-lint` that Can Run with Plugins
In order to use plugins, you'll need a golangci-lint executable that can run them. The normal version of this project
is built with the vendors option, which breaks plugins that have overlapping dependencies.
1. Download [golangci-lint](https://github.com/golangci/golangci-lint) source code
2. From the projects root directory, run `make vendor_free_build`
3. Copy the `golangci-lint` executable that was created to your path, project, or other location
### Configure Your Project for Linting
If you already have a linter plugin available, you can follow these steps to define it's usage in a projects
`.golangci.yml` file. An example linter can be found at [here](https://github.com/golangci/example-plugin-linter). If you're looking for
instructions on how to configure your own custom linter, they can be found further down.
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.
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
```
linters-settings:
custom:
example:
path: /example.so
description: The description of the linter
original-url: github.com/golangci/example-linter
```
That is all the configuration that is required to run a custom linter in your project. Custom linters are enabled by default,
but abide by the same rules as other linters. If the disable all option is specified either on command line or in
`.golang.yml` files `linters:disable-all: true`, custom linters will be disabled; they can be re-enabled by adding them
to the `linters:enable` list, or providing the enabled option on the command line, `golangci-lint run -Eexample`.
### To Create Your Own Custom Linter
Your linter must implement one or more `golang.org/x/tools/go/analysis.Analyzer` structs.
Your project should also use `go.mod`. All versions of libraries that overlap `golangci-lint` (including replaced
libraries) MUST be set to the same version as `golangci-lint`. You can see the versions by running `go version -m golangci-lint`.
You'll also need to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define a
variable of name `AnalyzerPlugin`. The `AnalyzerPlugin` instance MUST implement the following interface:
```
type AnalyzerPlugin interface {
GetAnalyzers() []*analysis.Analyzer
}
```
The type of `AnalyzerPlugin` is not important, but is by convention `type analyzerPlugin struct {}`. See
[plugin/example.go](https://github.com/golangci/example-plugin-linter/plugin/example.go) for more info.
To build the plugin, from the root project directory, run `go build -buildmode=plugin plugin/example.go`. This will create a plugin `*.so`
file that can be copied into your project or another well known location for usage in golangci-lint.
## False Positives ## False Positives
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: 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:

View File

@ -60,7 +60,7 @@ func NewExecutor(version, commit, date string) *Executor {
version: version, version: version,
commit: commit, commit: commit,
date: date, date: date,
DBManager: lintersdb.NewManager(nil), DBManager: lintersdb.NewManager(nil, nil),
debugf: logutils.Debug("exec"), debugf: logutils.Debug("exec"),
} }
@ -112,7 +112,7 @@ func NewExecutor(version, commit, date string) *Executor {
} }
// recreate after getting config // recreate after getting config
e.DBManager = lintersdb.NewManager(e.cfg) e.DBManager = lintersdb.NewManager(e.cfg, e.log).WithCustomLinters()
e.cfg.LintersSettings.Gocritic.InferEnabledChecks(e.log) e.cfg.LintersSettings.Gocritic.InferEnabledChecks(e.log)
if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil { if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil {

View File

@ -190,6 +190,8 @@ type LintersSettings struct {
Godox GodoxSettings Godox GodoxSettings
Dogsled DogsledSettings Dogsled DogsledSettings
Gocognit GocognitSettings Gocognit GocognitSettings
Custom map[string]CustomLinterSettings
} }
type GovetSettings struct { type GovetSettings struct {
@ -301,6 +303,12 @@ var defaultLintersSettings = LintersSettings{
}, },
} }
type CustomLinterSettings struct {
Path string
Description string
OriginalURL string `mapstructure:"original-url"`
}
type Linters struct { type Linters struct {
Enable []string Enable []string
Disable []string Disable []string

View File

@ -91,7 +91,7 @@ func TestGetEnabledLintersSet(t *testing.T) {
}, },
} }
m := NewManager(nil) m := NewManager(nil, nil)
es := NewEnabledSet(m, NewValidator(m), nil, nil) es := NewEnabledSet(m, NewValidator(m), nil, nil)
for _, c := range cases { for _, c := range cases {
c := c c := c

View File

@ -1,20 +1,28 @@
package lintersdb package lintersdb
import ( import (
"fmt"
"os" "os"
"plugin"
"golang.org/x/tools/go/analysis"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/golinters" "github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
"github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
) )
type Manager struct { type Manager struct {
nameToLCs map[string][]*linter.Config nameToLCs map[string][]*linter.Config
cfg *config.Config cfg *config.Config
log logutils.Log
} }
func NewManager(cfg *config.Config) *Manager { func NewManager(cfg *config.Config, log logutils.Log) *Manager {
m := &Manager{cfg: cfg} m := &Manager{cfg: cfg, log: log}
nameToLCs := make(map[string][]*linter.Config) nameToLCs := make(map[string][]*linter.Config)
for _, lc := range m.GetAllSupportedLinterConfigs() { for _, lc := range m.GetAllSupportedLinterConfigs() {
for _, name := range lc.AllNames() { for _, name := range lc.AllNames() {
@ -26,6 +34,27 @@ func NewManager(cfg *config.Config) *Manager {
return m return m
} }
func (m *Manager) WithCustomLinters() *Manager {
if m.log == nil {
m.log = report.NewLogWrapper(logutils.NewStderrLog(""), &report.Data{})
}
if m.cfg != nil {
for name, settings := range m.cfg.LintersSettings.Custom {
lc, err := m.loadCustomLinterConfig(name, settings)
if err != nil {
m.log.Errorf("Unable to load custom analyzer %s:%s, %v",
name,
settings.Path,
err)
} else {
m.nameToLCs[name] = append(m.nameToLCs[name], lc)
}
}
}
return m
}
func (Manager) AllPresets() []string { func (Manager) AllPresets() []string {
return []string{linter.PresetBugs, linter.PresetComplexity, linter.PresetFormatting, return []string{linter.PresetBugs, linter.PresetComplexity, linter.PresetFormatting,
linter.PresetPerformance, linter.PresetStyle, linter.PresetUnused} linter.PresetPerformance, linter.PresetStyle, linter.PresetUnused}
@ -267,3 +296,44 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {
return ret return ret
} }
func (m Manager) loadCustomLinterConfig(name string, settings config.CustomLinterSettings) (*linter.Config, error) {
analyzer, err := m.getAnalyzerPlugin(settings.Path)
if err != nil {
return nil, err
}
m.log.Infof("Loaded %s: %s", settings.Path, name)
customLinter := goanalysis.NewLinter(
name,
settings.Description,
analyzer.GetAnalyzers(),
nil).WithLoadMode(goanalysis.LoadModeTypesInfo)
linterConfig := linter.NewConfig(customLinter)
linterConfig.EnabledByDefault = true
linterConfig.IsSlow = false
linterConfig.WithURL(settings.OriginalURL)
return linterConfig, nil
}
type AnalyzerPlugin interface {
GetAnalyzers() []*analysis.Analyzer
}
func (m Manager) getAnalyzerPlugin(path string) (AnalyzerPlugin, error) {
plug, err := plugin.Open(path)
if err != nil {
return nil, err
}
symbol, err := plug.Lookup("AnalyzerPlugin")
if err != nil {
return nil, err
}
analyzerPlugin, ok := symbol.(AnalyzerPlugin)
if !ok {
return nil, fmt.Errorf("plugin %s does not abide by 'AnalyzerPlugin' interface", path)
}
return analyzerPlugin, nil
}

View File

@ -31,7 +31,7 @@ func newNolint2FileIssue(line int) result.Issue {
} }
func newTestNolintProcessor(log logutils.Log) *Nolint { func newTestNolintProcessor(log logutils.Log) *Nolint {
return NewNolint(log, lintersdb.NewManager(nil)) return NewNolint(log, lintersdb.NewManager(nil, nil))
} }
func getMockLog() *logutils.MockLog { func getMockLog() *logutils.MockLog {

View File

@ -114,7 +114,7 @@ func buildTemplateContext() (map[string]interface{}, error) {
func getLintersListMarkdown(enabled bool) string { func getLintersListMarkdown(enabled bool) string {
var neededLcs []*linter.Config var neededLcs []*linter.Config
lcs := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs() lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
for _, lc := range lcs { for _, lc := range lcs {
if lc.EnabledByDefault == enabled { if lc.EnabledByDefault == enabled {
neededLcs = append(neededLcs, lc) neededLcs = append(neededLcs, lc)
@ -139,7 +139,7 @@ func getLintersListMarkdown(enabled bool) string {
func getThanksList() string { func getThanksList() string {
var lines []string var lines []string
addedAuthors := map[string]bool{} addedAuthors := map[string]bool{}
for _, lc := range lintersdb.NewManager(nil).GetAllSupportedLinterConfigs() { for _, lc := range lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() {
if lc.OriginalURL == "" { if lc.OriginalURL == "" {
continue continue
} }

View File

@ -21,7 +21,7 @@ func inSlice(s []string, v string) bool {
} }
func getEnabledByDefaultFastLintersExcept(except ...string) []string { func getEnabledByDefaultFastLintersExcept(except ...string) []string {
m := lintersdb.NewManager(nil) m := lintersdb.NewManager(nil, nil)
ebdl := m.GetAllEnabledByDefaultLinters() ebdl := m.GetAllEnabledByDefaultLinters()
ret := []string{} ret := []string{}
for _, lc := range ebdl { for _, lc := range ebdl {
@ -38,7 +38,7 @@ func getEnabledByDefaultFastLintersExcept(except ...string) []string {
} }
func getAllFastLintersWith(with ...string) []string { func getAllFastLintersWith(with ...string) []string {
linters := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs() linters := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
ret := append([]string{}, with...) ret := append([]string{}, with...)
for _, lc := range linters { for _, lc := range linters {
if lc.IsSlowLinter() { if lc.IsSlowLinter() {
@ -51,7 +51,7 @@ func getAllFastLintersWith(with ...string) []string {
} }
func getEnabledByDefaultLinters() []string { func getEnabledByDefaultLinters() []string {
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters() ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
ret := []string{} ret := []string{}
for _, lc := range ebdl { for _, lc := range ebdl {
ret = append(ret, lc.Name()) ret = append(ret, lc.Name())
@ -61,7 +61,7 @@ func getEnabledByDefaultLinters() []string {
} }
func getEnabledByDefaultFastLintersWith(with ...string) []string { func getEnabledByDefaultFastLintersWith(with ...string) []string {
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters() ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
ret := append([]string{}, with...) ret := append([]string{}, with...)
for _, lc := range ebdl { for _, lc := range ebdl {
if lc.IsSlowLinter() { if lc.IsSlowLinter() {