diff --git a/.golangci.example.yml b/.golangci.example.yml index fc99b707..222d46ac 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -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 diff --git a/Makefile b/Makefile index e253dea0..825332da 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index d6dbe3fe..fe7ca20e 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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: diff --git a/README.tmpl.md b/README.tmpl.md index 3c279063..0432b729 100644 --- a/README.tmpl.md +++ b/README.tmpl.md @@ -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: diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go index 0089bc68..fe598013 100644 --- a/pkg/commands/executor.go +++ b/pkg/commands/executor.go @@ -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"), } @@ -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 { diff --git a/pkg/config/config.go b/pkg/config/config.go index 2ddfc60b..c1db0328 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -190,6 +190,8 @@ type LintersSettings struct { Godox GodoxSettings Dogsled DogsledSettings Gocognit GocognitSettings + + Custom map[string]CustomLinterSettings } 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 { Enable []string Disable []string diff --git a/pkg/lint/lintersdb/enabled_set_test.go b/pkg/lint/lintersdb/enabled_set_test.go index b2eaf383..f9b6393f 100644 --- a/pkg/lint/lintersdb/enabled_set_test.go +++ b/pkg/lint/lintersdb/enabled_set_test.go @@ -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 diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index a40c1ba1..9818b9f5 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/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() { @@ -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} @@ -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 +} diff --git a/pkg/result/processors/nolint_test.go b/pkg/result/processors/nolint_test.go index a9f245fe..b547fbd0 100644 --- a/pkg/result/processors/nolint_test.go +++ b/pkg/result/processors/nolint_test.go @@ -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 { diff --git a/scripts/gen_readme/main.go b/scripts/gen_readme/main.go index e3b30a3b..35858286 100644 --- a/scripts/gen_readme/main.go +++ b/scripts/gen_readme/main.go @@ -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) @@ -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 } diff --git a/test/enabled_linters_test.go b/test/enabled_linters_test.go index 436d42b2..0ade56b0 100644 --- a/test/enabled_linters_test.go +++ b/test/enabled_linters_test.go @@ -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 { @@ -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() { @@ -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()) @@ -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() {