mirror of
https://github.com/scratchfoundation/golangci-lint.git
synced 2025-06-10 20:02:18 -04:00
feat: new custom linters system (#4437)
This commit is contained in:
parent
f0fdea006f
commit
167204c1fd
31 changed files with 1339 additions and 110 deletions
.custom-gcl.reference.yml.gitignore.golangci.reference.yml.golangci.yml
cmd/golangci-lint
docs/src
go.modgo.sumpkg
commands
custom.gohelp.go
internal
linters.goroot.gorun.goconfig
lint/lintersdb
result/processors
37
.custom-gcl.reference.yml
Normal file
37
.custom-gcl.reference.yml
Normal file
|
@ -0,0 +1,37 @@
|
|||
# The golangci-lint version used to build the custom binary.
|
||||
# Require.
|
||||
version: v1.56.2
|
||||
|
||||
# the name of the custom binary.
|
||||
# Optional.
|
||||
# Default: custom-gcl
|
||||
name: custom-golangci-lint
|
||||
|
||||
# The directory path used to store the custom binary.
|
||||
# Optional.
|
||||
# Default: .
|
||||
destination: ./my/path/
|
||||
|
||||
# The list of the plugins to integrate inside the custom binary.
|
||||
plugins:
|
||||
# a plugin from a Go proxy
|
||||
- module: 'github.com/example/plugin3'
|
||||
version: v1.2.3
|
||||
|
||||
# a plugin from a Go proxy (with a specific import path)
|
||||
- module: 'github.com/example/plugin4'
|
||||
import: 'github.com/example/plugin4/foo'
|
||||
version: v1.0.0
|
||||
|
||||
# a plugin from local source (with absolute path)
|
||||
- module: 'github.com/example/plugin2'
|
||||
path: /my/local/path/plugin2
|
||||
|
||||
# a plugin from local source (with relative path)
|
||||
- module: 'github.com/example/plugin1'
|
||||
path: ./my/local/path/plugin1
|
||||
|
||||
# a plugin from local source (with absolute path and a specific import path)
|
||||
- module: 'github.com/example/plugin2'
|
||||
import: 'github.com/example/plugin4/foo'
|
||||
path: /my/local/path/plugin2
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -18,3 +18,7 @@
|
|||
/vendor/
|
||||
coverage.out
|
||||
coverage.xml
|
||||
/custom-golangci-lint
|
||||
/custom-gcl
|
||||
.custom-gcl.yml
|
||||
.custom-gcl.yaml
|
||||
|
|
|
@ -2481,6 +2481,10 @@ linters-settings:
|
|||
custom:
|
||||
# Each custom linter should have a unique name.
|
||||
example:
|
||||
# The plugin type.
|
||||
# It can be `goplugin` or `module`.
|
||||
# Default: goplugin
|
||||
type: module
|
||||
# The path to the plugin *.so. Can be absolute or local.
|
||||
# Required for each custom linter.
|
||||
path: /path/to/example.so
|
||||
|
|
|
@ -46,6 +46,9 @@ linters-settings:
|
|||
- whyNoLint
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
godox:
|
||||
keywords:
|
||||
- FIXME
|
||||
gofmt:
|
||||
rewrite-rules:
|
||||
- pattern: 'interface{}'
|
||||
|
@ -109,6 +112,7 @@ linters:
|
|||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- godox
|
||||
- gofmt
|
||||
- goimports
|
||||
- gomnd
|
||||
|
|
3
cmd/golangci-lint/plugins.go
Normal file
3
cmd/golangci-lint/plugins.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
// This file is used to declare module plugins.
|
|
@ -46,3 +46,10 @@
|
|||
link: /contributing/faq/
|
||||
- label: This Website
|
||||
link: /contributing/website/
|
||||
- label: Plugins
|
||||
items:
|
||||
- label: Module Plugin System
|
||||
link: /contributing/new-linters/
|
||||
- label: Go Plugin System
|
||||
link: /contributing/private-linters/
|
||||
|
||||
|
|
|
@ -2,10 +2,6 @@
|
|||
title: Contributing FAQ
|
||||
---
|
||||
|
||||
## How to write a custom linter
|
||||
|
||||
See [there](/contributing/new-linters#how-to-write-a-custom-linter).
|
||||
|
||||
## How to add a new open-source linter to `golangci-lint`
|
||||
|
||||
See [there](/contributing/new-linters#how-to-add-a-public-linter-to-golangci-lint).
|
||||
|
@ -16,7 +12,7 @@ See [there](/contributing/new-linters#how-to-add-a-private-linter-to-golangci-li
|
|||
|
||||
## How to update existing linter
|
||||
|
||||
Just update it's version in `go.mod`.
|
||||
Just update its version in `go.mod`.
|
||||
|
||||
## How to add configuration option to existing linter
|
||||
|
||||
|
|
|
@ -46,75 +46,7 @@ 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.
|
||||
Such linters can be added through 2 plugin systems:
|
||||
|
||||
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 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
|
||||
settings: # Settings are optional.
|
||||
one: Foo
|
||||
two:
|
||||
- name: Bar
|
||||
three:
|
||||
name: Bar
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
The configuration inside the `settings` field of linter have some limitations (there are NOT related to the plugin system itself):
|
||||
we use Viper to handle the configuration but Viper put all the keys in lowercase, and `.` cannot be used inside a key.
|
||||
|
||||
### Create a Plugin
|
||||
|
||||
Your linter must provide 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 file MUST be in the package `main`, and MUST define an exposed function called `New` with the following signature:
|
||||
```go
|
||||
func New(conf any) ([]*analysis.Analyzer, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
```bash
|
||||
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`.
|
||||
- [Go Plugin System](/plugins/module-plugins)
|
||||
- [Module Plugin System](/plugins/go-plugins)
|
||||
|
|
76
docs/src/docs/plugins/go-plugins.mdx
Normal file
76
docs/src/docs/plugins/go-plugins.mdx
Normal file
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
title: Go Plugin System
|
||||
---
|
||||
|
||||
Private linters can be added through [Go's plugin system](https://pkg.go.dev/plugin).
|
||||
|
||||
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).
|
||||
|
||||
## Create a Plugin
|
||||
|
||||
Your linter must provide 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 file MUST be in the package `main`, and MUST define an exposed function called `New` with the following signature:
|
||||
```go
|
||||
func New(conf any) ([]*analysis.Analyzer, error) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
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:
|
||||
```bash
|
||||
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`.
|
||||
|
||||
## Configure a Plugin
|
||||
|
||||
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 title=.golangci.yml
|
||||
linters-settings:
|
||||
custom:
|
||||
example:
|
||||
path: /example.so
|
||||
description: The description of the linter
|
||||
original-url: github.com/golangci/example-linter
|
||||
settings: # Settings are optional.
|
||||
one: Foo
|
||||
two:
|
||||
- name: Bar
|
||||
three:
|
||||
name: Bar
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
The configuration inside the `settings` field of linter have some limitations (there are NOT related to the plugin system itself):
|
||||
we use Viper to handle the configuration but Viper put all the keys in lowercase, and `.` cannot be used inside a key.
|
71
docs/src/docs/plugins/module-plugins.mdx
Normal file
71
docs/src/docs/plugins/module-plugins.mdx
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
title: Module Plugin System
|
||||
---
|
||||
|
||||
An example linter can be found at [here](https://github.com/golangci/example-plugin-module-linter/settings).
|
||||
|
||||
## The Automatic Way
|
||||
|
||||
- define your building configuration into `.custom-gcl.yml`
|
||||
- run the command `golangci-lint custom` ( or `golangci-lint custom -v` to have logs)
|
||||
- define the plugin inside the `linters-settings.custom` section with the type `module`.
|
||||
- run your custom version of golangci-lint
|
||||
|
||||
Requirements:
|
||||
- Go
|
||||
- git
|
||||
|
||||
### Configuration Example
|
||||
|
||||
```yaml title=.custom-gcl.yml
|
||||
version: v1.57.0
|
||||
plugins:
|
||||
# a plugin from a Go proxy
|
||||
- module: 'github.com/golangci/plugin1'
|
||||
import: 'github.com/golangci/plugin1/foo'
|
||||
version: v1.0.0
|
||||
|
||||
# a plugin from local source
|
||||
- module: 'github.com/golangci/plugin2'
|
||||
path: /my/local/path/plugin2
|
||||
```
|
||||
|
||||
```yaml title=.golangci.yml
|
||||
linters-settings:
|
||||
custom:
|
||||
foo:
|
||||
type: "module"
|
||||
description: This is an example usage of a plugin linter.
|
||||
settings:
|
||||
message: hello
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- foo
|
||||
```
|
||||
|
||||
## The Manual Way
|
||||
|
||||
- add a blank-import of your module inside `cmd/golangci-lint/plugins.go`
|
||||
- run `go mod tidy`. (the module containing the plugin will be imported)
|
||||
- run `make build`
|
||||
- define the plugin inside the configuration `linters-settings.custom` section with the type `module`.
|
||||
- run your custom version of golangci-lint
|
||||
|
||||
### Configuration Example
|
||||
|
||||
```yaml title=.golangci.yml
|
||||
linters-settings:
|
||||
custom:
|
||||
foo:
|
||||
type: "module"
|
||||
description: This is an example usage of a plugin linter.
|
||||
settings:
|
||||
message: hello
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- foo
|
||||
```
|
1
go.mod
1
go.mod
|
@ -44,6 +44,7 @@ require (
|
|||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a
|
||||
github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e
|
||||
github.com/golangci/misspell v0.4.1
|
||||
github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee
|
||||
github.com/golangci/revgrep v0.5.2
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed
|
||||
github.com/gordonklaus/ineffassign v0.1.0
|
||||
|
|
2
go.sum
generated
2
go.sum
generated
|
@ -231,6 +231,8 @@ github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e h1:ULcKCDV1LOZPFxGZ
|
|||
github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e/go.mod h1:Pm5KhLPA8gSnQwrQ6ukebRcapGb/BG9iUkdaiCcGHJM=
|
||||
github.com/golangci/misspell v0.4.1 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g=
|
||||
github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI=
|
||||
github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee h1:nl5nPZ5b2O2dj5+LizmFQ8gNq0r65OfALkp0M8EWJ8E=
|
||||
github.com/golangci/plugin-module-register v0.0.0-20240305222101-f76272ec86ee/go.mod h1:mGTAkB/NoZMvAGMkiv4+pmmwVO+Gp+zeV77nggByWCc=
|
||||
github.com/golangci/revgrep v0.5.2 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU=
|
||||
github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA=
|
||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed h1:IURFTjxeTfNFP0hTEi1YKjB/ub8zkpaOqFFMApi2EAs=
|
||||
|
|
81
pkg/commands/custom.go
Normal file
81
pkg/commands/custom.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/commands/internal"
|
||||
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||
)
|
||||
|
||||
const envKeepTempFiles = "CUSTOM_GCL_KEEP_TEMP_FILES"
|
||||
|
||||
type customCommand struct {
|
||||
cmd *cobra.Command
|
||||
|
||||
cfg *internal.Configuration
|
||||
|
||||
log logutils.Log
|
||||
}
|
||||
|
||||
func newCustomCommand(logger logutils.Log) *customCommand {
|
||||
c := &customCommand{log: logger}
|
||||
|
||||
customCmd := &cobra.Command{
|
||||
Use: "custom",
|
||||
Short: "Build a version of golangci-lint with custom linters.",
|
||||
Args: cobra.NoArgs,
|
||||
PreRunE: c.preRunE,
|
||||
RunE: c.runE,
|
||||
}
|
||||
|
||||
c.cmd = customCmd
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *customCommand) preRunE(_ *cobra.Command, _ []string) error {
|
||||
cfg, err := internal.LoadConfiguration()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cfg.Validate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.cfg = cfg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *customCommand) runE(_ *cobra.Command, _ []string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
tmp, err := os.MkdirTemp(os.TempDir(), "custom-gcl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temporary directory: %w", err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if os.Getenv(envKeepTempFiles) != "" {
|
||||
log.Printf("WARN: The env var %s has been dectected: the temporary directory is preserved: %s", envKeepTempFiles, tmp)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.RemoveAll(tmp)
|
||||
}()
|
||||
|
||||
err = internal.NewBuilder(c.log, c.cfg, tmp).Build(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build process: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -53,8 +53,7 @@ func newHelpCommand(logger logutils.Log) *helpCommand {
|
|||
func (c *helpCommand) preRunE(_ *cobra.Command, _ []string) error {
|
||||
// The command doesn't depend on the real configuration.
|
||||
// It just needs the list of all plugins and all presets.
|
||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(),
|
||||
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
|
||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(), lintersdb.NewLinterBuilder())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
219
pkg/commands/internal/builder.go
Normal file
219
pkg/commands/internal/builder.go
Normal file
|
@ -0,0 +1,219 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||
)
|
||||
|
||||
// Builder runs all the required commands to build a binary.
|
||||
type Builder struct {
|
||||
cfg *Configuration
|
||||
|
||||
log logutils.Log
|
||||
|
||||
root string
|
||||
repo string
|
||||
}
|
||||
|
||||
// NewBuilder creates a new Builder.
|
||||
func NewBuilder(logger logutils.Log, cfg *Configuration, root string) *Builder {
|
||||
return &Builder{
|
||||
cfg: cfg,
|
||||
log: logger,
|
||||
root: root,
|
||||
repo: filepath.Join(root, "golangci-lint"),
|
||||
}
|
||||
}
|
||||
|
||||
// Build builds the custom binary.
|
||||
func (b Builder) Build(ctx context.Context) error {
|
||||
b.log.Infof("Cloning golangci-lint repository.")
|
||||
|
||||
err := b.clone(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("clone golangci-lint: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("Adding plugin imports.")
|
||||
|
||||
err = b.updatePluginsFile()
|
||||
if err != nil {
|
||||
return fmt.Errorf("update plugin file: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("Adding replace directives.")
|
||||
|
||||
err = b.addReplaceDirectives(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add replace directives: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("Running go mod tidy.")
|
||||
|
||||
err = b.goModTidy(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("go mod tidy: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("Building golangci-lint binary.")
|
||||
|
||||
binaryName := b.getBinaryName()
|
||||
|
||||
err = b.goBuild(ctx, binaryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build golangci-lint binary: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("Moving golangci-lint binary.")
|
||||
|
||||
err = b.copyBinary(binaryName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("move golangci-lint binary: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) clone(ctx context.Context) error {
|
||||
//nolint:gosec // the variable is sanitized.
|
||||
cmd := exec.CommandContext(ctx,
|
||||
"git", "clone", "--branch", sanitizeVersion(b.cfg.Version),
|
||||
"--single-branch", "--depth", "1", "-c advice.detachedHead=false", "-q",
|
||||
"https://github.com/golangci/golangci-lint.git",
|
||||
)
|
||||
cmd.Dir = b.root
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
b.log.Infof(string(output))
|
||||
|
||||
return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) addReplaceDirectives(ctx context.Context) error {
|
||||
for _, plugin := range b.cfg.Plugins {
|
||||
if plugin.Path == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
replace := fmt.Sprintf("%s=%s", plugin.Module, plugin.Path)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "go", "mod", "edit", "-replace", replace)
|
||||
cmd.Dir = b.repo
|
||||
|
||||
b.log.Infof("run: %s", strings.Join(cmd.Args, " "))
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
b.log.Warnf(string(output))
|
||||
|
||||
return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) goModTidy(ctx context.Context) error {
|
||||
cmd := exec.CommandContext(ctx, "go", "mod", "tidy")
|
||||
cmd.Dir = b.repo
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
b.log.Warnf(string(output))
|
||||
|
||||
return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) goBuild(ctx context.Context, binaryName string) error {
|
||||
//nolint:gosec // the variable is sanitized.
|
||||
cmd := exec.CommandContext(ctx, "go", "build",
|
||||
"-ldflags",
|
||||
fmt.Sprintf(
|
||||
"-s -w -X 'main.version=%s-custom-gcl' -X 'main.date=%s'",
|
||||
sanitizeVersion(b.cfg.Version), time.Now().UTC().String(),
|
||||
),
|
||||
"-o", binaryName,
|
||||
"./cmd/golangci-lint",
|
||||
)
|
||||
cmd.Dir = b.repo
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
b.log.Warnf(string(output))
|
||||
|
||||
return fmt.Errorf("%s: %w", strings.Join(cmd.Args, " "), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) copyBinary(binaryName string) error {
|
||||
src := filepath.Join(b.repo, binaryName)
|
||||
|
||||
source, err := os.Open(filepath.Clean(src))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = source.Close() }()
|
||||
|
||||
info, err := source.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stat source file: %w", err)
|
||||
}
|
||||
|
||||
if b.cfg.Destination != "" {
|
||||
err = os.MkdirAll(b.cfg.Destination, os.ModePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
dst, err := os.OpenFile(filepath.Join(b.cfg.Destination, binaryName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination file: %w", err)
|
||||
}
|
||||
|
||||
defer func() { _ = dst.Close() }()
|
||||
|
||||
_, err = io.Copy(dst, source)
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy source to destination: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b Builder) getBinaryName() string {
|
||||
name := b.cfg.Name
|
||||
if runtime.GOOS == "windows" {
|
||||
name += ".exe"
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
func sanitizeVersion(v string) string {
|
||||
fn := func(c rune) bool {
|
||||
return !(unicode.IsLetter(c) || unicode.IsNumber(c) || c == '.' || c == '/')
|
||||
}
|
||||
|
||||
return strings.Join(strings.FieldsFunc(v, fn), "")
|
||||
}
|
57
pkg/commands/internal/builder_test.go
Normal file
57
pkg/commands/internal/builder_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_sanitizeVersion(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "ampersand",
|
||||
input: " te&st",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
desc: "pipe",
|
||||
input: " te|st",
|
||||
expected: "test",
|
||||
},
|
||||
{
|
||||
desc: "version",
|
||||
input: "v1.2.3",
|
||||
expected: "v1.2.3",
|
||||
},
|
||||
{
|
||||
desc: "branch",
|
||||
input: "feat/test",
|
||||
expected: "feat/test",
|
||||
},
|
||||
{
|
||||
desc: "branch",
|
||||
input: "value --key",
|
||||
expected: "valuekey",
|
||||
},
|
||||
{
|
||||
desc: "hash",
|
||||
input: "cd8b1177",
|
||||
expected: "cd8b1177",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
v := sanitizeVersion(test.input)
|
||||
|
||||
assert.Equal(t, test.expected, v)
|
||||
})
|
||||
}
|
||||
}
|
138
pkg/commands/internal/configuration.go
Normal file
138
pkg/commands/internal/configuration.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const base = ".custom-gcl"
|
||||
|
||||
const defaultBinaryName = "custom-gcl"
|
||||
|
||||
// Configuration represents the configuration file.
|
||||
type Configuration struct {
|
||||
// golangci-lint version.
|
||||
Version string `yaml:"version"`
|
||||
|
||||
// Name of the binary.
|
||||
Name string `yaml:"name,omitempty"`
|
||||
|
||||
// Destination is the path to a directory to store the binary.
|
||||
Destination string `yaml:"destination,omitempty"`
|
||||
|
||||
// Plugins information.
|
||||
Plugins []*Plugin `yaml:"plugins,omitempty"`
|
||||
}
|
||||
|
||||
// Validate checks and clean the configuration.
|
||||
func (c *Configuration) Validate() error {
|
||||
if strings.TrimSpace(c.Version) == "" {
|
||||
return errors.New("root field 'version' is required")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.Name) == "" {
|
||||
c.Name = defaultBinaryName
|
||||
}
|
||||
|
||||
if len(c.Plugins) == 0 {
|
||||
return errors.New("no plugins defined")
|
||||
}
|
||||
|
||||
for _, plugin := range c.Plugins {
|
||||
if strings.TrimSpace(plugin.Module) == "" {
|
||||
return errors.New("field 'module' is required")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(plugin.Import) == "" {
|
||||
plugin.Import = plugin.Module
|
||||
}
|
||||
|
||||
if strings.TrimSpace(plugin.Path) == "" && strings.TrimSpace(plugin.Version) == "" {
|
||||
return errors.New("missing information: 'version' or 'path' should be provided")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(plugin.Path) != "" && strings.TrimSpace(plugin.Version) != "" {
|
||||
return errors.New("invalid configuration: 'version' and 'path' should not be provided at the same time")
|
||||
}
|
||||
|
||||
if strings.TrimSpace(plugin.Path) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
abs, err := filepath.Abs(plugin.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
plugin.Path = abs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Plugin represents information about a plugin.
|
||||
type Plugin struct {
|
||||
// Module name.
|
||||
Module string `yaml:"module"`
|
||||
|
||||
// Import to use.
|
||||
Import string `yaml:"import,omitempty"`
|
||||
|
||||
// Version of the module.
|
||||
// Only for module available through a Go proxy.
|
||||
Version string `yaml:"version,omitempty"`
|
||||
|
||||
// Path to the local module.
|
||||
// Only for local module.
|
||||
Path string `yaml:"path,omitempty"`
|
||||
}
|
||||
|
||||
func LoadConfiguration() (*Configuration, error) {
|
||||
configFilePath, err := findConfigurationFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file %s not found: %w", configFilePath, err)
|
||||
}
|
||||
|
||||
file, err := os.Open(configFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("file %s open: %w", configFilePath, err)
|
||||
}
|
||||
|
||||
var cfg Configuration
|
||||
|
||||
err = yaml.NewDecoder(file).Decode(&cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("YAML decoding: %w", err)
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func findConfigurationFile() (string, error) {
|
||||
entries, err := os.ReadDir(".")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read directory: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
ext := filepath.Ext(entry.Name())
|
||||
|
||||
switch strings.ToLower(strings.TrimPrefix(ext, ".")) {
|
||||
case "yml", "yaml", "json":
|
||||
if isConf(ext, entry.Name()) {
|
||||
return entry.Name(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("configuration file not found")
|
||||
}
|
||||
|
||||
func isConf(ext, name string) bool {
|
||||
return base+ext == name
|
||||
}
|
127
pkg/commands/internal/configuration_test.go
Normal file
127
pkg/commands/internal/configuration_test.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestConfiguration_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
cfg *Configuration
|
||||
}{
|
||||
{
|
||||
desc: "version",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "example.org/foo/bar",
|
||||
Import: "example.org/foo/bar/test",
|
||||
Version: "v1.2.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "path",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "example.org/foo/bar",
|
||||
Import: "example.org/foo/bar/test",
|
||||
Path: "/my/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.cfg.Validate()
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfiguration_Validate_error(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
cfg *Configuration
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "missing version",
|
||||
cfg: &Configuration{},
|
||||
expected: "root field 'version' is required",
|
||||
},
|
||||
{
|
||||
desc: "no plugins",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
},
|
||||
expected: "no plugins defined",
|
||||
},
|
||||
{
|
||||
desc: "missing module",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "",
|
||||
Import: "example.org/foo/bar/test",
|
||||
Version: "v1.2.3",
|
||||
Path: "/my/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "field 'module' is required",
|
||||
},
|
||||
{
|
||||
desc: "module version and path",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "example.org/foo/bar",
|
||||
Import: "example.org/foo/bar/test",
|
||||
Version: "v1.2.3",
|
||||
Path: "/my/path",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "invalid configuration: 'version' and 'path' should not be provided at the same time",
|
||||
},
|
||||
{
|
||||
desc: "no module version and path",
|
||||
cfg: &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "example.org/foo/bar",
|
||||
Import: "example.org/foo/bar/test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "missing information: 'version' or 'path' should be provided",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.cfg.Validate()
|
||||
|
||||
assert.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
69
pkg/commands/internal/imports.go
Normal file
69
pkg/commands/internal/imports.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const importsTemplate = `
|
||||
package main
|
||||
|
||||
import (
|
||||
{{range .Imports -}}
|
||||
_ "{{.}}"
|
||||
{{end -}}
|
||||
)
|
||||
`
|
||||
|
||||
func (b Builder) updatePluginsFile() error {
|
||||
importsDest := filepath.Join(b.repo, "cmd", "golangci-lint", "plugins.go")
|
||||
|
||||
info, err := os.Stat(importsDest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file %s not found: %w", importsDest, err)
|
||||
}
|
||||
|
||||
source, err := generateImports(b.cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate imports: %w", err)
|
||||
}
|
||||
|
||||
b.log.Infof("generated imports info %s:\n%s\n", importsDest, source)
|
||||
|
||||
err = os.WriteFile(filepath.Clean(importsDest), source, info.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("write file %s: %w", importsDest, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateImports(cfg *Configuration) ([]byte, error) {
|
||||
impTmpl, err := template.New("plugins.go").Parse(importsTemplate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse template: %w", err)
|
||||
}
|
||||
|
||||
var imps []string
|
||||
for _, plugin := range cfg.Plugins {
|
||||
imps = append(imps, plugin.Import)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
err = impTmpl.Execute(buf, map[string]any{"Imports": imps})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute template: %w", err)
|
||||
}
|
||||
|
||||
source, err := format.Source(buf.Bytes())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("format source: %w", err)
|
||||
}
|
||||
|
||||
return source, nil
|
||||
}
|
36
pkg/commands/internal/imports_test.go
Normal file
36
pkg/commands/internal/imports_test.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package internal
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_generateImports(t *testing.T) {
|
||||
cfg := &Configuration{
|
||||
Version: "v1.57.0",
|
||||
Plugins: []*Plugin{
|
||||
{
|
||||
Module: "example.org/foo/bar",
|
||||
Import: "example.org/foo/bar/test",
|
||||
Version: "v1.2.3",
|
||||
},
|
||||
{
|
||||
Module: "example.com/foo/bar",
|
||||
Import: "example.com/foo/bar/test",
|
||||
Path: "/my/path",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := generateImports(cfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
expected, err := os.ReadFile(filepath.Join("testdata", "imports.go"))
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, expected, data)
|
||||
}
|
6
pkg/commands/internal/testdata/imports.go
vendored
Normal file
6
pkg/commands/internal/testdata/imports.go
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
_ "example.com/foo/bar/test"
|
||||
_ "example.org/foo/bar/test"
|
||||
)
|
|
@ -65,7 +65,7 @@ func (c *lintersCommand) preRunE(cmd *cobra.Command, _ []string) error {
|
|||
}
|
||||
|
||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
|
||||
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
|
||||
lintersdb.NewLinterBuilder(), lintersdb.NewPluginModuleBuilder(c.log), lintersdb.NewPluginGoBuilder(c.log))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ func newRootCommand(info BuildInfo) *rootCommand {
|
|||
newCacheCommand().cmd,
|
||||
newConfigCommand(log).cmd,
|
||||
newVersionCommand(info).cmd,
|
||||
newCustomCommand(log).cmd,
|
||||
)
|
||||
|
||||
rootCmd.SetHelpCommand(newHelpCommand(log).cmd)
|
||||
|
|
|
@ -180,7 +180,7 @@ func (c *runCommand) persistentPostRunE(_ *cobra.Command, _ []string) error {
|
|||
|
||||
func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error {
|
||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
|
||||
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
|
||||
lintersdb.NewLinterBuilder(), lintersdb.NewPluginModuleBuilder(c.log), lintersdb.NewPluginGoBuilder(c.log))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
@ -281,7 +282,17 @@ type LintersSettings struct {
|
|||
}
|
||||
|
||||
func (s *LintersSettings) Validate() error {
|
||||
return s.Govet.Validate()
|
||||
if err := s.Govet.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for name, settings := range s.Custom {
|
||||
if err := settings.Validate(); err != nil {
|
||||
return fmt.Errorf("custom linter %q: %w", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AsasalintSettings struct {
|
||||
|
@ -946,17 +957,15 @@ type WSLSettings struct {
|
|||
}
|
||||
|
||||
// CustomLinterSettings encapsulates the meta-data of a private linter.
|
||||
// For example, a private linter may be added to the golangci config file as shown below.
|
||||
//
|
||||
// linters-settings:
|
||||
// custom:
|
||||
// example:
|
||||
// path: /example.so
|
||||
// description: The description of the linter
|
||||
// original-url: github.com/golangci/example-linter
|
||||
type CustomLinterSettings struct {
|
||||
// Type plugin type.
|
||||
// It can be `goplugin` or `module`.
|
||||
Type string `mapstructure:"type"`
|
||||
|
||||
// Path to a plugin *.so file that implements the private linter.
|
||||
// Only for Go plugin system.
|
||||
Path string
|
||||
|
||||
// Description describes the purpose of the private linter.
|
||||
Description string
|
||||
// OriginalURL The URL containing the source code for the private linter.
|
||||
|
@ -965,3 +974,19 @@ type CustomLinterSettings struct {
|
|||
// Settings plugin settings only work with linterdb.PluginConstructor symbol.
|
||||
Settings any
|
||||
}
|
||||
|
||||
func (s *CustomLinterSettings) Validate() error {
|
||||
if s.Type == "module" {
|
||||
if s.Path != "" {
|
||||
return errors.New("path not supported with module type")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if s.Path == "" {
|
||||
return errors.New("path is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
236
pkg/config/linters_settings_test.go
Normal file
236
pkg/config/linters_settings_test.go
Normal file
|
@ -0,0 +1,236 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLintersSettings_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *LintersSettings
|
||||
}{
|
||||
{
|
||||
desc: "custom linter",
|
||||
settings: &LintersSettings{
|
||||
Custom: map[string]CustomLinterSettings{
|
||||
"example": {
|
||||
Type: "module",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "govet",
|
||||
settings: &LintersSettings{
|
||||
Govet: GovetSettings{
|
||||
Enable: []string{"a"},
|
||||
DisableAll: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLintersSettings_Validate_error(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *LintersSettings
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "custom linter error",
|
||||
settings: &LintersSettings{
|
||||
Custom: map[string]CustomLinterSettings{
|
||||
"example": {
|
||||
Type: "module",
|
||||
Path: "example",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: `custom linter "example": path not supported with module type`,
|
||||
},
|
||||
{
|
||||
desc: "govet error",
|
||||
settings: &LintersSettings{
|
||||
Govet: GovetSettings{
|
||||
EnableAll: true,
|
||||
DisableAll: true,
|
||||
},
|
||||
},
|
||||
expected: "govet: enable-all and disable-all can't be combined",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
|
||||
assert.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomLinterSettings_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *CustomLinterSettings
|
||||
}{
|
||||
{
|
||||
desc: "only path",
|
||||
settings: &CustomLinterSettings{
|
||||
Path: "example",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "path and type goplugin",
|
||||
settings: &CustomLinterSettings{
|
||||
Type: "goplugin",
|
||||
Path: "example",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "type module",
|
||||
settings: &CustomLinterSettings{
|
||||
Type: "module",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomLinterSettings_Validate_error(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *CustomLinterSettings
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "missing path",
|
||||
settings: &CustomLinterSettings{},
|
||||
expected: "path is required",
|
||||
},
|
||||
{
|
||||
desc: "module and path",
|
||||
settings: &CustomLinterSettings{
|
||||
Type: "module",
|
||||
Path: "example",
|
||||
},
|
||||
expected: "path not supported with module type",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
|
||||
assert.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovetSettings_Validate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *GovetSettings
|
||||
}{
|
||||
{
|
||||
desc: "empty",
|
||||
settings: &GovetSettings{},
|
||||
},
|
||||
{
|
||||
desc: "disable-all and enable",
|
||||
settings: &GovetSettings{
|
||||
Enable: []string{"a"},
|
||||
DisableAll: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "enable-all and disable",
|
||||
settings: &GovetSettings{
|
||||
Disable: []string{"a"},
|
||||
EnableAll: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGovetSettings_Validate_error(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
settings *GovetSettings
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
desc: "enable-all and disable-all",
|
||||
settings: &GovetSettings{
|
||||
EnableAll: true,
|
||||
DisableAll: true,
|
||||
},
|
||||
expected: "govet: enable-all and disable-all can't be combined",
|
||||
},
|
||||
{
|
||||
desc: "enable-all and enable",
|
||||
settings: &GovetSettings{
|
||||
EnableAll: true,
|
||||
Enable: []string{"a"},
|
||||
},
|
||||
expected: "govet: enable-all and enable can't be combined",
|
||||
},
|
||||
{
|
||||
desc: "disable-all and disable",
|
||||
settings: &GovetSettings{
|
||||
DisableAll: true,
|
||||
Disable: []string{"a"},
|
||||
},
|
||||
expected: "govet: disable-all and disable can't be combined",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
test := test
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := test.settings.Validate()
|
||||
|
||||
assert.EqualError(t, err, test.expected)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -16,9 +16,9 @@ func NewLinterBuilder() *LinterBuilder {
|
|||
|
||||
// Build loads all the "internal" linters.
|
||||
// The configuration is use for the linter settings.
|
||||
func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
|
||||
func (b LinterBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
|
||||
if cfg == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const megacheckName = "megacheck"
|
||||
|
@ -707,5 +707,5 @@ func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
|
|||
WithSince("v1.26.0").
|
||||
WithPresets(linter.PresetStyle).
|
||||
WithURL("https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint/README.md"),
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -14,43 +14,51 @@ import (
|
|||
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||
)
|
||||
|
||||
const goPluginType = "goplugin"
|
||||
|
||||
type AnalyzerPlugin interface {
|
||||
GetAnalyzers() []*analysis.Analyzer
|
||||
}
|
||||
|
||||
// PluginBuilder builds the custom linters (plugins) based on the configuration.
|
||||
type PluginBuilder struct {
|
||||
// PluginGoBuilder builds the custom linters (Go plugin) based on the configuration.
|
||||
type PluginGoBuilder struct {
|
||||
log logutils.Log
|
||||
}
|
||||
|
||||
// NewPluginBuilder creates new PluginBuilder.
|
||||
func NewPluginBuilder(log logutils.Log) *PluginBuilder {
|
||||
return &PluginBuilder{log: log}
|
||||
// NewPluginGoBuilder creates new PluginGoBuilder.
|
||||
func NewPluginGoBuilder(log logutils.Log) *PluginGoBuilder {
|
||||
return &PluginGoBuilder{log: log}
|
||||
}
|
||||
|
||||
// Build loads custom linters that are specified in the golangci-lint config file.
|
||||
func (b *PluginBuilder) Build(cfg *config.Config) []*linter.Config {
|
||||
func (b *PluginGoBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
|
||||
if cfg == nil || b.log == nil {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var linters []*linter.Config
|
||||
|
||||
for name, settings := range cfg.LintersSettings.Custom {
|
||||
lc, err := b.loadConfig(cfg, name, settings)
|
||||
if settings.Type != goPluginType && settings.Type != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
settings := settings
|
||||
|
||||
lc, err := b.loadConfig(cfg, name, &settings)
|
||||
if err != nil {
|
||||
b.log.Errorf("Unable to load custom analyzer %s:%s, %v", name, settings.Path, err)
|
||||
return nil, fmt.Errorf("unable to load custom analyzer %q: %s, %w", name, settings.Path, err)
|
||||
} else {
|
||||
linters = append(linters, lc)
|
||||
}
|
||||
}
|
||||
|
||||
return linters
|
||||
return linters, nil
|
||||
}
|
||||
|
||||
// loadConfig loads the configuration of private linters.
|
||||
// Private linters are dynamically loaded from .so plugin files.
|
||||
func (b *PluginBuilder) loadConfig(cfg *config.Config, name string, settings config.CustomLinterSettings) (*linter.Config, error) {
|
||||
func (b *PluginGoBuilder) loadConfig(cfg *config.Config, name string, settings *config.CustomLinterSettings) (*linter.Config, error) {
|
||||
analyzers, err := b.getAnalyzerPlugin(cfg, settings.Path, settings.Settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -74,7 +82,7 @@ func (b *PluginBuilder) loadConfig(cfg *config.Config, name string, settings con
|
|||
// 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 (b *PluginBuilder) getAnalyzerPlugin(cfg *config.Config, path string, settings any) ([]*analysis.Analyzer, error) {
|
||||
func (b *PluginGoBuilder) getAnalyzerPlugin(cfg *config.Config, path string, settings any) ([]*analysis.Analyzer, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
// resolve non-absolute paths relative to config file's directory
|
||||
path = filepath.Join(cfg.GetConfigDir(), path)
|
||||
|
@ -93,7 +101,7 @@ func (b *PluginBuilder) getAnalyzerPlugin(cfg *config.Config, path string, setti
|
|||
return analyzers, nil
|
||||
}
|
||||
|
||||
func (b *PluginBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
|
||||
func (b *PluginGoBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*analysis.Analyzer, error) {
|
||||
symbol, err := plug.Lookup("New")
|
||||
if err != nil {
|
||||
analyzers, errP := b.lookupAnalyzerPlugin(plug)
|
||||
|
@ -113,7 +121,7 @@ func (b *PluginBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*anal
|
|||
return constructor(settings)
|
||||
}
|
||||
|
||||
func (b *PluginBuilder) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
|
||||
func (b *PluginGoBuilder) lookupAnalyzerPlugin(plug *plugin.Plugin) ([]*analysis.Analyzer, error) {
|
||||
symbol, err := plug.Lookup("AnalyzerPlugin")
|
||||
if err != nil {
|
||||
return nil, err
|
85
pkg/lint/lintersdb/builder_plugin_module.go
Normal file
85
pkg/lint/lintersdb/builder_plugin_module.go
Normal file
|
@ -0,0 +1,85 @@
|
|||
package lintersdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/golangci/plugin-module-register/register"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
const modulePluginType = "module"
|
||||
|
||||
// PluginModuleBuilder builds the custom linters (module plugin) based on the configuration.
|
||||
type PluginModuleBuilder struct {
|
||||
log logutils.Log
|
||||
}
|
||||
|
||||
// NewPluginModuleBuilder creates new PluginModuleBuilder.
|
||||
func NewPluginModuleBuilder(log logutils.Log) *PluginModuleBuilder {
|
||||
return &PluginModuleBuilder{log: log}
|
||||
}
|
||||
|
||||
// Build loads custom linters that are specified in the golangci-lint config file.
|
||||
func (b *PluginModuleBuilder) Build(cfg *config.Config) ([]*linter.Config, error) {
|
||||
if cfg == nil || b.log == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var linters []*linter.Config
|
||||
|
||||
for name, settings := range cfg.LintersSettings.Custom {
|
||||
if settings.Type != modulePluginType {
|
||||
continue
|
||||
}
|
||||
|
||||
b.log.Infof("Loaded %s: %s", settings.Path, name)
|
||||
|
||||
newPlugin, err := register.GetPlugin(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin(%s): %w", name, err)
|
||||
}
|
||||
|
||||
p, err := newPlugin(settings.Settings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin(%s): newPlugin %w", name, err)
|
||||
}
|
||||
|
||||
analyzers, err := p.BuildAnalyzers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("plugin(%s): BuildAnalyzers %w", name, err)
|
||||
}
|
||||
|
||||
customLinter := goanalysis.NewLinter(name, settings.Description, analyzers, nil)
|
||||
|
||||
switch strings.ToLower(p.GetLoadMode()) {
|
||||
case register.LoadModeSyntax:
|
||||
customLinter = customLinter.WithLoadMode(goanalysis.LoadModeSyntax)
|
||||
case register.LoadModeTypesInfo:
|
||||
customLinter = customLinter.WithLoadMode(goanalysis.LoadModeTypesInfo)
|
||||
default:
|
||||
customLinter = customLinter.WithLoadMode(goanalysis.LoadModeTypesInfo)
|
||||
}
|
||||
|
||||
lc := linter.NewConfig(customLinter).
|
||||
WithEnabledByDefault().
|
||||
WithURL(settings.OriginalURL)
|
||||
|
||||
switch strings.ToLower(p.GetLoadMode()) {
|
||||
case register.LoadModeSyntax:
|
||||
// noop
|
||||
case register.LoadModeTypesInfo:
|
||||
lc = lc.WithLoadForGoAnalysis()
|
||||
default:
|
||||
lc = lc.WithLoadForGoAnalysis()
|
||||
}
|
||||
|
||||
linters = append(linters, lc)
|
||||
}
|
||||
|
||||
return linters, nil
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package lintersdb
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
|
@ -17,7 +18,7 @@ import (
|
|||
const EnvTestRun = "GL_TEST_RUN"
|
||||
|
||||
type Builder interface {
|
||||
Build(cfg *config.Config) []*linter.Config
|
||||
Build(cfg *config.Config) ([]*linter.Config, error)
|
||||
}
|
||||
|
||||
// Manager is a type of database for all linters (internals or plugins).
|
||||
|
@ -48,7 +49,12 @@ func NewManager(log logutils.Log, cfg *config.Config, builders ...Builder) (*Man
|
|||
}
|
||||
|
||||
for _, builder := range builders {
|
||||
m.linters = append(m.linters, builder.Build(m.cfg)...)
|
||||
linters, err := builder.Build(m.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build linters: %w", err)
|
||||
}
|
||||
|
||||
m.linters = append(m.linters, linters...)
|
||||
}
|
||||
|
||||
for _, lc := range m.linters {
|
||||
|
|
|
@ -34,8 +34,7 @@ func newNolint2FileIssue(line int) result.Issue {
|
|||
}
|
||||
|
||||
func newTestNolintProcessor(log logutils.Log) *Nolint {
|
||||
dbManager, _ := lintersdb.NewManager(log, config.NewDefault(),
|
||||
lintersdb.NewPluginBuilder(log), lintersdb.NewLinterBuilder())
|
||||
dbManager, _ := lintersdb.NewManager(log, config.NewDefault(), lintersdb.NewLinterBuilder())
|
||||
|
||||
return NewNolint(log, dbManager, nil)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue