diff --git a/.custom-gcl.reference.yml b/.custom-gcl.reference.yml new file mode 100644 index 000000000000..a068eaf64e90 --- /dev/null +++ b/.custom-gcl.reference.yml @@ -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 diff --git a/.gitignore b/.gitignore index bb2653285201..827702fbb926 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ /vendor/ coverage.out coverage.xml +/custom-golangci-lint +/custom-gcl +.custom-gcl.yml +.custom-gcl.yaml diff --git a/.golangci.reference.yml b/.golangci.reference.yml index 7ac8e56e98c8..d22e9be86839 100644 --- a/.golangci.reference.yml +++ b/.golangci.reference.yml @@ -2472,6 +2472,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 diff --git a/.golangci.yml b/.golangci.yml index 48c3d05e13a2..c9700f77a79b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/cmd/golangci-lint/plugins.go b/cmd/golangci-lint/plugins.go new file mode 100644 index 000000000000..541ff7624271 --- /dev/null +++ b/cmd/golangci-lint/plugins.go @@ -0,0 +1,3 @@ +package main + +// This file is used to declare module plugins. diff --git a/docs/src/config/sidebar.yml b/docs/src/config/sidebar.yml index b034cdb4c1b2..34ca440d4b00 100644 --- a/docs/src/config/sidebar.yml +++ b/docs/src/config/sidebar.yml @@ -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/ + diff --git a/docs/src/docs/contributing/faq.mdx b/docs/src/docs/contributing/faq.mdx index 9ed95b77dba2..1da0c75a0535 100644 --- a/docs/src/docs/contributing/faq.mdx +++ b/docs/src/docs/contributing/faq.mdx @@ -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 diff --git a/docs/src/docs/contributing/new-linters.mdx b/docs/src/docs/contributing/new-linters.mdx index 8e2551cb48c6..55da80e20fe9 100644 --- a/docs/src/docs/contributing/new-linters.mdx +++ b/docs/src/docs/contributing/new-linters.mdx @@ -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) diff --git a/docs/src/docs/plugins/go-plugins.mdx b/docs/src/docs/plugins/go-plugins.mdx new file mode 100644 index 000000000000..ea54811a6b99 --- /dev/null +++ b/docs/src/docs/plugins/go-plugins.mdx @@ -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. diff --git a/docs/src/docs/plugins/module-plugins.mdx b/docs/src/docs/plugins/module-plugins.mdx new file mode 100644 index 000000000000..a0f9d79b61b0 --- /dev/null +++ b/docs/src/docs/plugins/module-plugins.mdx @@ -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 +``` diff --git a/go.mod b/go.mod index 132f68d0afd7..811660608f8d 100644 --- a/go.mod +++ b/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-20180507085042-28b1c447d1f4 github.com/gordonklaus/ineffassign v0.1.0 diff --git a/go.sum b/go.sum index 852b05046c45..a82641724034 100644 --- a/go.sum +++ b/go.sum @@ -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-20180507085042-28b1c447d1f4 h1:zwtduBRr5SSWhqsYNgcuWO2kFlpdOZbP0+yRjmvPGys= diff --git a/pkg/commands/custom.go b/pkg/commands/custom.go new file mode 100644 index 000000000000..3ecb724b76d7 --- /dev/null +++ b/pkg/commands/custom.go @@ -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 +} diff --git a/pkg/commands/help.go b/pkg/commands/help.go index 655d1e675fe0..42da4a3dc9f6 100644 --- a/pkg/commands/help.go +++ b/pkg/commands/help.go @@ -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 } diff --git a/pkg/commands/internal/builder.go b/pkg/commands/internal/builder.go new file mode 100644 index 000000000000..dd7839251197 --- /dev/null +++ b/pkg/commands/internal/builder.go @@ -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), "") +} diff --git a/pkg/commands/internal/builder_test.go b/pkg/commands/internal/builder_test.go new file mode 100644 index 000000000000..508755cf2271 --- /dev/null +++ b/pkg/commands/internal/builder_test.go @@ -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) + }) + } +} diff --git a/pkg/commands/internal/configuration.go b/pkg/commands/internal/configuration.go new file mode 100644 index 000000000000..5327025946cc --- /dev/null +++ b/pkg/commands/internal/configuration.go @@ -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 +} diff --git a/pkg/commands/internal/configuration_test.go b/pkg/commands/internal/configuration_test.go new file mode 100644 index 000000000000..3c85a9075f52 --- /dev/null +++ b/pkg/commands/internal/configuration_test.go @@ -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) + }) + } +} diff --git a/pkg/commands/internal/imports.go b/pkg/commands/internal/imports.go new file mode 100644 index 000000000000..3bebf596b1ab --- /dev/null +++ b/pkg/commands/internal/imports.go @@ -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 +} diff --git a/pkg/commands/internal/imports_test.go b/pkg/commands/internal/imports_test.go new file mode 100644 index 000000000000..049e55e7f1f1 --- /dev/null +++ b/pkg/commands/internal/imports_test.go @@ -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) +} diff --git a/pkg/commands/internal/testdata/imports.go b/pkg/commands/internal/testdata/imports.go new file mode 100644 index 000000000000..c3c4f1312492 --- /dev/null +++ b/pkg/commands/internal/testdata/imports.go @@ -0,0 +1,6 @@ +package main + +import ( + _ "example.com/foo/bar/test" + _ "example.org/foo/bar/test" +) diff --git a/pkg/commands/linters.go b/pkg/commands/linters.go index 30d1958c1ee6..d474579bdcec 100644 --- a/pkg/commands/linters.go +++ b/pkg/commands/linters.go @@ -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 } diff --git a/pkg/commands/root.go b/pkg/commands/root.go index 2a5e7cf1768e..d473f7609c8e 100644 --- a/pkg/commands/root.go +++ b/pkg/commands/root.go @@ -66,6 +66,7 @@ func newRootCommand(info BuildInfo) *rootCommand { newCacheCommand().cmd, newConfigCommand(log).cmd, newVersionCommand(info).cmd, + newCustomCommand(log).cmd, ) rootCmd.SetHelpCommand(newHelpCommand(log).cmd) diff --git a/pkg/commands/run.go b/pkg/commands/run.go index 60f241af3a7f..414e847fef9f 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -177,7 +177,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 } diff --git a/pkg/config/linters_settings.go b/pkg/config/linters_settings.go index 319daca4b6dc..d6fd3d406b75 100644 --- a/pkg/config/linters_settings.go +++ b/pkg/config/linters_settings.go @@ -3,6 +3,7 @@ package config import ( "encoding" "errors" + "fmt" "runtime" "gopkg.in/yaml.v3" @@ -280,7 +281,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 { @@ -940,17 +951,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. @@ -959,3 +968,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 +} diff --git a/pkg/config/linters_settings_test.go b/pkg/config/linters_settings_test.go new file mode 100644 index 000000000000..1fd77d2e2215 --- /dev/null +++ b/pkg/config/linters_settings_test.go @@ -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) + }) + } +} diff --git a/pkg/lint/lintersdb/builder_linter.go b/pkg/lint/lintersdb/builder_linter.go index b9466a219f0e..6f748d80e2fa 100644 --- a/pkg/lint/lintersdb/builder_linter.go +++ b/pkg/lint/lintersdb/builder_linter.go @@ -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 } diff --git a/pkg/lint/lintersdb/builder_plugin.go b/pkg/lint/lintersdb/builder_plugin_go.go similarity index 72% rename from pkg/lint/lintersdb/builder_plugin.go rename to pkg/lint/lintersdb/builder_plugin_go.go index 401a645c3e76..4c6b4b5988f5 100644 --- a/pkg/lint/lintersdb/builder_plugin.go +++ b/pkg/lint/lintersdb/builder_plugin_go.go @@ -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 diff --git a/pkg/lint/lintersdb/builder_plugin_module.go b/pkg/lint/lintersdb/builder_plugin_module.go new file mode 100644 index 000000000000..904b18bd5e4d --- /dev/null +++ b/pkg/lint/lintersdb/builder_plugin_module.go @@ -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 +} diff --git a/pkg/lint/lintersdb/manager.go b/pkg/lint/lintersdb/manager.go index a55ef9a39244..57d45dd48c33 100644 --- a/pkg/lint/lintersdb/manager.go +++ b/pkg/lint/lintersdb/manager.go @@ -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 { diff --git a/pkg/result/processors/nolint_test.go b/pkg/result/processors/nolint_test.go index 1588a2e14520..d6fb56a80426 100644 --- a/pkg/result/processors/nolint_test.go +++ b/pkg/result/processors/nolint_test.go @@ -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) }