---
title: Architecture
---

import ResponsiveContainer from "components/ResponsiveContainer";

There are the following `golangci-lint` execution steps:

<ResponsiveContainer>

```mermaid
graph LR
    init[Init]
    loadPackages[Load packages]
    runLinters[Run linters]
    postprocess[Postprocess issues]
    print[Print issues]

    init --> loadPackages --> runLinters --> postprocess --> print

```

</ResponsiveContainer>

## Init

The execution starts here:

```go title=cmd/golangci-lint/main.go
func main() {
	e := commands.NewExecutor(version, commit, date)

	if err := e.Execute(); err != nil {
		fmt.Fprintf(os.Stderr, "failed executing command with error %v\n", err)
		os.Exit(exitcodes.Failure)
	}
}
```

The **executer** is our abstraction:

```go title=pkg/commands/executor.go
type Executor struct {
	rootCmd    *cobra.Command
	runCmd     *cobra.Command
	lintersCmd *cobra.Command

	exitCode              int

	cfg               *config.Config
	log               logutils.Log
	reportData        report.Data
	DBManager         *lintersdb.Manager
	EnabledLintersSet *lintersdb.EnabledSet
	contextLoader     *lint.ContextLoader
	goenv             *goutil.Env
	fileCache         *fsutils.FileCache
	lineCache         *fsutils.LineCache
	pkgCache          *pkgcache.Cache
	debugf            logutils.DebugFunc
	sw                *timeutils.Stopwatch

	loadGuard *load.Guard
	flock     *flock.Flock
}
```

We use dependency injection and all root dependencies are stored in this executor.

In the function `NewExecutor` we do the following:

