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

     1  // checker is a partial copy of https://github.com/golang/tools/blob/master/go/analysis/internal/checker
     2  // Copyright 2018 The Go Authors. All rights reserved.
     3  // Use of this source code is governed by a BSD-style
     4  // license that can be found in the LICENSE file.
     5  
     6  // Package goanalysis defines the implementation of the checker commands.
     7  // The same code drives the multi-analysis driver, the single-analysis
     8  // driver that is conventionally provided for convenience along with
     9  // each analysis package, and the test driver.
    10  package goanalysis
    11  
    12  import (
    13  	"encoding/gob"
    14  	"fmt"
    15  	"go/token"
    16  	"runtime"
    17  	"sort"
    18  	"sync"
    19  
    20  	"golang.org/x/exp/maps"
    21  	"golang.org/x/tools/go/analysis"
    22  	"golang.org/x/tools/go/packages"
    23  
    24  	"github.com/vanstinator/golangci-lint/internal/errorutil"
    25  	"github.com/vanstinator/golangci-lint/internal/pkgcache"
    26  	"github.com/vanstinator/golangci-lint/pkg/golinters/goanalysis/load"
    27  	"github.com/vanstinator/golangci-lint/pkg/logutils"
    28  	"github.com/vanstinator/golangci-lint/pkg/timeutils"
    29  )
    30  
    31  var (
    32  	debugf = logutils.Debug(logutils.DebugKeyGoAnalysis)
    33  
    34  	analyzeDebugf     = logutils.Debug(logutils.DebugKeyGoAnalysisAnalyze)
    35  	isMemoryDebug     = logutils.HaveDebugTag(logutils.DebugKeyGoAnalysisMemory)
    36  	issuesCacheDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisIssuesCache)
    37  
    38  	factsDebugf        = logutils.Debug(logutils.DebugKeyGoAnalysisFacts)
    39  	factsCacheDebugf   = logutils.Debug(logutils.DebugKeyGoAnalysisFactsCache)
    40  	factsInheritDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisFactsInherit)
    41  	factsExportDebugf  = logutils.Debug(logutils.DebugKeyGoAnalysisFacts)
    42  	isFactsExportDebug = logutils.HaveDebugTag(logutils.DebugKeyGoAnalysisFactsExport)
    43  )
    44  
    45  type Diagnostic struct {
    46  	analysis.Diagnostic
    47  	Analyzer *analysis.Analyzer
    48  	Position token.Position
    49  	Pkg      *packages.Package
    50  }
    51  
    52  type runner struct {
    53  	log            logutils.Log
    54  	prefix         string // ensure unique analyzer names
    55  	pkgCache       *pkgcache.Cache
    56  	loadGuard      *load.Guard
    57  	loadMode       LoadMode
    58  	passToPkg      map[*analysis.Pass]*packages.Package
    59  	passToPkgGuard sync.Mutex
    60  	sw             *timeutils.Stopwatch
    61  }
    62  
    63  func newRunner(prefix string, logger logutils.Log, pkgCache *pkgcache.Cache, loadGuard *load.Guard,
    64  	loadMode LoadMode, sw *timeutils.Stopwatch) *runner {
    65  	return &runner{
    66  		prefix:    prefix,
    67  		log:       logger,
    68  		pkgCache:  pkgCache,
    69  		loadGuard: loadGuard,
    70  		loadMode:  loadMode,
    71  		passToPkg: map[*analysis.Pass]*packages.Package{},
    72  		sw:        sw,
    73  	}
    74  }
    75  
    76  // Run loads the packages specified by args using go/packages,
    77  // then applies the specified analyzers to them.
    78  // Analysis flags must already have been set.
    79  // It provides most of the logic for the main functions of both the
    80  // singlechecker and the multi-analysis commands.
    81  // It returns the appropriate exit code.
    82  func (r *runner) run(analyzers []*analysis.Analyzer, initialPackages []*packages.Package) ([]Diagnostic,
    83  	[]error, map[*analysis.Pass]*packages.Package) {
    84  	debugf("Analyzing %d packages on load mode %s", len(initialPackages), r.loadMode)
    85  	defer r.pkgCache.Trim()
    86  
    87  	roots := r.analyze(initialPackages, analyzers)
    88  
    89  	diags, errs := extractDiagnostics(roots)
    90  
    91  	return diags, errs, r.passToPkg
    92  }
    93  
    94  type actKey struct {
    95  	*analysis.Analyzer
    96  	*packages.Package
    97  }
    98  
    99  func (r *runner) markAllActions(a *analysis.Analyzer, pkg *packages.Package, markedActions map[actKey]struct{}) {
   100  	k := actKey{a, pkg}
   101  	if _, ok := markedActions[k]; ok {
   102  		return
   103  	}
   104  
   105  	for _, req := range a.Requires {
   106  		r.markAllActions(req, pkg, markedActions)
   107  	}
   108  
   109  	if len(a.FactTypes) != 0 {
   110  		for path := range pkg.Imports {
   111  			r.markAllActions(a, pkg.Imports[path], markedActions)
   112  		}
   113  	}
   114  
   115  	markedActions[k] = struct{}{}
   116  }
   117  
   118  func (r *runner) makeAction(a *analysis.Analyzer, pkg *packages.Package,
   119  	initialPkgs map[*packages.Package]bool, actions map[actKey]*action, actAlloc *actionAllocator) *action {
   120  	k := actKey{a, pkg}
   121  	act, ok := actions[k]
   122  	if ok {
   123  		return act
   124  	}
   125  
   126  	act = actAlloc.alloc()
   127  	act.a = a
   128  	act.pkg = pkg
   129  	act.r = r
   130  	act.isInitialPkg = initialPkgs[pkg]
   131  	act.needAnalyzeSource = initialPkgs[pkg]
   132  	act.analysisDoneCh = make(chan struct{})
   133  
   134  	depsCount := len(a.Requires)
   135  	if len(a.FactTypes) > 0 {
   136  		depsCount += len(pkg.Imports)
   137  	}
   138  	act.deps = make([]*action, 0, depsCount)
   139  
   140  	// Add a dependency on each required analyzers.
   141  	for _, req := range a.Requires {
   142  		act.deps = append(act.deps, r.makeAction(req, pkg, initialPkgs, actions, actAlloc))
   143  	}
   144  
   145  	r.buildActionFactDeps(act, a, pkg, initialPkgs, actions, actAlloc)
   146  
   147  	actions[k] = act
   148  
   149  	return act
   150  }
   151  
   152  func (r *runner) buildActionFactDeps(act *action, a *analysis.Analyzer, pkg *packages.Package,
   153  	initialPkgs map[*packages.Package]bool, actions map[actKey]*action, actAlloc *actionAllocator) {
   154  	// An analysis that consumes/produces facts
   155  	// must run on the package's dependencies too.
   156  	if len(a.FactTypes) == 0 {
   157  		return
   158  	}
   159  
   160  	act.objectFacts = make(map[objectFactKey]analysis.Fact)
   161  	act.packageFacts = make(map[packageFactKey]analysis.Fact)
   162  
   163  	paths := maps.Keys(pkg.Imports)
   164  	sort.Strings(paths) // for determinism
   165  	for _, path := range paths {
   166  		dep := r.makeAction(a, pkg.Imports[path], initialPkgs, actions, actAlloc)
   167  		act.deps = append(act.deps, dep)
   168  	}
   169  
   170  	// Need to register fact types for pkgcache proper gob encoding.
   171  	for _, f := range a.FactTypes {
   172  		gob.Register(f)
   173  	}
   174  }
   175  
   176  //nolint:gocritic
   177  func (r *runner) prepareAnalysis(pkgs []*packages.Package,
   178  	analyzers []*analysis.Analyzer) (map[*packages.Package]bool, []*action, []*action) {
   179  	// Construct the action graph.
   180  
   181  	// Each graph node (action) is one unit of analysis.
   182  	// Edges express package-to-package (vertical) dependencies,
   183  	// and analysis-to-analysis (horizontal) dependencies.
   184  
   185  	// This place is memory-intensive: e.g. Istio project has 120k total actions.
   186  	// Therefore, optimize it carefully.
   187  	markedActions := make(map[actKey]struct{}, len(analyzers)*len(pkgs))
   188  	for _, a := range analyzers {
   189  		for _, pkg := range pkgs {
   190  			r.markAllActions(a, pkg, markedActions)
   191  		}
   192  	}
   193  	totalActionsCount := len(markedActions)
   194  
   195  	actions := make(map[actKey]*action, totalActionsCount)
   196  	actAlloc := newActionAllocator(totalActionsCount)
   197  
   198  	initialPkgs := make(map[*packages.Package]bool, len(pkgs))
   199  	for _, pkg := range pkgs {
   200  		initialPkgs[pkg] = true
   201  	}
   202  
   203  	// Build nodes for initial packages.
   204  	roots := make([]*action, 0, len(pkgs)*len(analyzers))
   205  	for _, a := range analyzers {
   206  		for _, pkg := range pkgs {
   207  			root := r.makeAction(a, pkg, initialPkgs, actions, actAlloc)
   208  			root.isroot = true
   209  			roots = append(roots, root)
   210  		}
   211  	}
   212  
   213  	allActions := maps.Values(actions)
   214  
   215  	debugf("Built %d actions", len(actions))
   216  
   217  	return initialPkgs, allActions, roots
   218  }
   219  
   220  func (r *runner) analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action {
   221  	initialPkgs, actions, rootActions := r.prepareAnalysis(pkgs, analyzers)
   222  
   223  	actionPerPkg := map[*packages.Package][]*action{}
   224  	for _, act := range actions {
   225  		actionPerPkg[act.pkg] = append(actionPerPkg[act.pkg], act)
   226  	}
   227  
   228  	// Fill Imports field.
   229  	loadingPackages := map[*packages.Package]*loadingPackage{}
   230  	var dfs func(pkg *packages.Package)
   231  	dfs = func(pkg *packages.Package) {
   232  		if loadingPackages[pkg] != nil {
   233  			return
   234  		}
   235  
   236  		imports := map[string]*loadingPackage{}
   237  		for impPath, imp := range pkg.Imports {
   238  			dfs(imp)
   239  			impLp := loadingPackages[imp]
   240  			impLp.dependents++
   241  			imports[impPath] = impLp
   242  		}
   243  
   244  		loadingPackages[pkg] = &loadingPackage{
   245  			pkg:        pkg,
   246  			imports:    imports,
   247  			isInitial:  initialPkgs[pkg],
   248  			log:        r.log,
   249  			actions:    actionPerPkg[pkg],
   250  			loadGuard:  r.loadGuard,
   251  			dependents: 1, // self dependent
   252  		}
   253  	}
   254  	for _, act := range actions {
   255  		dfs(act.pkg)
   256  	}
   257  
   258  	// Limit memory and IO usage.
   259  	gomaxprocs := runtime.GOMAXPROCS(-1)
   260  	debugf("Analyzing at most %d packages in parallel", gomaxprocs)
   261  	loadSem := make(chan struct{}, gomaxprocs)
   262  
   263  	var wg sync.WaitGroup
   264  	debugf("There are %d initial and %d total packages", len(initialPkgs), len(loadingPackages))
   265  	for _, lp := range loadingPackages {
   266  		if lp.isInitial {
   267  			wg.Add(1)
   268  			go func(lp *loadingPackage) {
   269  				lp.analyzeRecursive(r.loadMode, loadSem)
   270  				wg.Done()
   271  			}(lp)
   272  		}
   273  	}
   274  	wg.Wait()
   275  
   276  	return rootActions
   277  }
   278  
   279  //nolint:nakedret
   280  func extractDiagnostics(roots []*action) (retDiags []Diagnostic, retErrors []error) {
   281  	extracted := make(map[*action]bool)
   282  	var extract func(*action)
   283  	var visitAll func(actions []*action)
   284  	visitAll = func(actions []*action) {
   285  		for _, act := range actions {
   286  			if !extracted[act] {
   287  				extracted[act] = true
   288  				visitAll(act.deps)
   289  				extract(act)
   290  			}
   291  		}
   292  	}
   293  
   294  	// De-duplicate diagnostics by position (not token.Pos) to
   295  	// avoid double-reporting in source files that belong to
   296  	// multiple packages, such as foo and foo.test.
   297  	type key struct {
   298  		token.Position
   299  		*analysis.Analyzer
   300  		message string
   301  	}
   302  	seen := make(map[key]bool)
   303  
   304  	extract = func(act *action) {
   305  		if act.err != nil {
   306  			if pe, ok := act.err.(*errorutil.PanicError); ok {
   307  				panic(pe)
   308  			}
   309  			retErrors = append(retErrors, fmt.Errorf("%s: %w", act.a.Name, act.err))
   310  			return
   311  		}
   312  
   313  		if act.isroot {
   314  			for _, diag := range act.diagnostics {
   315  				// We don't display a.Name/f.Category
   316  				// as most users don't care.
   317  
   318  				posn := act.pkg.Fset.Position(diag.Pos)
   319  				k := key{posn, act.a, diag.Message}
   320  				if seen[k] {
   321  					continue // duplicate
   322  				}
   323  				seen[k] = true
   324  
   325  				retDiag := Diagnostic{
   326  					Diagnostic: diag,
   327  					Analyzer:   act.a,
   328  					Position:   posn,
   329  					Pkg:        act.pkg,
   330  				}
   331  				retDiags = append(retDiags, retDiag)
   332  			}
   333  		}
   334  	}
   335  	visitAll(roots)
   336  	return
   337  }