github.com/vanstinator/golangci-lint@v0.0.0-20240223191551-cc572f00d9d1/pkg/lint/runner.go (about)

     1  package lint
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"runtime/debug"
     8  	"strings"
     9  
    10  	gopackages "golang.org/x/tools/go/packages"
    11  
    12  	"github.com/vanstinator/golangci-lint/internal/errorutil"
    13  	"github.com/vanstinator/golangci-lint/pkg/config"
    14  	"github.com/vanstinator/golangci-lint/pkg/fsutils"
    15  	"github.com/vanstinator/golangci-lint/pkg/goutil"
    16  	"github.com/vanstinator/golangci-lint/pkg/lint/linter"
    17  	"github.com/vanstinator/golangci-lint/pkg/lint/lintersdb"
    18  	"github.com/vanstinator/golangci-lint/pkg/logutils"
    19  	"github.com/vanstinator/golangci-lint/pkg/packages"
    20  	"github.com/vanstinator/golangci-lint/pkg/result"
    21  	"github.com/vanstinator/golangci-lint/pkg/result/processors"
    22  	"github.com/vanstinator/golangci-lint/pkg/timeutils"
    23  )
    24  
    25  type Runner struct {
    26  	Processors []processors.Processor
    27  	Log        logutils.Log
    28  }
    29  
    30  func NewRunner(cfg *config.Config, log logutils.Log, goenv *goutil.Env,
    31  	es *lintersdb.EnabledSet,
    32  	lineCache *fsutils.LineCache, fileCache *fsutils.FileCache,
    33  	dbManager *lintersdb.Manager, pkgs []*gopackages.Package) (*Runner, error) {
    34  	// Beware that some processors need to add the path prefix when working with paths
    35  	// because they get invoked before the path prefixer (exclude and severity rules)
    36  	// or process other paths (skip files).
    37  	files := fsutils.NewFiles(lineCache, cfg.Output.PathPrefix)
    38  
    39  	skipFilesProcessor, err := processors.NewSkipFiles(cfg.Run.SkipFiles, cfg.Output.PathPrefix)
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  
    44  	skipDirs := cfg.Run.SkipDirs
    45  	if cfg.Run.UseDefaultSkipDirs {
    46  		skipDirs = append(skipDirs, packages.StdExcludeDirRegexps...)
    47  	}
    48  	skipDirsProcessor, err := processors.NewSkipDirs(skipDirs, log.Child(logutils.DebugKeySkipDirs), cfg.Run.Args, cfg.Output.PathPrefix)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	enabledLinters, err := es.GetEnabledLintersMap()
    54  	if err != nil {
    55  		return nil, fmt.Errorf("failed to get enabled linters: %w", err)
    56  	}
    57  
    58  	// print deprecated messages
    59  	if !cfg.InternalCmdTest {
    60  		for name, lc := range enabledLinters {
    61  			if !lc.IsDeprecated() {
    62  				continue
    63  			}
    64  
    65  			var extra string
    66  			if lc.Deprecation.Replacement != "" {
    67  				extra = fmt.Sprintf("Replaced by %s.", lc.Deprecation.Replacement)
    68  			}
    69  
    70  			log.Warnf("The linter '%s' is deprecated (since %s) due to: %s %s", name, lc.Deprecation.Since, lc.Deprecation.Message, extra)
    71  		}
    72  	}
    73  
    74  	return &Runner{
    75  		Processors: []processors.Processor{
    76  			processors.NewCgo(goenv),
    77  
    78  			// Must go after Cgo.
    79  			processors.NewFilenameUnadjuster(pkgs, log.Child(logutils.DebugKeyFilenameUnadjuster)),
    80  
    81  			// Must be before diff, nolint and exclude autogenerated processor at least.
    82  			processors.NewPathPrettifier(),
    83  			skipFilesProcessor,
    84  			skipDirsProcessor, // must be after path prettifier
    85  
    86  			processors.NewAutogeneratedExclude(),
    87  
    88  			// Must be before exclude because users see already marked output and configure excluding by it.
    89  			processors.NewIdentifierMarker(),
    90  
    91  			getExcludeProcessor(&cfg.Issues),
    92  			getExcludeRulesProcessor(&cfg.Issues, log, files),
    93  			processors.NewNolint(log.Child(logutils.DebugKeyNolint), dbManager, enabledLinters),
    94  
    95  			processors.NewUniqByLine(cfg),
    96  			processors.NewDiff(cfg.Issues.Diff, cfg.Issues.DiffFromRevision, cfg.Issues.DiffPatchFilePath, cfg.Issues.WholeFiles),
    97  			processors.NewMaxPerFileFromLinter(cfg),
    98  			processors.NewMaxSameIssues(cfg.Issues.MaxSameIssues, log.Child(logutils.DebugKeyMaxSameIssues), cfg),
    99  			processors.NewMaxFromLinter(cfg.Issues.MaxIssuesPerLinter, log.Child(logutils.DebugKeyMaxFromLinter), cfg),
   100  			processors.NewSourceCode(lineCache, log.Child(logutils.DebugKeySourceCode)),
   101  			processors.NewPathShortener(),
   102  			getSeverityRulesProcessor(&cfg.Severity, log, files),
   103  
   104  			// The fixer still needs to see paths for the issues that are relative to the current directory.
   105  			processors.NewFixer(cfg, log, fileCache),
   106  
   107  			// Now we can modify the issues for output.
   108  			processors.NewPathPrefixer(cfg.Output.PathPrefix),
   109  			processors.NewSortResults(cfg),
   110  		},
   111  		Log: log,
   112  	}, nil
   113  }
   114  
   115  func (r *Runner) runLinterSafe(ctx context.Context, lintCtx *linter.Context,
   116  	lc *linter.Config) (ret []result.Issue, err error) {
   117  	defer func() {
   118  		if panicData := recover(); panicData != nil {
   119  			if pe, ok := panicData.(*errorutil.PanicError); ok {
   120  				err = fmt.Errorf("%s: %w", lc.Name(), pe)
   121  
   122  				// Don't print stacktrace from goroutines twice
   123  				r.Log.Errorf("Panic: %s: %s", pe, pe.Stack())
   124  			} else {
   125  				err = fmt.Errorf("panic occurred: %s", panicData)
   126  				r.Log.Errorf("Panic stack trace: %s", debug.Stack())
   127  			}
   128  		}
   129  	}()
   130  
   131  	issues, err := lc.Linter.Run(ctx, lintCtx)
   132  
   133  	if lc.DoesChangeTypes {
   134  		// Packages in lintCtx might be dirty due to the last analysis,
   135  		// which affects to the next analysis.
   136  		// To avoid this issue, we clear type information from the packages.
   137  		// See https://github.com/vanstinator/golangci-lint/pull/944.
   138  		// Currently, DoesChangeTypes is true only for `unused`.
   139  		lintCtx.ClearTypesInPackages()
   140  	}
   141  
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  
   146  	for i := range issues {
   147  		if issues[i].FromLinter == "" {
   148  			issues[i].FromLinter = lc.Name()
   149  		}
   150  	}
   151  
   152  	return issues, nil
   153  }
   154  
   155  type processorStat struct {
   156  	inCount  int
   157  	outCount int
   158  }
   159  
   160  func (r Runner) processLintResults(inIssues []result.Issue) []result.Issue {
   161  	sw := timeutils.NewStopwatch("processing", r.Log)
   162  
   163  	var issuesBefore, issuesAfter int
   164  	statPerProcessor := map[string]processorStat{}
   165  
   166  	var outIssues []result.Issue
   167  	if len(inIssues) != 0 {
   168  		issuesBefore += len(inIssues)
   169  		outIssues = r.processIssues(inIssues, sw, statPerProcessor)
   170  		issuesAfter += len(outIssues)
   171  	}
   172  
   173  	// finalize processors: logging, clearing, no heavy work here
   174  
   175  	for _, p := range r.Processors {
   176  		p := p
   177  		sw.TrackStage(p.Name(), func() {
   178  			p.Finish()
   179  		})
   180  	}
   181  
   182  	if issuesBefore != issuesAfter {
   183  		r.Log.Infof("Issues before processing: %d, after processing: %d", issuesBefore, issuesAfter)
   184  	}
   185  	r.printPerProcessorStat(statPerProcessor)
   186  	sw.PrintStages()
   187  
   188  	return outIssues
   189  }
   190  
   191  func (r Runner) printPerProcessorStat(stat map[string]processorStat) {
   192  	parts := make([]string, 0, len(stat))
   193  	for name, ps := range stat {
   194  		if ps.inCount != 0 {
   195  			parts = append(parts, fmt.Sprintf("%s: %d/%d", name, ps.outCount, ps.inCount))
   196  		}
   197  	}
   198  	if len(parts) != 0 {
   199  		r.Log.Infof("Processors filtering stat (out/in): %s", strings.Join(parts, ", "))
   200  	}
   201  }
   202  
   203  func (r Runner) Run(ctx context.Context, linters []*linter.Config, lintCtx *linter.Context) ([]result.Issue, error) {
   204  	sw := timeutils.NewStopwatch("linters", r.Log)
   205  	defer sw.Print()
   206  
   207  	var (
   208  		lintErrors error
   209  		issues     []result.Issue
   210  	)
   211  
   212  	for _, lc := range linters {
   213  		lc := lc
   214  		sw.TrackStage(lc.Name(), func() {
   215  			linterIssues, err := r.runLinterSafe(ctx, lintCtx, lc)
   216  			if err != nil {
   217  				lintErrors = errors.Join(lintErrors, fmt.Errorf("can't run linter %s", lc.Linter.Name()), err)
   218  				r.Log.Warnf("Can't run linter %s: %v", lc.Linter.Name(), err)
   219  
   220  				return
   221  			}
   222  
   223  			issues = append(issues, linterIssues...)
   224  		})
   225  	}
   226  
   227  	return r.processLintResults(issues), lintErrors
   228  }
   229  
   230  func (r *Runner) processIssues(issues []result.Issue, sw *timeutils.Stopwatch, statPerProcessor map[string]processorStat) []result.Issue {
   231  	for _, p := range r.Processors {
   232  		var newIssues []result.Issue
   233  		var err error
   234  		p := p
   235  		sw.TrackStage(p.Name(), func() {
   236  			newIssues, err = p.Process(issues)
   237  		})
   238  
   239  		if err != nil {
   240  			r.Log.Warnf("Can't process result by %s processor: %s", p.Name(), err)
   241  		} else {
   242  			stat := statPerProcessor[p.Name()]
   243  			stat.inCount += len(issues)
   244  			stat.outCount += len(newIssues)
   245  			statPerProcessor[p.Name()] = stat
   246  			issues = newIssues
   247  		}
   248  
   249  		if issues == nil {
   250  			issues = []result.Issue{}
   251  		}
   252  	}
   253  
   254  	return issues
   255  }
   256  
   257  func getExcludeProcessor(cfg *config.Issues) processors.Processor {
   258  	var excludeTotalPattern string
   259  
   260  	if len(cfg.ExcludePatterns) != 0 {
   261  		excludeTotalPattern = fmt.Sprintf("(%s)", strings.Join(cfg.ExcludePatterns, "|"))
   262  	}
   263  
   264  	var excludeProcessor processors.Processor
   265  	if cfg.ExcludeCaseSensitive {
   266  		excludeProcessor = processors.NewExcludeCaseSensitive(excludeTotalPattern)
   267  	} else {
   268  		excludeProcessor = processors.NewExclude(excludeTotalPattern)
   269  	}
   270  
   271  	return excludeProcessor
   272  }
   273  
   274  func getExcludeRulesProcessor(cfg *config.Issues, log logutils.Log, files *fsutils.Files) processors.Processor {
   275  	var excludeRules []processors.ExcludeRule
   276  	for _, r := range cfg.ExcludeRules {
   277  		excludeRules = append(excludeRules, processors.ExcludeRule{
   278  			BaseRule: processors.BaseRule{
   279  				Text:       r.Text,
   280  				Source:     r.Source,
   281  				Path:       r.Path,
   282  				PathExcept: r.PathExcept,
   283  				Linters:    r.Linters,
   284  			},
   285  		})
   286  	}
   287  
   288  	if cfg.UseDefaultExcludes {
   289  		for _, r := range config.GetExcludePatterns(cfg.IncludeDefaultExcludes) {
   290  			excludeRules = append(excludeRules, processors.ExcludeRule{
   291  				BaseRule: processors.BaseRule{
   292  					Text:    r.Pattern,
   293  					Linters: []string{r.Linter},
   294  				},
   295  			})
   296  		}
   297  	}
   298  
   299  	var excludeRulesProcessor processors.Processor
   300  	if cfg.ExcludeCaseSensitive {
   301  		excludeRulesProcessor = processors.NewExcludeRulesCaseSensitive(
   302  			excludeRules,
   303  			files,
   304  			log.Child(logutils.DebugKeyExcludeRules),
   305  		)
   306  	} else {
   307  		excludeRulesProcessor = processors.NewExcludeRules(
   308  			excludeRules,
   309  			files,
   310  			log.Child(logutils.DebugKeyExcludeRules),
   311  		)
   312  	}
   313  
   314  	return excludeRulesProcessor
   315  }
   316  
   317  func getSeverityRulesProcessor(cfg *config.Severity, log logutils.Log, files *fsutils.Files) processors.Processor {
   318  	var severityRules []processors.SeverityRule
   319  	for _, r := range cfg.Rules {
   320  		severityRules = append(severityRules, processors.SeverityRule{
   321  			Severity: r.Severity,
   322  			BaseRule: processors.BaseRule{
   323  				Text:       r.Text,
   324  				Source:     r.Source,
   325  				Path:       r.Path,
   326  				PathExcept: r.PathExcept,
   327  				Linters:    r.Linters,
   328  			},
   329  		})
   330  	}
   331  
   332  	var severityRulesProcessor processors.Processor
   333  	if cfg.CaseSensitive {
   334  		severityRulesProcessor = processors.NewSeverityRulesCaseSensitive(
   335  			cfg.Default,
   336  			severityRules,
   337  			files,
   338  			log.Child(logutils.DebugKeySeverityRules),
   339  		)
   340  	} else {
   341  		severityRulesProcessor = processors.NewSeverityRules(
   342  			cfg.Default,
   343  			severityRules,
   344  			files,
   345  			log.Child(logutils.DebugKeySeverityRules),
   346  		)
   347  	}
   348  
   349  	return severityRulesProcessor
   350  }