-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Go plugin system #841
Changes from 9 commits
a71b4d4
4be5882
b835e43
f86a8e2
b9c6f81
c45ef5a
bd80105
f2fc003
a70cc0e
2b444e6
f82a639
7e0a420
1cd09ac
329c6e0
ba0c6dd
220d964
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
# 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please, remove it for simplification. Linters with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can remove it from the documentation with ease. |
||
|
||
linters: | ||
enable: | ||
- megacheck | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're going to merge it. Let's fix description. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please point to |
||
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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,14 @@ | ||
package lintersdb | ||
|
||
import ( | ||
"fmt" | ||
dbraley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"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" | ||
dbraley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"os" | ||
"plugin" | ||
|
||
"github.com/golangci/golangci-lint/pkg/config" | ||
"github.com/golangci/golangci-lint/pkg/golinters" | ||
|
@@ -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 { | ||
dbraley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
m := &Manager{cfg: cfg, log: log} | ||
nameToLCs := make(map[string][]*linter.Config) | ||
for _, lc := range m.GetAllSupportedLinterConfigs() { | ||
for _, name := range lc.AllNames() { | ||
|
@@ -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} | ||
|
@@ -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 { | ||
dbraley marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
There was a problem hiding this comment.
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.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 inlinters:enable
. But that also preserves the ability to turn on and off custom linters on the command line.