385 lines
11 KiB
Go
385 lines
11 KiB
Go
package goanalysis
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"go/types"
|
|
"io"
|
|
"reflect"
|
|
"runtime/debug"
|
|
"time"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/packages"
|
|
"golang.org/x/tools/go/types/objectpath"
|
|
|
|
"github.com/golangci/golangci-lint/internal/errorutil"
|
|
"github.com/golangci/golangci-lint/internal/pkgcache"
|
|
)
|
|
|
|
type actionAllocator struct {
|
|
allocatedActions []action
|
|
nextFreeIndex int
|
|
}
|
|
|
|
func newActionAllocator(maxCount int) *actionAllocator {
|
|
return &actionAllocator{
|
|
allocatedActions: make([]action, maxCount),
|
|
nextFreeIndex: 0,
|
|
}
|
|
}
|
|
|
|
func (actAlloc *actionAllocator) alloc() *action {
|
|
if actAlloc.nextFreeIndex == len(actAlloc.allocatedActions) {
|
|
panic(fmt.Sprintf("Made too many allocations of actions: %d allowed", len(actAlloc.allocatedActions)))
|
|
}
|
|
act := &actAlloc.allocatedActions[actAlloc.nextFreeIndex]
|
|
actAlloc.nextFreeIndex++
|
|
return act
|
|
}
|
|
|
|
// An action represents one unit of analysis work: the application of
|
|
// one analysis to one package. Actions form a DAG, both within a
|
|
// package (as different analyzers are applied, either in sequence or
|
|
// parallel), and across packages (as dependencies are analyzed).
|
|
type action struct {
|
|
a *analysis.Analyzer
|
|
pkg *packages.Package
|
|
pass *analysis.Pass
|
|
deps []*action
|
|
objectFacts map[objectFactKey]analysis.Fact
|
|
packageFacts map[packageFactKey]analysis.Fact
|
|
result any
|
|
diagnostics []analysis.Diagnostic
|
|
err error
|
|
r *runner
|
|
analysisDoneCh chan struct{}
|
|
loadCachedFactsDone bool
|
|
loadCachedFactsOk bool
|
|
isroot bool
|
|
isInitialPkg bool
|
|
needAnalyzeSource bool
|
|
}
|
|
|
|
func (act *action) String() string {
|
|
return fmt.Sprintf("%s@%s", act.a, act.pkg)
|
|
}
|
|
|
|
func (act *action) loadCachedFacts() bool {
|
|
if act.loadCachedFactsDone { // can't be set in parallel
|
|
return act.loadCachedFactsOk
|
|
}
|
|
|
|
res := func() bool {
|
|
if act.isInitialPkg {
|
|
return true // load cached facts only for non-initial packages
|
|
}
|
|
|
|
if len(act.a.FactTypes) == 0 {
|
|
return true // no need to load facts
|
|
}
|
|
|
|
return act.loadPersistedFacts()
|
|
}()
|
|
act.loadCachedFactsDone = true
|
|
act.loadCachedFactsOk = res
|
|
return res
|
|
}
|
|
|
|
func (act *action) waitUntilDependingAnalyzersWorked() {
|
|
for _, dep := range act.deps {
|
|
if dep.pkg == act.pkg {
|
|
<-dep.analysisDoneCh
|
|
}
|
|
}
|
|
}
|
|
|
|
func (act *action) analyzeSafe() {
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
if !act.isroot {
|
|
// This line allows to display "hidden" panic with analyzers like buildssa.
|
|
// Some linters are dependent of sub-analyzers but when a sub-analyzer fails the linter is not aware of that,
|
|
// this results to another panic (ex: "interface conversion: interface {} is nil, not *buildssa.SSA").
|
|
act.r.log.Errorf("%s: panic during analysis: %v, %s", act.a.Name, p, string(debug.Stack()))
|
|
}
|
|
|
|
act.err = errorutil.NewPanicError(fmt.Sprintf("%s: package %q (isInitialPkg: %t, needAnalyzeSource: %t): %s",
|
|
act.a.Name, act.pkg.Name, act.isInitialPkg, act.needAnalyzeSource, p), debug.Stack())
|
|
}
|
|
}()
|
|
act.r.sw.TrackStage(act.a.Name, func() {
|
|
act.analyze()
|
|
})
|
|
}
|
|
|
|
func (act *action) analyze() {
|
|
defer close(act.analysisDoneCh) // unblock actions depending on this action
|
|
|
|
if !act.needAnalyzeSource {
|
|
return
|
|
}
|
|
|
|
defer func(now time.Time) {
|
|
analyzeDebugf("go/analysis: %s: %s: analyzed package %q in %s", act.r.prefix, act.a.Name, act.pkg.Name, time.Since(now))
|
|
}(time.Now())
|
|
|
|
// Report an error if any dependency failures.
|
|
var depErrors error
|
|
for _, dep := range act.deps {
|
|
if dep.err == nil {
|
|
continue
|
|
}
|
|
|
|
depErrors = errors.Join(depErrors, errors.Unwrap(dep.err))
|
|
}
|
|
if depErrors != nil {
|
|
act.err = fmt.Errorf("failed prerequisites: %w", depErrors)
|
|
return
|
|
}
|
|
|
|
// Plumb the output values of the dependencies
|
|
// into the inputs of this action. Also facts.
|
|
inputs := make(map[*analysis.Analyzer]any)
|
|
startedAt := time.Now()
|
|
for _, dep := range act.deps {
|
|
if dep.pkg == act.pkg {
|
|
// Same package, different analysis (horizontal edge):
|
|
// in-memory outputs of prerequisite analyzers
|
|
// become inputs to this analysis pass.
|
|
inputs[dep.a] = dep.result
|
|
} else if dep.a == act.a { // (always true)
|
|
// Same analysis, different package (vertical edge):
|
|
// serialized facts produced by prerequisite analysis
|
|
// become available to this analysis pass.
|
|
inheritFacts(act, dep)
|
|
}
|
|
}
|
|
factsDebugf("%s: Inherited facts in %s", act, time.Since(startedAt))
|
|
|
|
// Run the analysis.
|
|
pass := &analysis.Pass{
|
|
Analyzer: act.a,
|
|
Fset: act.pkg.Fset,
|
|
Files: act.pkg.Syntax,
|
|
OtherFiles: act.pkg.OtherFiles,
|
|
Pkg: act.pkg.Types,
|
|
TypesInfo: act.pkg.TypesInfo,
|
|
TypesSizes: act.pkg.TypesSizes,
|
|
ResultOf: inputs,
|
|
Report: func(d analysis.Diagnostic) { act.diagnostics = append(act.diagnostics, d) },
|
|
ImportObjectFact: act.importObjectFact,
|
|
ExportObjectFact: act.exportObjectFact,
|
|
ImportPackageFact: act.importPackageFact,
|
|
ExportPackageFact: act.exportPackageFact,
|
|
AllObjectFacts: act.allObjectFacts,
|
|
AllPackageFacts: act.allPackageFacts,
|
|
}
|
|
act.pass = pass
|
|
act.r.passToPkgGuard.Lock()
|
|
act.r.passToPkg[pass] = act.pkg
|
|
act.r.passToPkgGuard.Unlock()
|
|
|
|
if act.pkg.IllTyped {
|
|
// It looks like there should be !pass.Analyzer.RunDespiteErrors
|
|
// but govet's cgocall crashes on it. Govet itself contains !pass.Analyzer.RunDespiteErrors condition here,
|
|
// but it exits before it if packages.Load have failed.
|
|
act.err = fmt.Errorf("analysis skipped: %w", &IllTypedError{Pkg: act.pkg})
|
|
} else {
|
|
startedAt = time.Now()
|
|
act.result, act.err = pass.Analyzer.Run(pass)
|
|
analyzedIn := time.Since(startedAt)
|
|
if analyzedIn > time.Millisecond*10 {
|
|
debugf("%s: run analyzer in %s", act, analyzedIn)
|
|
}
|
|
}
|
|
|
|
// disallow calls after Run
|
|
pass.ExportObjectFact = nil
|
|
pass.ExportPackageFact = nil
|
|
|
|
if err := act.persistFactsToCache(); err != nil {
|
|
act.r.log.Warnf("Failed to persist facts to cache: %s", err)
|
|
}
|
|
}
|
|
|
|
// importObjectFact implements Pass.ImportObjectFact.
|
|
// Given a non-nil pointer ptr of type *T, where *T satisfies Fact,
|
|
// importObjectFact copies the fact value to *ptr.
|
|
func (act *action) importObjectFact(obj types.Object, ptr analysis.Fact) bool {
|
|
if obj == nil {
|
|
panic("nil object")
|
|
}
|
|
key := objectFactKey{obj, act.factType(ptr)}
|
|
if v, ok := act.objectFacts[key]; ok {
|
|
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// exportObjectFact implements Pass.ExportObjectFact.
|
|
func (act *action) exportObjectFact(obj types.Object, fact analysis.Fact) {
|
|
if obj.Pkg() != act.pkg.Types {
|
|
act.r.log.Panicf("internal error: in analysis %s of package %s: Fact.Set(%s, %T): can't set facts on objects belonging another package",
|
|
act.a, act.pkg, obj, fact)
|
|
}
|
|
|
|
key := objectFactKey{obj, act.factType(fact)}
|
|
act.objectFacts[key] = fact // clobber any existing entry
|
|
if isFactsExportDebug {
|
|
objstr := types.ObjectString(obj, (*types.Package).Name)
|
|
factsExportDebugf("%s: object %s has fact %s\n",
|
|
act.pkg.Fset.Position(obj.Pos()), objstr, fact)
|
|
}
|
|
}
|
|
|
|
func (act *action) allObjectFacts() []analysis.ObjectFact {
|
|
out := make([]analysis.ObjectFact, 0, len(act.objectFacts))
|
|
for key, fact := range act.objectFacts {
|
|
out = append(out, analysis.ObjectFact{
|
|
Object: key.obj,
|
|
Fact: fact,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
// importPackageFact implements Pass.ImportPackageFact.
|
|
// Given a non-nil pointer ptr of type *T, where *T satisfies Fact,
|
|
// fact copies the fact value to *ptr.
|
|
func (act *action) importPackageFact(pkg *types.Package, ptr analysis.Fact) bool {
|
|
if pkg == nil {
|
|
panic("nil package")
|
|
}
|
|
key := packageFactKey{pkg, act.factType(ptr)}
|
|
if v, ok := act.packageFacts[key]; ok {
|
|
reflect.ValueOf(ptr).Elem().Set(reflect.ValueOf(v).Elem())
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// exportPackageFact implements Pass.ExportPackageFact.
|
|
func (act *action) exportPackageFact(fact analysis.Fact) {
|
|
key := packageFactKey{act.pass.Pkg, act.factType(fact)}
|
|
act.packageFacts[key] = fact // clobber any existing entry
|
|
factsDebugf("%s: package %s has fact %s\n",
|
|
act.pkg.Fset.Position(act.pass.Files[0].Pos()), act.pass.Pkg.Path(), fact)
|
|
}
|
|
|
|
func (act *action) allPackageFacts() []analysis.PackageFact {
|
|
out := make([]analysis.PackageFact, 0, len(act.packageFacts))
|
|
for key, fact := range act.packageFacts {
|
|
out = append(out, analysis.PackageFact{
|
|
Package: key.pkg,
|
|
Fact: fact,
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (act *action) factType(fact analysis.Fact) reflect.Type {
|
|
t := reflect.TypeOf(fact)
|
|
if t.Kind() != reflect.Ptr {
|
|
act.r.log.Fatalf("invalid Fact type: got %T, want pointer", t)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func (act *action) persistFactsToCache() error {
|
|
analyzer := act.a
|
|
if len(analyzer.FactTypes) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Merge new facts into the package and persist them.
|
|
var facts []Fact
|
|
for key, fact := range act.packageFacts {
|
|
if key.pkg != act.pkg.Types {
|
|
// The fact is from inherited facts from another package
|
|
continue
|
|
}
|
|
facts = append(facts, Fact{
|
|
Path: "",
|
|
Fact: fact,
|
|
})
|
|
}
|
|
for key, fact := range act.objectFacts {
|
|
obj := key.obj
|
|
if obj.Pkg() != act.pkg.Types {
|
|
// The fact is from inherited facts from another package
|
|
continue
|
|
}
|
|
|
|
path, err := objectpath.For(obj)
|
|
if err != nil {
|
|
// The object is not globally addressable
|
|
continue
|
|
}
|
|
|
|
facts = append(facts, Fact{
|
|
Path: string(path),
|
|
Fact: fact,
|
|
})
|
|
}
|
|
|
|
factsCacheDebugf("Caching %d facts for package %q and analyzer %s", len(facts), act.pkg.Name, act.a.Name)
|
|
|
|
key := fmt.Sprintf("%s/facts", analyzer.Name)
|
|
return act.r.pkgCache.Put(act.pkg, pkgcache.HashModeNeedAllDeps, key, facts)
|
|
}
|
|
|
|
func (act *action) loadPersistedFacts() bool {
|
|
var facts []Fact
|
|
key := fmt.Sprintf("%s/facts", act.a.Name)
|
|
if err := act.r.pkgCache.Get(act.pkg, pkgcache.HashModeNeedAllDeps, key, &facts); err != nil {
|
|
if !errors.Is(err, pkgcache.ErrMissing) && !errors.Is(err, io.EOF) {
|
|
act.r.log.Warnf("Failed to get persisted facts: %s", err)
|
|
}
|
|
|
|
factsCacheDebugf("No cached facts for package %q and analyzer %s", act.pkg.Name, act.a.Name)
|
|
return false
|
|
}
|
|
|
|
factsCacheDebugf("Loaded %d cached facts for package %q and analyzer %s", len(facts), act.pkg.Name, act.a.Name)
|
|
|
|
for _, f := range facts {
|
|
if f.Path == "" { // this is a package fact
|
|
key := packageFactKey{act.pkg.Types, act.factType(f.Fact)}
|
|
act.packageFacts[key] = f.Fact
|
|
continue
|
|
}
|
|
obj, err := objectpath.Object(act.pkg.Types, objectpath.Path(f.Path))
|
|
if err != nil {
|
|
// Be lenient about these errors. For example, when
|
|
// analyzing io/ioutil from source, we may get a fact
|
|
// for methods on the devNull type, and objectpath
|
|
// will happily create a path for them. However, when
|
|
// we later load io/ioutil from export data, the path
|
|
// no longer resolves.
|
|
//
|
|
// If an exported type embeds the unexported type,
|
|
// then (part of) the unexported type will become part
|
|
// of the type information and our path will resolve
|
|
// again.
|
|
continue
|
|
}
|
|
factKey := objectFactKey{obj, act.factType(f.Fact)}
|
|
act.objectFacts[factKey] = f.Fact
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (act *action) markDepsForAnalyzingSource() {
|
|
// Horizontal deps (analyzer.Requires) must be loaded from source and analyzed before analyzing
|
|
// this action.
|
|
for _, dep := range act.deps {
|
|
if dep.pkg == act.pkg {
|
|
// Analyze source only for horizontal dependencies, e.g. from "buildssa".
|
|
dep.needAnalyzeSource = true // can't be set in parallel
|
|
}
|
|
}
|
|
}
|