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 9 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
15 changes: 15 additions & 0 deletions .golangci.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,21 @@ linters-settings:
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false

# 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 name. This name should match with what the linter reports for itself.
example:
# The path to the plugin *.so. Required for each custom linter
path: /path/to/example.so
# If the linter should be enabled by default. Options in the `linters`
# section override this, including `disable-all`. Optional, false by default.
enabled: true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why duplicate it with linters section? It looks difficult to understand for users.

Copy link
Contributor Author

@dbraley dbraley Nov 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. There are a couple of ways to reduce that duplication. The easiest and I think most consistent is to set the linter to enabled by default and remove this option. That does leave the 'problem' that if the user has linters:disable-all: true in the linters section, that will override enable by default even for the custom linters. Thus, the user will be forced to define the linter again in linters:enable. But that also preserves the ability to turn on and off custom linters on the command line.

# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
original-url: github.com/dbraley/example-linter
# Indicates if this linter should be considered slow. Optional, false by default.
slow: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please, remove it for simplification. Linters with go/analysis interface can't be slow by current rules. The only slow linter is unused.

Copy link
Contributor Author

@dbraley dbraley Nov 11, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove it from the documentation with ease. Actually getting the code to ignore it could be slightly more problematic. But I think for what you're asking just removing the documentation of it should suffice, yes? edit: NM, was misremembering how this worked. That's easy to implement, will do.


linters:
enable:
- megacheck
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,8 @@ go.sum: go.mod
.PHONY: vendor
vendor: go.mod go.sum
go mod vendor

unexport GOFLAGS
vendor_free_build: FORCE
go build -o golangci-lint ./cmd/golangci-lint
.PHONY: vendor_free_build
75 changes: 75 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,21 @@ linters-settings:
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false

# 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 name. This name should match with what the linter reports for itself.
example:
# The path to the plugin *.so. Required for each custom linter
path: /path/to/example.so
# If the linter should be enabled by default. Options in the `linters`
# section override this, including `disable-all`. Optional, false by default.
enabled: true
# Intended to point to the repo location of the linter. Optional, just for documentation purposes.
original-url: github.com/dbraley/example-linter
# Indicates if this linter should be considered slow. Optional, false by default.
slow: false

linters:
enable:
- megacheck
Expand Down Expand Up @@ -1027,6 +1042,66 @@ 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 the `i473` branch of https://github.com/dbraley/golangci-lint/tree/i473
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're going to merge it. Let's fix description.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, sorry, wanted the documentation to be accurate during the review. Will fix now.

2. From that 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 https://github.com/dbraley/example-linter. If you're looking for
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please point to github.com/golangci/example-plugin-linter. I will fork your repo into it.

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 https://github.com/dbraley/golangci-lint/blob/i473/.golangci.yml to the root directory.
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
```
linters-settings:
custom:
example: # If this doesn't match the linters name definition, it will warn you but still run
path: /example.so # Adjust this the location of the plugin
enabled: true # This determines if the linter is run by default
original-url: github.com/dbraley/example-linter # This is just documentation for custom linters
slow: false # Set this to true to observe `--fast` option
```
3. If your `.golangci.yml` file has `linters:disable-all` set to true, you'll also need to add your linter to the `linters:enable` seciont:
```
linters:
enable:
# ...
- varcheck
- whitespace
# Custom linters
- example
```
4. Alternately, you can leave it disabled and turn it on via command line only: `golangci-lint run -Eexample`
5. If everything works correctly, you should see the linter show up as enabled by running `golangci-lint linters`. This linter will look for `// TODO: ` in front of functions, so you can also add that to your code and see the errors.
6. You can also see the linter get loaded with the `-v` option for either `run` or `linters`

### To Configure Your Own `golang.org/x/tools/go/analysis` Based Linter

