Skip to content

Go plugin system #841

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jan 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .golangci.example.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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