Table of Contents
There are the following golangci-lint
execution steps:
Init
The execution starts here:
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:
pkg/commands/executor.go
type Executor struct {rootCmd *cobra.CommandrunCmd *cobra.CommandlintersCmd *cobra.CommandexitCode intcfg *config.Configlog logutils.LogreportData report.DataDBManager *lintersdb.ManagerEnabledLintersSet *lintersdb.EnabledSetcontextLoader *lint.ContextLoadergoenv *goutil.EnvfileCache *fsutils.FileCachelineCache *fsutils.LineCachepkgCache *pkgcache.Cachedebugf logutils.DebugFuncsw *timeutils.StopwatchloadGuard *load.Guardflock *flock.Flock}
We use dependency injection and all root dependencies are stored in this executor.
In the function NewExecutor
we do the following:
- init dependencies
- init cobra commands
- parse config file using viper and merge it with command line args.
The following execution is controlled by cobra
. If user 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:
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 terminalse.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 from enabled linters set some parsing of a source code can be performed at this step.
Packages loading stars here:
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 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:
pkg/lint/linter/config.go
func (lc *Config) WithLoadFiles() *Config {lc.LoadMode |= packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFilesreturn lc}
If a linter uses go/analysis
and needs type information, we need to extract more data by go/packages
:
/pkg/lint/linter/config.go
func (lc *Config) WithLoadForGoAnalysis() *Config {lc = lc.WithLoadFiles()lc.LoadMode |= packages.NeedImports | packages.NeedDeps | packages.NeedExportsFile | packages.NeedTypesSizesreturn 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 enaled linters. All linters are registered here:
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://golang.org/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
:
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:
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:
pkg/golinters/goanalysis/metalinter.go
type MetaLinter struct {linters []*LinteranalyzerToLinterName 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 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 set all heavyweight data to nil
as becomes unneeded to save memory.
We don't use existing 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:
pkg/result/issue.go
type Issue struct {FromLinter stringText string// Source lines of a code with the issue to showSourceLines []string// If we know how to fix the issue we can provide replacement linesReplacement *Replacement// Pkg is needed for proper caching of linting resultsPkg *packages.Package `json:"-"`LineRange *Range `json:",omitempty"`Pos token.Position// HunkPos is used only when golangci-lint is run over a diffHunkPos int `json:",omitempty"`// If we are expecting a nolint (because this is from nolintlint), record the expected linterExpectNoLint boolExpectedNoLintLinter string}
Postprocess Issues
We have an abstraction of result.Processor
to postprocess found issues:
$ 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:
pkg/result/processors/processor.go
type Processor interface {Process(issues []result.Issue) ([]result.Issue, error)Name() stringFinish()}
A processor can hide issues (nolint
, exclude
) or change issues (path_shortener
).
Print Issues
We have an abstraction for printint found issues.
$ 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
.