Your Linter must implement one or more `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 want to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define:
1. A variable of type `analyzerPlugin`. The type `analyzerPlugin` can be defined as a string, struct, whatever.
```
type analyzerPlugin struct{}
var AnalyzerPlugin analyzerPlugin
```
2. A function with signature `func (*analyzerPlugin) GetLinterName() string`
3. A function with signature `func (*analyzerPlugin) GetLinterDesc() string`
4. A function with signature `func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer`

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
60 changes: 60 additions & 0 deletions README.tmpl.md
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,66 @@ 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 the `i473` branch of https://github.com/dbraley/golangci-lint/tree/i473
2. From that 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 https://github.com/dbraley/example-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 https://github.com/dbraley/golangci-lint/blob/i473/.golangci.yml to the root directory.
2. Adjust the yaml to appropriate `linters-settings:custom` entries as so:
```
linters-settings:
custom:
example: # If this doesn't match the linters name definition, it will warn you but still run
path: /example.so # Adjust this the location of the plugin
enabled: true # This determines if the linter is run by default
original-url: github.com/dbraley/example-linter # This is just documentation for custom linters
slow: false # Set this to true to observe `--fast` option
```
3. If your `.golangci.yml` file has `linters:disable-all` set to true, you'll also need to add your linter to the `linters:enable` seciont:
```
linters:
enable:
# ...
- varcheck
- whitespace
# Custom linters
- example
```
4. Alternately, you can leave it disabled and turn it on via command line only: `golangci-lint run -Eexample`
5. If everything works correctly, you should see the linter show up as enabled by running `golangci-lint linters`. This linter will look for `// TODO: ` in front of functions, so you can also add that to your code and see the errors.
6. You can also see the linter get loaded with the `-v` option for either `run` or `linters`

### To Configure Your Own `golang.org/x/tools/go/analysis` Based Linter

Your Linter must implement one or more `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 want to create a go file like `plugin/example.go`. This MUST be in the package `main`, and define:
1. A variable of type `analyzerPlugin`. The type `analyzerPlugin` can be defined as a string, struct, whatever.
```
type analyzerPlugin struct{}
var AnalyzerPlugin analyzerPlugin
```
2. A function with signature `func (*analyzerPlugin) GetLinterName() string`
3. A function with signature `func (*analyzerPlugin) GetLinterDesc() string`
4. A function with signature `func (*analyzerPlugin) GetAnalyzers() []*analysis.Analyzer`

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 @@ -62,7 +62,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 @@ -114,7 +114,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
9 changes: 9 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 @@ -299,6 +301,13 @@ var defaultLintersSettings = LintersSettings{
},
}

type CustomLinterSettings struct {
Path string
Enabled bool
OriginalUrl string `mapstructure:"original-url"`
Slow bool
}

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
80 changes: 78 additions & 2 deletions pkg/lint/lintersdb/manager.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package lintersdb

import (
"fmt"
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/prometheus/common/log"
"golang.org/x/tools/go/analysis"
"os"
"plugin"

"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/golinters"
Expand All @@ -11,10 +18,11 @@ import (
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 {
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 @@ -264,3 +293,50 @@ 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
} else {
m.log.Infof("Loaded %s: %s", settings.Path, analyzer.GetLinterName())
customLinter := goanalysis.NewLinter(
analyzer.GetLinterName(),
analyzer.GetLinterDesc(),
analyzer.GetAnalyzers(),
nil).WithLoadMode(goanalysis.LoadModeTypesInfo)
linterConfig := linter.NewConfig(customLinter)
linterConfig.EnabledByDefault = settings.Enabled
linterConfig.IsSlow = settings.Slow
linterConfig.WithURL(settings.OriginalUrl)
if name != linterConfig.Name() {
m.log.Warnf("Configuration linter name %s doesn't match plugin linter name %s", name, linterConfig.Name())
}
return linterConfig, nil
}
}

type AnalyzerPlugin interface {
GetLinterName() string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name is already in the config, why duplicate it?
also, maybe we can move description to config to simplify things for linters authors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, yeah, I can get onboard with that. Sounds good!

GetLinterDesc() string
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
Loading