338 lines
9.3 KiB
Go
338 lines
9.3 KiB
Go
// checker is a partial copy of https://github.com/golang/tools/blob/master/go/analysis/internal/checker
|
|
// Copyright 2018 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// Package goanalysis defines the implementation of the checker commands.
|
|
// The same code drives the multi-analysis driver, the single-analysis
|
|
// driver that is conventionally provided for convenience along with
|
|
// each analysis package, and the test driver.
|
|
package goanalysis
|
|
|
|
import (
|
|
"encoding/gob"
|
|
"fmt"
|
|
"go/token"
|
|
"runtime"
|
|
"sort"
|
|
"sync"
|
|
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/packages"
|
|
|
|
"github.com/golangci/golangci-lint/internal/errorutil"
|
|
"github.com/golangci/golangci-lint/internal/pkgcache"
|
|
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
"github.com/golangci/golangci-lint/pkg/timeutils"
|
|
)
|
|
|
|
var (
|
|
debugf = logutils.Debug(logutils.DebugKeyGoAnalysis)
|
|
|
|
analyzeDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisAnalyze)
|
|
isMemoryDebug = logutils.HaveDebugTag(logutils.DebugKeyGoAnalysisMemory)
|
|
issuesCacheDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisIssuesCache)
|
|
|
|
factsDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisFacts)
|
|
factsCacheDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisFactsCache)
|
|
factsInheritDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisFactsInherit)
|
|
factsExportDebugf = logutils.Debug(logutils.DebugKeyGoAnalysisFacts)
|
|
isFactsExportDebug = logutils.HaveDebugTag(logutils.DebugKeyGoAnalysisFactsExport)
|
|
)
|
|
|
|
type Diagnostic struct {
|
|
analysis.Diagnostic
|
|
Analyzer *analysis.Analyzer
|
|
Position token.Position
|
|
Pkg *packages.Package
|
|
}
|
|
|
|
type runner struct {
|
|
log logutils.Log
|
|
prefix string // ensure unique analyzer names
|
|
pkgCache *pkgcache.Cache
|
|
loadGuard *load.Guard
|
|
loadMode LoadMode
|
|
passToPkg map[*analysis.Pass]*packages.Package
|
|
passToPkgGuard sync.Mutex
|
|
sw *timeutils.Stopwatch
|
|
}
|
|
|
|
func newRunner(prefix string, logger logutils.Log, pkgCache *pkgcache.Cache, loadGuard *load.Guard,
|
|
loadMode LoadMode, sw *timeutils.Stopwatch) *runner {
|
|
return &runner{
|
|
prefix: prefix,
|
|
log: logger,
|
|
pkgCache: pkgCache,
|
|
loadGuard: loadGuard,
|
|
loadMode: loadMode,
|
|
passToPkg: map[*analysis.Pass]*packages.Package{},
|
|
sw: sw,
|
|
}
|
|
}
|
|
|
|
// Run loads the packages specified by args using go/packages,
|
|
// then applies the specified analyzers to them.
|
|
// Analysis flags must already have been set.
|
|
// It provides most of the logic for the main functions of both the
|
|
// singlechecker and the multi-analysis commands.
|
|
// It returns the appropriate exit code.
|
|
func (r *runner) run(analyzers []*analysis.Analyzer, initialPackages []*packages.Package) ([]Diagnostic,
|
|
[]error, map[*analysis.Pass]*packages.Package) {
|
|
debugf("Analyzing %d packages on load mode %s", len(initialPackages), r.loadMode)
|
|
defer r.pkgCache.Trim()
|
|
|
|
roots := r.analyze(initialPackages, analyzers)
|
|
|
|
diags, errs := extractDiagnostics(roots)
|
|
|
|
return diags, errs, r.passToPkg
|
|
}
|
|
|
|
type actKey struct {
|
|
*analysis.Analyzer
|
|
*packages.Package
|
|
}
|
|
|
|
func (r *runner) markAllActions(a *analysis.Analyzer, pkg *packages.Package, markedActions map[actKey]struct{}) {
|
|
k := actKey{a, pkg}
|
|
if _, ok := markedActions[k]; ok {
|
|
return
|
|
}
|
|
|
|
for _, req := range a.Requires {
|
|
r.markAllActions(req, pkg, markedActions)
|
|
}
|
|
|
|
if len(a.FactTypes) != 0 {
|
|
for path := range pkg.Imports {
|
|
r.markAllActions(a, pkg.Imports[path], markedActions)
|
|
}
|
|
}
|
|
|
|
markedActions[k] = struct{}{}
|
|
}
|
|
|
|
func (r *runner) makeAction(a *analysis.Analyzer, pkg *packages.Package,
|
|
initialPkgs map[*packages.Package]bool, actions map[actKey]*action, actAlloc *actionAllocator) *action {
|
|
k := actKey{a, pkg}
|
|
act, ok := actions[k]
|
|
if ok {
|
|
return act
|
|
}
|
|
|
|
act = actAlloc.alloc()
|
|
act.a = a
|
|
act.pkg = pkg
|
|
act.r = r
|
|
act.isInitialPkg = initialPkgs[pkg]
|
|
act.needAnalyzeSource = initialPkgs[pkg]
|
|
act.analysisDoneCh = make(chan struct{})
|
|
|
|
depsCount := len(a.Requires)
|
|
if len(a.FactTypes) > 0 {
|
|
depsCount += len(pkg.Imports)
|
|
}
|
|
act.deps = make([]*action, 0, depsCount)
|
|
|
|
// Add a dependency on each required analyzers.
|
|
for _, req := range a.Requires {
|
|
act.deps = append(act.deps, r.makeAction(req, pkg, initialPkgs, actions, actAlloc))
|
|
}
|
|
|
|
r.buildActionFactDeps(act, a, pkg, initialPkgs, actions, actAlloc)
|
|
|
|
actions[k] = act
|
|
|
|
return act
|
|
}
|
|
|
|
func (r *runner) buildActionFactDeps(act *action, a *analysis.Analyzer, pkg *packages.Package,
|
|
initialPkgs map[*packages.Package]bool, actions map[actKey]*action, actAlloc *actionAllocator) {
|
|
// An analysis that consumes/produces facts
|
|
// must run on the package's dependencies too.
|
|
if len(a.FactTypes) == 0 {
|
|
return
|
|
}
|
|
|
|
act.objectFacts = make(map[objectFactKey]analysis.Fact)
|
|
act.packageFacts = make(map[packageFactKey]analysis.Fact)
|
|
|
|
paths := maps.Keys(pkg.Imports)
|
|
sort.Strings(paths) // for determinism
|
|
for _, path := range paths {
|
|
dep := r.makeAction(a, pkg.Imports[path], initialPkgs, actions, actAlloc)
|
|
act.deps = append(act.deps, dep)
|
|
}
|
|
|
|
// Need to register fact types for pkgcache proper gob encoding.
|
|
for _, f := range a.FactTypes {
|
|
gob.Register(f)
|
|
}
|
|
}
|
|
|
|
//nolint:gocritic
|
|
func (r *runner) prepareAnalysis(pkgs []*packages.Package,
|
|
analyzers []*analysis.Analyzer) (map[*packages.Package]bool, []*action, []*action) {
|
|
// Construct the action graph.
|
|
|
|
// Each graph node (action) is one unit of analysis.
|
|
// Edges express package-to-package (vertical) dependencies,
|
|
// and analysis-to-analysis (horizontal) dependencies.
|
|
|
|
// This place is memory-intensive: e.g. Istio project has 120k total actions.
|
|
// Therefore, optimize it carefully.
|
|
markedActions := make(map[actKey]struct{}, len(analyzers)*len(pkgs))
|
|
for _, a := range analyzers {
|
|
for _, pkg := range pkgs {
|
|
r.markAllActions(a, pkg, markedActions)
|
|
}
|
|
}
|
|
totalActionsCount := len(markedActions)
|
|
|
|
actions := make(map[actKey]*action, totalActionsCount)
|
|
actAlloc := newActionAllocator(totalActionsCount)
|
|
|
|
initialPkgs := make(map[*packages.Package]bool, len(pkgs))
|
|
for _, pkg := range pkgs {
|
|
initialPkgs[pkg] = true
|
|
}
|
|
|
|
// Build nodes for initial packages.
|
|
roots := make([]*action, 0, len(pkgs)*len(analyzers))
|
|
for _, a := range analyzers {
|
|
for _, pkg := range pkgs {
|
|
root := r.makeAction(a, pkg, initialPkgs, actions, actAlloc)
|
|
root.isroot = true
|
|
roots = append(roots, root)
|
|
}
|
|
}
|
|
|
|
allActions := maps.Values(actions)
|
|
|
|
debugf("Built %d actions", len(actions))
|
|
|
|
return initialPkgs, allActions, roots
|
|
}
|
|
|
|
func (r *runner) analyze(pkgs []*packages.Package, analyzers []*analysis.Analyzer) []*action {
|
|
initialPkgs, actions, rootActions := r.prepareAnalysis(pkgs, analyzers)
|
|
|
|
actionPerPkg := map[*packages.Package][]*action{}
|
|
for _, act := range actions {
|
|
actionPerPkg[act.pkg] = append(actionPerPkg[act.pkg], act)
|
|
}
|
|
|
|
// Fill Imports field.
|
|
loadingPackages := map[*packages.Package]*loadingPackage{}
|
|
var dfs func(pkg *packages.Package)
|
|
dfs = func(pkg *packages.Package) {
|
|
if loadingPackages[pkg] != nil {
|
|
return
|
|
}
|
|
|
|
imports := map[string]*loadingPackage{}
|
|
for impPath, imp := range pkg.Imports {
|
|
dfs(imp)
|
|
impLp := loadingPackages[imp]
|
|
impLp.dependents++
|
|
imports[impPath] = impLp
|
|
}
|
|
|
|
loadingPackages[pkg] = &loadingPackage{
|
|
pkg: pkg,
|
|
imports: imports,
|
|
isInitial: initialPkgs[pkg],
|
|
log: r.log,
|
|
actions: actionPerPkg[pkg],
|
|
loadGuard: r.loadGuard,
|
|
dependents: 1, // self dependent
|
|
}
|
|
}
|
|
for _, act := range actions {
|
|
dfs(act.pkg)
|
|
}
|
|
|
|
// Limit memory and IO usage.
|
|
gomaxprocs := runtime.GOMAXPROCS(-1)
|
|
debugf("Analyzing at most %d packages in parallel", gomaxprocs)
|
|
loadSem := make(chan struct{}, gomaxprocs)
|
|
|
|
var wg sync.WaitGroup
|
|
debugf("There are %d initial and %d total packages", len(initialPkgs), len(loadingPackages))
|
|
for _, lp := range loadingPackages {
|
|
if lp.isInitial {
|
|
wg.Add(1)
|
|
go func(lp *loadingPackage) {
|
|
lp.analyzeRecursive(r.loadMode, loadSem)
|
|
wg.Done()
|
|
}(lp)
|
|
}
|
|
}
|
|
wg.Wait()
|
|
|
|
return rootActions
|
|
}
|
|
|
|
//nolint:nakedret
|
|
func extractDiagnostics(roots []*action) (retDiags []Diagnostic, retErrors []error) {
|
|
extracted := make(map[*action]bool)
|
|
var extract func(*action)
|
|
var visitAll func(actions []*action)
|
|
visitAll = func(actions []*action) {
|
|
for _, act := range actions {
|
|
if !extracted[act] {
|
|
extracted[act] = true
|
|
visitAll(act.deps)
|
|
extract(act)
|
|
}
|
|
}
|
|
}
|
|
|
|
// De-duplicate diagnostics by position (not token.Pos) to
|
|
// avoid double-reporting in source files that belong to
|
|
// multiple packages, such as foo and foo.test.
|
|
type key struct {
|
|
token.Position
|
|
*analysis.Analyzer
|
|
message string
|
|
}
|
|
seen := make(map[key]bool)
|
|
|
|
extract = func(act *action) {
|
|
if act.err != nil {
|
|
if pe, ok := act.err.(*errorutil.PanicError); ok {
|
|
panic(pe)
|
|
}
|
|
retErrors = append(retErrors, fmt.Errorf("%s: %w", act.a.Name, act.err))
|
|
return
|
|
}
|
|
|
|
if act.isroot {
|
|
for _, diag := range act.diagnostics {
|
|
// We don't display a.Name/f.Category
|
|
// as most users don't care.
|
|
|
|
posn := act.pkg.Fset.Position(diag.Pos)
|
|
k := key{posn, act.a, diag.Message}
|
|
if seen[k] {
|
|
continue // duplicate
|
|
}
|
|
seen[k] = true
|
|
|
|
retDiag := Diagnostic{
|
|
Diagnostic: diag,
|
|
Analyzer: act.a,
|
|
Position: posn,
|
|
Pkg: act.pkg,
|
|
}
|
|
retDiags = append(retDiags, retDiag)
|
|
}
|
|
}
|
|
}
|
|
visitAll(roots)
|
|
return
|
|
}
|