diff --git a/docs/src/docs/contributing/new-linters.mdx b/docs/src/docs/contributing/new-linters.mdx index 6bc875c8403c..5c4a8c82b566 100644 --- a/docs/src/docs/contributing/new-linters.mdx +++ b/docs/src/docs/contributing/new-linters.mdx @@ -4,8 +4,8 @@ title: New linters ## How to write a custom linter -Use `go/analysis` and take a look at [this tutorial](https://disaev.me/p/writing-useful-go-analysis-linter/): it shows how to write `go/analysis` linter -from scratch and integrate it into `golangci-lint`. +Use `go/analysis` and take a look at [this tutorial](https://disaev.me/p/writing-useful-go-analysis-linter/): +it shows how to write `go/analysis` linter from scratch and integrate it into `golangci-lint`. ## How to add a public linter to `golangci-lint` @@ -39,43 +39,52 @@ After that: Some people and organizations may choose to have custom-made linters run as a part of `golangci-lint`. Typically, these linters can't be open-sourced or too specific. + Such linters can be added through Go's plugin library. For a private linter (which acts as a plugin) to work properly, the plugin as well as the golangci-lint binary needs to be built for the same environment. `CGO_ENABLED` is another requirement. + This means that `golangci-lint` needs to be built for whatever machine you intend to run it on (cloning the golangci-lint repository and running a `CGO_ENABLED=1 make build` should do the trick for your machine). ### Configure a Plugin -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. +If you already have a linter plugin available, you can follow these steps to define its 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: - -```yaml -linters-settings: - custom: - example: - path: /example.so - description: The description of the linter - original-url: github.com/golangci/example-linter -``` + ```yaml + 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 disabled by default, and are not enabled when `linters.enable-all` is specified. They can be enabled by adding them the `linters.enable` list, or providing the enabled option on the command line (`golangci-lint run -Eexample`). ### Create a Plugin 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: +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: ```go type AnalyzerPlugin interface { @@ -83,8 +92,8 @@ type AnalyzerPlugin interface { } ``` -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/blob/master/plugin/example.go) for more info. +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/blob/master/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. +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. diff --git a/pkg/lint/lintersdb/custom_linters.go b/pkg/lint/lintersdb/custom_linters.go new file mode 100644 index 000000000000..f7369aa68fb1 --- /dev/null +++ b/pkg/lint/lintersdb/custom_linters.go @@ -0,0 +1,98 @@ +package lintersdb + +import ( + "fmt" + "path/filepath" + "plugin" + + "github.com/spf13/viper" + "golang.org/x/tools/go/analysis" + + "github.com/golangci/golangci-lint/pkg/config" + "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 AnalyzerPlugin interface { + GetAnalyzers() []*analysis.Analyzer +} + +// WithCustomLinters loads private linters that are specified in the golangci config file. +func (m *Manager) WithCustomLinters() *Manager { + if m.log == nil { + m.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &report.Data{}) + } + + if m.cfg == nil { + return m + } + + 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 +} + +// loadCustomLinterConfig loads the configuration of private linters. +// Private linters are dynamically loaded from .so plugin files. +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 +} + +// getAnalyzerPlugin loads a private linter as specified in the config file, +// loads the plugin from a .so file, +// and returns the 'AnalyzerPlugin' interface implemented by the private plugin. +// An error is returned if the private linter cannot be loaded +// or the linter does not implement the AnalyzerPlugin interface. +func (m *Manager) getAnalyzerPlugin(path string) (AnalyzerPlugin, error) { + if !filepath.IsAbs(path) { + // resolve non-absolute paths relative to config file's directory + configFilePath := viper.ConfigFileUsed() + absConfigFilePath, err := filepath.Abs(configFilePath) + if err != nil { + return nil, fmt.Errorf("could not get absolute representation of config file path %q: %v", configFilePath, err) + } + path = filepath.Join(filepath.Dir(absConfigFilePath), path) + } + + 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/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index 17ce2d9cbdc1..1930fd8d2a0a 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -1,19 +1,10 @@ package lintersdb import ( - "fmt" - "path/filepath" - "plugin" - - "github.com/spf13/viper" - "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 { @@ -35,28 +26,6 @@ func NewManager(cfg *config.Config, log logutils.Log) *Manager { return m } -// WithCustomLinters loads private linters that are specified in the golangci config file. -func (m *Manager) WithCustomLinters() *Manager { - if m.log == nil { - m.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &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, @@ -965,61 +934,3 @@ func (m Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config { return ret } - -// loadCustomLinterConfig loads the configuration of private linters. -// Private linters are dynamically loaded from .so plugin files. -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 -} - -// getAnalyzerPlugin loads a private linter as specified in the config file, -// loads the plugin from a .so file, and returns the 'AnalyzerPlugin' interface -// implemented by the private plugin. -// An error is returned if the private linter cannot be loaded or the linter -// does not implement the AnalyzerPlugin interface. -func (m Manager) getAnalyzerPlugin(path string) (AnalyzerPlugin, error) { - if !filepath.IsAbs(path) { - // resolve non-absolute paths relative to config file's directory - configFilePath := viper.ConfigFileUsed() - absConfigFilePath, err := filepath.Abs(configFilePath) - if err != nil { - return nil, fmt.Errorf("could not get absolute representation of config file path %q: %v", configFilePath, err) - } - path = filepath.Join(filepath.Dir(absConfigFilePath), path) - } - - 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 -}