Skip to content

Commit

Permalink
chore: move code related to plugin to a dedicated file
Browse files Browse the repository at this point in the history
  • Loading branch information
ldez committed Jun 4, 2023
1 parent c9d759e commit 292fa5b
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 111 deletions.
53 changes: 31 additions & 22 deletions docs/src/docs/contributing/new-linters.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -39,52 +39,61 @@ 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 {
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/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.
98 changes: 98 additions & 0 deletions pkg/lint/lintersdb/custom_linters.go
Original file line number Diff line number Diff line change
@@ -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
}
89 changes: 0 additions & 89 deletions pkg/lint/lintersdb/manager.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -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
}

0 comments on commit 292fa5b

Please sign in to comment.