2024-03-26 20:55:39 +01:00

311 lines
7.7 KiB
Go

package lintersdb
import (
"fmt"
"os"
"slices"
"sort"
"golang.org/x/exp/maps"
"github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/goanalysis"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
)
type Builder interface {
Build(cfg *config.Config) ([]*linter.Config, error)
}
// Manager is a type of database for all linters (internals or plugins).
// It provides methods to access to the linter sets.
type Manager struct {
log logutils.Log
debugf logutils.DebugFunc
cfg *config.Config
linters []*linter.Config
nameToLCs map[string][]*linter.Config
}
// NewManager creates a new Manager.
// This constructor will call the builders to build and store the linters.
func NewManager(log logutils.Log, cfg *config.Config, builders ...Builder) (*Manager, error) {
m := &Manager{
log: log,
debugf: logutils.Debug(logutils.DebugKeyEnabledLinters),
nameToLCs: make(map[string][]*linter.Config),
}
m.cfg = cfg
if cfg == nil {
m.cfg = config.NewDefault()
}
for _, builder := range builders {
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 _, name := range lc.AllNames() {
m.nameToLCs[name] = append(m.nameToLCs[name], lc)
}
}
err := NewValidator(m).Validate(m.cfg)
if err != nil {
return nil, err
}
return m, nil
}
func (m *Manager) GetLinterConfigs(name string) []*linter.Config {
return m.nameToLCs[name]
}
func (m *Manager) GetAllSupportedLinterConfigs() []*linter.Config {
return m.linters
}
func (m *Manager) GetAllLinterConfigsForPreset(p string) []*linter.Config {
var ret []*linter.Config
for _, lc := range m.linters {
if lc.IsDeprecated() {
continue
}
if slices.Contains(lc.InPresets, p) {
ret = append(ret, lc)
}
}
return ret
}
func (m *Manager) GetEnabledLintersMap() (map[string]*linter.Config, error) {
enabledLinters := m.build(m.GetAllEnabledByDefaultLinters())
if os.Getenv(logutils.EnvTestRun) == "1" {
m.verbosePrintLintersStatus(enabledLinters)
}
return enabledLinters, nil
}
// 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 (m *Manager) GetOptimizedLinters() ([]*linter.Config, error) {
resultLintersSet := m.build(m.GetAllEnabledByDefaultLinters())
m.verbosePrintLintersStatus(resultLintersSet)
m.combineGoAnalysisLinters(resultLintersSet)
resultLinters := maps.Values(resultLintersSet)
// Make order of execution of linters (go/analysis metalinter and unused) stable.
sort.Slice(resultLinters, func(i, j int) bool {
a, b := resultLinters[i], resultLinters[j]
if b.Name() == linter.LastLinter {
return true
}
if a.Name() == linter.LastLinter {
return false
}
if a.DoesChangeTypes != b.DoesChangeTypes {
return b.DoesChangeTypes // move type-changing linters to the end to optimize speed
}
return a.Name() < b.Name()
})
return resultLinters, nil
}
func (m *Manager) GetAllEnabledByDefaultLinters() []*linter.Config {
var ret []*linter.Config
for _, lc := range m.linters {
if lc.EnabledByDefault {
ret = append(ret, lc)
}
}
return ret
}
//nolint:gocyclo // the complexity cannot be reduced.
func (m *Manager) build(enabledByDefaultLinters []*linter.Config) map[string]*linter.Config {
m.debugf("Linters config: %#v", m.cfg.Linters)
resultLintersSet := map[string]*linter.Config{}
switch {
case m.cfg.Linters.DisableAll:
// no default linters
case len(m.cfg.Linters.Presets) != 0:
// imply --disable-all
case m.cfg.Linters.EnableAll:
resultLintersSet = linterConfigsToMap(m.linters)
default:
resultLintersSet = linterConfigsToMap(enabledByDefaultLinters)
}
// --presets can only add linters to default set
for _, p := range m.cfg.Linters.Presets {
for _, lc := range m.GetAllLinterConfigsForPreset(p) {
lc := lc
resultLintersSet[lc.Name()] = lc
}
}
// --fast removes slow linters from current set.
// It should be after --presets to be able to run only fast linters in preset.
// It should be before --enable and --disable to be able to enable or disable specific linter.
if m.cfg.Linters.Fast {
for name, lc := range resultLintersSet {
if lc.IsSlowLinter() {
delete(resultLintersSet, name)
}
}
}
for _, name := range m.cfg.Linters.Enable {
for _, lc := range m.GetLinterConfigs(name) {
// it's important to use lc.Name() nor name because name can be alias
resultLintersSet[lc.Name()] = lc
}
}
for _, name := range m.cfg.Linters.Disable {
for _, lc := range m.GetLinterConfigs(name) {
// it's important to use lc.Name() nor name because name can be alias
delete(resultLintersSet, lc.Name())
}
}
// typecheck is not a real linter and cannot be disabled.
if _, ok := resultLintersSet["typecheck"]; !ok && (m.cfg == nil || !m.cfg.InternalCmdTest) {
for _, lc := range m.GetLinterConfigs("typecheck") {
// it's important to use lc.Name() nor name because name can be alias
resultLintersSet[lc.Name()] = lc
}
}
return resultLintersSet
}
func (m *Manager) combineGoAnalysisLinters(linters map[string]*linter.Config) {
var goanalysisLinters []*goanalysis.Linter
goanalysisPresets := map[string]bool{}
for _, lc := range linters {
lnt, ok := lc.Linter.(*goanalysis.Linter)
if !ok {
continue
}
if lnt.LoadMode() == goanalysis.LoadModeWholeProgram {
// It's ineffective by CPU and memory to run whole-program and incremental analyzers at once.
continue
}
goanalysisLinters = append(goanalysisLinters, lnt)
for _, p := range lc.InPresets {
goanalysisPresets[p] = true
}
}
if len(goanalysisLinters) <= 1 {
m.debugf("Didn't combine go/analysis linters: got only %d linters", len(goanalysisLinters))
return
}
for _, lnt := range goanalysisLinters {
delete(linters, lnt.Name())
}
// Make order of execution of go/analysis analyzers stable.
sort.Slice(goanalysisLinters, func(i, j int) bool {
a, b := goanalysisLinters[i], goanalysisLinters[j]
if b.Name() == linter.LastLinter {
return true
}
if a.Name() == linter.LastLinter {
return false
}
return a.Name() <= b.Name()
})
ml := goanalysis.NewMetaLinter(goanalysisLinters)
presets := maps.Keys(goanalysisPresets)
sort.Strings(presets)
mlConfig := &linter.Config{
Linter: ml,
EnabledByDefault: false,
InPresets: presets,
AlternativeNames: nil,
OriginalURL: "",
}
mlConfig = mlConfig.WithLoadForGoAnalysis()
linters[ml.Name()] = mlConfig
m.debugf("Combined %d go/analysis linters into one metalinter", len(goanalysisLinters))
}
func (m *Manager) verbosePrintLintersStatus(lcs map[string]*linter.Config) {
var linterNames []string
for _, lc := range lcs {
if lc.Internal {
continue
}
linterNames = append(linterNames, lc.Name())
}
sort.Strings(linterNames)
m.log.Infof("Active %d linters: %s", len(linterNames), linterNames)
if len(m.cfg.Linters.Presets) != 0 {
sort.Strings(m.cfg.Linters.Presets)
m.log.Infof("Active presets: %s", m.cfg.Linters.Presets)
}
}
func AllPresets() []string {
return []string{
linter.PresetBugs,
linter.PresetComment,
linter.PresetComplexity,
linter.PresetError,
linter.PresetFormatting,
linter.PresetImport,
linter.PresetMetaLinter,
linter.PresetModule,
linter.PresetPerformance,
linter.PresetSQL,
linter.PresetStyle,
linter.PresetTest,
linter.PresetUnused,
}
}
func linterConfigsToMap(lcs []*linter.Config) map[string]*linter.Config {
ret := map[string]*linter.Config{}
for _, lc := range lcs {
ret[lc.Name()] = lc
}
return ret
}