1. init dependencies
2. init [cobra](https://github.com/spf13/cobra) commands
3. parse config file using [viper](https://github.com/spf13/viper) and merge it with command line args.

The following execution is controlled by `cobra`. If a user executes `golangci-lint run`
then `cobra` executes `e.runCmd`.

Different `cobra` commands have different runners, e.g. a `run` command is configured in the following way:

```go title=pkg/commands/run.go
func (e *Executor) initRun() {
	e.runCmd = &cobra.Command{
		Use:   "run",
		Short: welcomeMessage,
		Run:   e.executeRun,
		PreRun: func(_ *cobra.Command, _ []string) {
			if ok := e.acquireFileLock(); !ok {
				e.log.Fatalf("Parallel golangci-lint is running")
			}
		},
		PostRun: func(_ *cobra.Command, _ []string) {
			e.releaseFileLock()
		},
	}
	e.rootCmd.AddCommand(e.runCmd)
	e.runCmd.SetOutput(logutils.StdOut) // use custom output to properly color it in Windows terminals
	e.initRunConfiguration(e.runCmd)
}
```

The primary execution function of the `run` command is `executeRun`.

## Load Packages

Loading packages is listing all packages and their recursive dependencies for analysis.
Also, depending on the enabled linters set some parsing of the source code can be performed
at this step.

Packages loading starts here:

```go title=pkg/lint/load.go
func (cl *ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) {
	loadMode := cl.findLoadMode(linters)
	pkgs, err := cl.loadPackages(ctx, loadMode)
	if err != nil {
		return nil, err
	}

    // ...
    ret := &linter.Context{
        // ...
    }
	return ret, nil
}
```

First, we find a load mode as union of load modes for all enabled linters.
We use [go/packages](https://pkg.go.dev/golang.org/x/tools/go/packages) for packages loading and use it's enum `packages.Need*` for load modes.
Load mode sets which data does a linter needs for execution.

A linter that works only with AST need minimum of information: only filenames and AST. There is no need for
packages dependencies or type information. AST is built during `go/analysis` execution to reduce memory usage.
Such AST-based linters are configured with the following code:

```go title=pkg/lint/linter/config.go
func (lc *Config) WithLoadFiles() *Config {
	lc.LoadMode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles
	return lc
}
```

If a linter uses `go/analysis` and needs type information, we need to extract more data by `go/packages`:

```go title=/pkg/lint/linter/config.go
func (lc *Config) WithLoadForGoAnalysis() *Config {
	lc = lc.WithLoadFiles()
	lc.LoadMode |= packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | packages.NeedTypesSizes
	return lc
}
```

After finding a load mode, we run `go/packages`: the library get list of dirs (or `./...` as the default value) as input
and outputs list of packages and requested information about them: filenames, type information, AST, etc.

## Run Linters

First, we need to find all enabled linters. All linters are registered here:

```go title=pkg/lint/lintersdb/manager.go
func (m Manager) GetAllSupportedLinterConfigs() []*linter.Config {
    // ...
	lcs := []*linter.Config{
		linter.NewConfig(golinters.NewGovet(govetCfg)).
			WithLoadForGoAnalysis().
			WithPresets(linter.PresetBugs).
			WithAlternativeNames("vet", "vetshadow").
			WithURL("https://pkg.go.dev/cmd/vet"),
		linter.NewConfig(golinters.NewBodyclose()).
			WithLoadForGoAnalysis().
			WithPresets(linter.PresetPerformance, linter.PresetBugs).
            WithURL("https://github.com/timakin/bodyclose"),
        // ...
    }
    // ...
}
```

We filter requested in config and command-line linters in `EnabledSet`:

```go title=pkg/lint/lintersdb/enabled_set.go
func (es EnabledSet) GetEnabledLintersMap() (map[string]*linter.Config, error)
```

We merge enabled linters into one `MetaLinter` to improve execution time if we can:

```go title=pkg/lint/lintersdb/enabled_set.go
// GetOptimizedLinters returns enabled linters after optimization (merging) of multiple linters
// into a fewer number of linters. E.g. some go/analysis linters can be optimized into
// one metalinter for data reuse and speed up.
func (es EnabledSet) GetOptimizedLinters() ([]*linter.Config, error) {
    // ...
    es.combineGoAnalysisLinters(resultLintersSet)
    // ...
}
```

The `MetaLinter` just stores all merged linters inside to run them at once:

```go title=pkg/golinters/goanalysis/metalinter.go
type MetaLinter struct {
	linters              []*Linter
	analyzerToLinterName map[*analysis.Analyzer]string
}
```

Currently, all linters except `unused` can be merged into this meta linter.
The `unused` isn't merged because it has high memory usage.

Linters execution starts in `runAnalyzers`. It's the most complex part of the `golangci-lint`.
We use custom [go/analysis](https://pkg.go.dev/golang.org/x/tools/go/analysis) runner there. It runs as much as it can in parallel. It lazy-loads as much as it can
to reduce memory usage. Also, it sets all heavyweight data to `nil` as becomes unneeded to save memory.

We don't use existing [multichecker](https://pkg.go.dev/golang.org/x/tools/go/analysis/multichecker) because
it doesn't use caching and doesn't have some important performance optimizations.

All found by linters issues are represented with `result.Issue` struct:

```go title=pkg/result/issue.go
type Issue struct {
	FromLinter string
	Text       string

	// Source lines of a code with the issue to show
	SourceLines []string

	// If we know how to fix the issue we can provide replacement lines
	Replacement *Replacement

	// Pkg is needed for proper caching of linting results
	Pkg *packages.Package `json:"-"`

	LineRange *Range `json:",omitempty"`

	Pos token.Position

	// HunkPos is used only when golangci-lint is run over a diff
	HunkPos int `json:",omitempty"`

	// If we are expecting a nolint (because this is from nolintlint), record the expected linter
	ExpectNoLint         bool
	ExpectedNoLintLinter string
}
```

## Postprocess Issues

We have an abstraction of `result.Processor` to postprocess found issues:

```sh
$ tree -L 1 ./pkg/result/processors/
./pkg/result/processors/
├── autogenerated_exclude.go
├── autogenerated_exclude_test.go
├── cgo.go
├── diff.go
├── exclude.go
├── exclude_rules.go
├── exclude_rules_test.go
├── exclude_test.go
├── filename_unadjuster.go
├── fixer.go
├── identifier_marker.go
├── identifier_marker_test.go
├── max_from_linter.go
├── max_from_linter_test.go
├── max_per_file_from_linter.go
├── max_per_file_from_linter_test.go
├── max_same_issues.go
├── max_same_issues_test.go
├── nolint.go
├── nolint_test.go
├── path_prettifier.go
├── path_shortener.go
├── processor.go
├── skip_dirs.go
├── skip_files.go
├── skip_files_test.go
├── source_code.go
├── testdata
├── uniq_by_line.go
├── uniq_by_line_test.go
└── utils.go
```

The abstraction is simple:

```go title=pkg/result/processors/processor.go
type Processor interface {
	Process(issues []result.Issue) ([]result.Issue, error)
	Name() string
	Finish()
}
```

A processor can hide issues (`nolint`, `exclude`) or change issues (`path_shortener`).

## Print Issues

We have an abstraction for printing found issues.

```sh
$ tree -L 1 ./pkg/printers/
./pkg/printers/
├── checkstyle.go
├── codeclimate.go
├── github.go
├── github_test.go
├── json.go
├── junitxml.go
├── printer.go
├── tab.go
└── text.go
```

Needed printer is selected by command line option `--out-format`.
