mirror of
https://github.com/scratchfoundation/golangci-lint.git
synced 2025-06-28 21:30:19 -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/
|
/vendor/
|
||||||
coverage.out
|
coverage.out
|
||||||
coverage.xml
|
coverage.xml
|
||||||
|
/custom-golangci-lint
|
||||||
|
/custom-gcl
|
||||||
|
.custom-gcl.yml
|
||||||
|
.custom-gcl.yaml
|
||||||
|
|
|
@ -2481,6 +2481,10 @@ linters-settings:
|
||||||
custom:
|
custom:
|
||||||
# Each custom linter should have a unique name.
|
# Each custom linter should have a unique name.
|
||||||
example:
|
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.
|
# The path to the plugin *.so. Can be absolute or local.
|
||||||
# Required for each custom linter.
|
# Required for each custom linter.
|
||||||
path: /path/to/example.so
|
path: /path/to/example.so
|
||||||
|
|
|
@ -46,6 +46,9 @@ linters-settings:
|
||||||
- whyNoLint
|
- whyNoLint
|
||||||
gocyclo:
|
gocyclo:
|
||||||
min-complexity: 15
|
min-complexity: 15
|
||||||
|
godox:
|
||||||
|
keywords:
|
||||||
|
- FIXME
|
||||||
gofmt:
|
gofmt:
|
||||||
rewrite-rules:
|
rewrite-rules:
|
||||||
- pattern: 'interface{}'
|
- pattern: 'interface{}'
|
||||||
|
@ -109,6 +112,7 @@ linters:
|
||||||
- goconst
|
- goconst
|
||||||
- gocritic
|
- gocritic
|
||||||
- gocyclo
|
- gocyclo
|
||||||
|
- godox
|
||||||
- gofmt
|
- gofmt
|
||||||
- goimports
|
- goimports
|
||||||
- gomnd
|
- 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/
|
link: /contributing/faq/
|
||||||
- label: This Website
|
- label: This Website
|
||||||
link: /contributing/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
|
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`
|
## 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).
|
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
|
## 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
|
## 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`.
|
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.
|
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,
|
- [Go Plugin System](/plugins/module-plugins)
|
||||||
the plugin as well as the golangci-lint binary **needs to be built for the same environment**.
|
- [Module Plugin System](/plugins/go-plugins)
|
||||||
|
|
||||||
`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`.
|
|
||||||
|
|
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/dupl v0.0.0-20180902072040-3e9179ac440a
|
||||||
github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e
|
github.com/golangci/gofmt v0.0.0-20231018234816-f50ced29576e
|
||||||
github.com/golangci/misspell v0.4.1
|
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/revgrep v0.5.2
|
||||||
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed
|
github.com/golangci/unconvert v0.0.0-20240309020433-c5143eacb3ed
|
||||||
github.com/gordonklaus/ineffassign v0.1.0
|
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/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 h1:+y73iSicVy2PqyX7kmUefHusENlrP9YwuHZHPLGQj/g=
|
||||||
github.com/golangci/misspell v0.4.1/go.mod h1:9mAN1quEo3DlpbaIKKyEvRxK1pwqR9s/Sea1bJCtlNI=
|
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 h1:EndcWoRhcnfj2NHQ+28hyuXpLMF+dQmCN+YaeeIl4FU=
|
||||||
github.com/golangci/revgrep v0.5.2/go.mod h1:bjAMA+Sh/QUfTDcHzxfyHxr4xKvllVr/0sCv2e7jJHA=
|
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=
|
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 {
|
func (c *helpCommand) preRunE(_ *cobra.Command, _ []string) error {
|
||||||
// The command doesn't depend on the real configuration.
|
// The command doesn't depend on the real configuration.
|
||||||
// It just needs the list of all plugins and all presets.
|
// It just needs the list of all plugins and all presets.
|
||||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(),
|
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), config.NewDefault(), lintersdb.NewLinterBuilder())
|
||||||
lintersdb.NewPluginBuilder(c.log), lintersdb.NewLinterBuilder())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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,
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,6 +63,7 @@ func newRootCommand(info BuildInfo) *rootCommand {
|
||||||
newCacheCommand().cmd,
|
newCacheCommand().cmd,
|
||||||
newConfigCommand(log).cmd,
|
newConfigCommand(log).cmd,
|
||||||
newVersionCommand(info).cmd,
|
newVersionCommand(info).cmd,
|
||||||
|
newCustomCommand(log).cmd,
|
||||||
)
|
)
|
||||||
|
|
||||||
rootCmd.SetHelpCommand(newHelpCommand(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 {
|
func (c *runCommand) preRunE(_ *cobra.Command, _ []string) error {
|
||||||
dbManager, err := lintersdb.NewManager(c.log.Child(logutils.DebugKeyLintersDB), c.cfg,
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package config
|
||||||
import (
|
import (
|
||||||
"encoding"
|
"encoding"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
@ -281,7 +282,17 @@ type LintersSettings struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LintersSettings) Validate() error {
|
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 {
|
type AsasalintSettings struct {
|
||||||
|
@ -946,17 +957,15 @@ type WSLSettings struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CustomLinterSettings encapsulates the meta-data of a private linter.
|
// 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 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.
|
// Path to a plugin *.so file that implements the private linter.
|
||||||
|
// Only for Go plugin system.
|
||||||
Path string
|
Path string
|
||||||
|
|
||||||
// Description describes the purpose of the private linter.
|
// Description describes the purpose of the private linter.
|
||||||
Description string
|
Description string
|
||||||
// OriginalURL The URL containing the source code for the private linter.
|
// 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 plugin settings only work with linterdb.PluginConstructor symbol.
|
||||||
Settings any
|
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.
|
// Build loads all the "internal" linters.
|
||||||
// The configuration is use for the linter settings.
|
// 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 {
|
if cfg == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const megacheckName = "megacheck"
|
const megacheckName = "megacheck"
|
||||||
|
@ -707,5 +707,5 @@ func (b LinterBuilder) Build(cfg *config.Config) []*linter.Config {
|
||||||
WithSince("v1.26.0").
|
WithSince("v1.26.0").
|
||||||
WithPresets(linter.PresetStyle).
|
WithPresets(linter.PresetStyle).
|
||||||
WithURL("https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint/README.md"),
|
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"
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const goPluginType = "goplugin"
|
||||||
|
|
||||||
type AnalyzerPlugin interface {
|
type AnalyzerPlugin interface {
|
||||||
GetAnalyzers() []*analysis.Analyzer
|
GetAnalyzers() []*analysis.Analyzer
|
||||||
}
|
}
|
||||||
|
|
||||||
// PluginBuilder builds the custom linters (plugins) based on the configuration.
|
// PluginGoBuilder builds the custom linters (Go plugin) based on the configuration.
|
||||||
type PluginBuilder struct {
|
type PluginGoBuilder struct {
|
||||||
log logutils.Log
|
log logutils.Log
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPluginBuilder creates new PluginBuilder.
|
// NewPluginGoBuilder creates new PluginGoBuilder.
|
||||||
func NewPluginBuilder(log logutils.Log) *PluginBuilder {
|
func NewPluginGoBuilder(log logutils.Log) *PluginGoBuilder {
|
||||||
return &PluginBuilder{log: log}
|
return &PluginGoBuilder{log: log}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build loads custom linters that are specified in the golangci-lint config file.
|
// 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 {
|
if cfg == nil || b.log == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var linters []*linter.Config
|
var linters []*linter.Config
|
||||||
|
|
||||||
for name, settings := range cfg.LintersSettings.Custom {
|
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 {
|
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 {
|
} else {
|
||||||
linters = append(linters, lc)
|
linters = append(linters, lc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return linters
|
return linters, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadConfig loads the configuration of private linters.
|
// loadConfig loads the configuration of private linters.
|
||||||
// Private linters are dynamically loaded from .so plugin files.
|
// 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)
|
analyzers, err := b.getAnalyzerPlugin(cfg, settings.Path, settings.Settings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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.
|
// and returns the 'AnalyzerPlugin' interface implemented by the private plugin.
|
||||||
// An error is returned if the private linter cannot be loaded
|
// An error is returned if the private linter cannot be loaded
|
||||||
// or the linter does not implement the AnalyzerPlugin interface.
|
// 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) {
|
if !filepath.IsAbs(path) {
|
||||||
// resolve non-absolute paths relative to config file's directory
|
// resolve non-absolute paths relative to config file's directory
|
||||||
path = filepath.Join(cfg.GetConfigDir(), path)
|
path = filepath.Join(cfg.GetConfigDir(), path)
|
||||||
|
@ -93,7 +101,7 @@ func (b *PluginBuilder) getAnalyzerPlugin(cfg *config.Config, path string, setti
|
||||||
return analyzers, nil
|
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")
|
symbol, err := plug.Lookup("New")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
analyzers, errP := b.lookupAnalyzerPlugin(plug)
|
analyzers, errP := b.lookupAnalyzerPlugin(plug)
|
||||||
|
@ -113,7 +121,7 @@ func (b *PluginBuilder) lookupPlugin(plug *plugin.Plugin, settings any) ([]*anal
|
||||||
return constructor(settings)
|
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")
|
symbol, err := plug.Lookup("AnalyzerPlugin")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
package lintersdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
|
@ -17,7 +18,7 @@ import (
|
||||||
const EnvTestRun = "GL_TEST_RUN"
|
const EnvTestRun = "GL_TEST_RUN"
|
||||||
|
|
||||||
type Builder interface {
|
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).
|
// 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 {
|
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 {
|
for _, lc := range m.linters {
|
||||||
|
|
|
@ -34,8 +34,7 @@ func newNolint2FileIssue(line int) result.Issue {
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTestNolintProcessor(log logutils.Log) *Nolint {
|
func newTestNolintProcessor(log logutils.Log) *Nolint {
|
||||||
dbManager, _ := lintersdb.NewManager(log, config.NewDefault(),
|
dbManager, _ := lintersdb.NewManager(log, config.NewDefault(), lintersdb.NewLinterBuilder())
|
||||||
lintersdb.NewPluginBuilder(log), lintersdb.NewLinterBuilder())
|
|
||||||
|
|
||||||
return NewNolint(log, dbManager, nil)
|
return NewNolint(log, dbManager, nil)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue