Skip to content

Commit

Permalink
I473 (#841)
Browse files Browse the repository at this point in the history
Support custom linters integration by plugins

Co-authored-by: Isaev Denis <idenx@yandex.com>
  • Loading branch information
dbraley and jirfag committed Jan 8, 2020
1 parent d3e36a9 commit be3c688
Show file tree
Hide file tree
Showing 11 changed files with 223 additions and 13 deletions.
12 changes: 12 additions & 0 deletions .golangci.example.yml
Expand Up @@ -230,6 +230,18 @@ linters-settings:
# Force newlines in end of case at this limit (0 = never).
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:
enable:
- megacheck
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Expand Up @@ -108,4 +108,8 @@ go.sum: go.mod

vendor: go.mod go.sum
go mod vendor
.PHONY: vendor

unexport GOFLAGS
vendor_free_build: FORCE
go build -o golangci-lint ./cmd/golangci-lint
.PHONY: vendor_free_build vendor
64 changes: 64 additions & 0 deletions README.md
Expand Up @@ -834,6 +834,18 @@ linters-settings:
# Force newlines in end of case at this limit (0 = never).
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:
enable:
- megacheck
Expand Down Expand Up @@ -1026,6 +1038,58 @@ service:
- 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 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:
Expand Down
52 changes: 52 additions & 0 deletions README.tmpl.md
Expand Up @@ -455,6 +455,58 @@ than the default and have more strict settings:
{{.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 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:
Expand Down
4 changes: 2 additions & 2 deletions pkg/commands/executor.go
Expand Up @@ -60,7 +60,7 @@ func NewExecutor(version, commit, date string) *Executor {
version: version,
commit: commit,
date: date,
DBManager: lintersdb.NewManager(nil),
DBManager: lintersdb.NewManager(nil, nil),
debugf: logutils.Debug("exec"),
}

Expand Down Expand Up @@ -112,7 +112,7 @@ func NewExecutor(version, commit, date string) *Executor {
}

// 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)
if err = e.cfg.LintersSettings.Gocritic.Validate(e.log); err != nil {
Expand Down
8 changes: 8 additions & 0 deletions pkg/config/config.go
Expand Up @@ -190,6 +190,8 @@ type LintersSettings struct {
Godox GodoxSettings
Dogsled DogsledSettings
Gocognit GocognitSettings

Custom map[string]CustomLinterSettings
}

type GovetSettings struct {
Expand Down Expand Up @@ -301,6 +303,12 @@ var defaultLintersSettings = LintersSettings{
},
}

type CustomLinterSettings struct {
Path string
Description string
OriginalURL string `mapstructure:"original-url"`
}

type Linters struct {
Enable []string
Disable []string
Expand Down
2 changes: 1 addition & 1 deletion pkg/lint/lintersdb/enabled_set_test.go
Expand Up @@ -91,7 +91,7 @@ func TestGetEnabledLintersSet(t *testing.T) {
},
}

m := NewManager(nil)
m := NewManager(nil, nil)
es := NewEnabledSet(m, NewValidator(m), nil, nil)
for _, c := range cases {
c := c
Expand Down
74 changes: 72 additions & 2 deletions pkg/lint/lintersdb/manager.go
@@ -1,20 +1,28 @@
package lintersdb

import (
"fmt"
"os"
"plugin"

"golang.org/x/tools/go/analysis"

"github.com/golangci/golangci-lint/pkg/config"
"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/logutils"
"github.com/golangci/golangci-lint/pkg/report"
)

type Manager struct {
nameToLCs map[string][]*linter.Config
cfg *config.Config
log logutils.Log
}

func NewManager(cfg *config.Config) *Manager {
m := &Manager{cfg: cfg}
func NewManager(cfg *config.Config, log logutils.Log) *Manager {
m := &Manager{cfg: cfg, log: log}
nameToLCs := make(map[string][]*linter.Config)
for _, lc := range m.GetAllSupportedLinterConfigs() {
for _, name := range lc.AllNames() {
Expand All @@ -26,6 +34,27 @@ func NewManager(cfg *config.Config) *Manager {
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 {
return []string{linter.PresetBugs, linter.PresetComplexity, linter.PresetFormatting,
linter.PresetPerformance, linter.PresetStyle, linter.PresetUnused}
Expand Down Expand Up @@ -267,3 +296,44 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {

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
}
2 changes: 1 addition & 1 deletion pkg/result/processors/nolint_test.go
Expand Up @@ -31,7 +31,7 @@ func newNolint2FileIssue(line int) result.Issue {
}

func newTestNolintProcessor(log logutils.Log) *Nolint {
return NewNolint(log, lintersdb.NewManager(nil))
return NewNolint(log, lintersdb.NewManager(nil, nil))
}

func getMockLog() *logutils.MockLog {
Expand Down
4 changes: 2 additions & 2 deletions scripts/gen_readme/main.go
Expand Up @@ -114,7 +114,7 @@ func buildTemplateContext() (map[string]interface{}, error) {

func getLintersListMarkdown(enabled bool) string {
var neededLcs []*linter.Config
lcs := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs()
lcs := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
for _, lc := range lcs {
if lc.EnabledByDefault == enabled {
neededLcs = append(neededLcs, lc)
Expand All @@ -139,7 +139,7 @@ func getLintersListMarkdown(enabled bool) string {
func getThanksList() string {
var lines []string
addedAuthors := map[string]bool{}
for _, lc := range lintersdb.NewManager(nil).GetAllSupportedLinterConfigs() {
for _, lc := range lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs() {
if lc.OriginalURL == "" {
continue
}
Expand Down
8 changes: 4 additions & 4 deletions test/enabled_linters_test.go
Expand Up @@ -21,7 +21,7 @@ func inSlice(s []string, v string) bool {
}

func getEnabledByDefaultFastLintersExcept(except ...string) []string {
m := lintersdb.NewManager(nil)
m := lintersdb.NewManager(nil, nil)
ebdl := m.GetAllEnabledByDefaultLinters()
ret := []string{}
for _, lc := range ebdl {
Expand All @@ -38,7 +38,7 @@ func getEnabledByDefaultFastLintersExcept(except ...string) []string {
}

func getAllFastLintersWith(with ...string) []string {
linters := lintersdb.NewManager(nil).GetAllSupportedLinterConfigs()
linters := lintersdb.NewManager(nil, nil).GetAllSupportedLinterConfigs()
ret := append([]string{}, with...)
for _, lc := range linters {
if lc.IsSlowLinter() {
Expand All @@ -51,7 +51,7 @@ func getAllFastLintersWith(with ...string) []string {
}

func getEnabledByDefaultLinters() []string {
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters()
ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
ret := []string{}
for _, lc := range ebdl {
ret = append(ret, lc.Name())
Expand All @@ -61,7 +61,7 @@ func getEnabledByDefaultLinters() []string {
}

func getEnabledByDefaultFastLintersWith(with ...string) []string {
ebdl := lintersdb.NewManager(nil).GetAllEnabledByDefaultLinters()
ebdl := lintersdb.NewManager(nil, nil).GetAllEnabledByDefaultLinters()
ret := append([]string{}, with...)
for _, lc := range ebdl {
if lc.IsSlowLinter() {
Expand Down

0 comments on commit be3c688

Please sign in to comment.