kilo/vendor/honnef.co/go/tools/lintcmd/runner/runner.go

1331 lines
36 KiB
Go
Raw Normal View History

// Package runner implements a go/analysis runner. It makes heavy use
// of on-disk caching to reduce overall memory usage and to speed up
// repeat runs.
//
// Public API
//
// A Runner maps a list of analyzers and package patterns to a list of
// results. Results provide access to diagnostics, directives, errors
// encountered, and information about packages. Results explicitly do
// not contain ASTs or type information. All position information is
// returned in the form of token.Position, not token.Pos. All work
// that requires access to the loaded representation of a package has
// to occur inside analyzers.
//
// Planning and execution
//
// Analyzing packages is split into two phases: planning and
// execution.
//
// During planning, a directed acyclic graph of package dependencies
// is computed. We materialize the full graph so that we can execute
// the graph from the bottom up, without keeping unnecessary data in
// memory during a DFS and with simplified parallel execution.
//
// During execution, leaf nodes (nodes with no outstanding
// dependencies) get executed in parallel, bounded by a semaphore
// sized according to the number of CPUs. Conceptually, this happens
// in a loop, processing new leaf nodes as they appear, until no more
// nodes are left. In the actual implementation, nodes know their
// dependents, and the last dependency of a node to be processed is
// responsible for scheduling its dependent.
//
// The graph is rooted at a synthetic root node. Upon execution of the
// root node, the algorithm terminates.
//
// Analyzing a package repeats the same planning + execution steps,
// but this time on a graph of analyzers for the package. Parallel
// execution of individual analyzers is bounded by the same semaphore
// as executing packages.
//
// Parallelism
//
// Actions are executed in parallel where the dependency graph allows.
// Overall parallelism is bounded by a semaphore, sized according to
// GOMAXPROCS. Each concurrently processed package takes up a
// token, as does each analyzer but a package can always execute at
// least one analyzer, using the package's token.
//
// Depending on the overall shape of the graph, there may be GOMAXPROCS
// packages running a single analyzer each, a single package running
// GOMAXPROCS analyzers, or anything in between.
//
// Total memory consumption grows roughly linearly with the number of
// CPUs, while total execution time is inversely proportional to the
// number of CPUs. Overall, parallelism is affected by the shape of
// the dependency graph. A lot of inter-connected packages will see
// less parallelism than a lot of independent packages.
//
// Caching
//
// The runner caches facts, directives and diagnostics in a
// content-addressable cache that is designed after Go's own cache.
// Additionally, it makes use of Go's export data.
//
// This cache not only speeds up repeat runs, it also reduces peak
// memory usage. When we've analyzed a package, we cache the results
// and drop them from memory. When a dependent needs any of this
// information, or when analysis is complete and we wish to render the
// results, the data gets loaded from disk again.
//
// Data only exists in memory when it is immediately needed, not
// retained for possible future uses. This trades increased CPU usage
// for reduced memory usage. A single dependency may be loaded many
// times over, but it greatly reduces peak memory usage, as an
// arbitrary amount of time may pass between analyzing a dependency
// and its dependent, during which other packages will be processed.
package runner
// OPT(dh): we could reduce disk storage usage of cached data by
// compressing it, either directly at the cache layer, or by feeding
// compressed data to the cache. Of course doing so may negatively
// affect CPU usage, and there are lower hanging fruit, such as
// needing to cache less data in the first place.
// OPT(dh): right now, each package is analyzed completely
// independently. Each package loads all of its dependencies from
// export data and cached facts. If we have two packages A and B,
// which both depend on C, and which both get analyzed in parallel,
// then C will be loaded twice. This wastes CPU time and memory. It
// would be nice if we could reuse a single C for the analysis of both
// A and B.
//
// We can't reuse the actual types.Package or facts, because each
// package gets its own token.FileSet. Sharing a global FileSet has
// several drawbacks, including increased memory usage and running the
// risk of running out of FileSet address space.
//
// We could however avoid loading the same raw export data from disk
// twice, as well as deserializing gob data twice. One possible
// solution would be a duplicate-suppressing in-memory cache that
// caches data for a limited amount of time. When the same package
// needs to be loaded twice in close succession, we can reuse work,
// without holding unnecessary data in memory for an extended period
// of time.
//
// We would likely need to do extensive benchmarking to figure out how
// long to keep data around to find a sweet spot where we reduce CPU
// load without increasing memory usage.
//
// We can probably populate the cache after we've analyzed a package,
// on the assumption that it will have to be loaded again in the near
// future.
import (
"bytes"
"encoding/gob"
"fmt"
"go/token"
"go/types"
"io"
"io/ioutil"
"os"
"reflect"
"runtime"
"sort"
"strings"
"sync/atomic"
"time"
"honnef.co/go/tools/analysis/lint"
"honnef.co/go/tools/analysis/report"
"honnef.co/go/tools/config"
"honnef.co/go/tools/go/loader"
tsync "honnef.co/go/tools/internal/sync"
"honnef.co/go/tools/lintcmd/cache"
"honnef.co/go/tools/unused"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/go/types/objectpath"
)
const sanityCheck = false
// Diagnostic is like go/analysis.Diagnostic, but with all token.Pos resolved to token.Position.
type Diagnostic struct {
Position token.Position
End token.Position
Category string
Message string
SuggestedFixes []SuggestedFix
Related []RelatedInformation
}
// RelatedInformation provides additional context for a diagnostic.
type RelatedInformation struct {
Position token.Position
End token.Position
Message string
}
type SuggestedFix struct {
Message string
TextEdits []TextEdit
}
type TextEdit struct {
Position token.Position
End token.Position
NewText []byte
}
// A Result describes the result of analyzing a single package.
//
// It holds references to cached diagnostics and directives. They can
// be loaded on demand with the Load method.
type Result struct {
Package *loader.PackageSpec
Config config.Config
Initial bool
Skipped bool
Failed bool
Errors []error
// Action results, path to file
results string
// Results relevant to testing, only set when test mode is enabled, path to file
testData string
}
type SerializedDirective struct {
Command string
Arguments []string
// The position of the comment
DirectivePosition token.Position
// The position of the node that the comment is attached to
NodePosition token.Position
}
func serializeDirective(dir lint.Directive, fset *token.FileSet) SerializedDirective {
return SerializedDirective{
Command: dir.Command,
Arguments: dir.Arguments,
DirectivePosition: report.DisplayPosition(fset, dir.Directive.Pos()),
NodePosition: report.DisplayPosition(fset, dir.Node.Pos()),
}
}
type ResultData struct {
Directives []SerializedDirective
Diagnostics []Diagnostic
Unused unused.SerializedResult
}
func (r Result) Load() (ResultData, error) {
if r.Failed {
panic("Load called on failed Result")
}
if r.results == "" {
// this package was only a dependency
return ResultData{}, nil
}
f, err := os.Open(r.results)
if err != nil {
return ResultData{}, fmt.Errorf("failed loading result: %w", err)
}
defer f.Close()
var out ResultData
err = gob.NewDecoder(f).Decode(&out)
return out, err
}
type Want struct {
Position token.Position
Comment string
}
// TestData contains extra information about analysis runs that is only available in test mode.
type TestData struct {
// Wants contains a list of '// want' comments extracted from Go files.
// These comments are used in unit tests.
Wants []Want
// Facts contains facts produced by analyzers for a package.
// Unlike vetx, this list only contains facts specific to this package,
// not all facts for the transitive closure of dependencies.
Facts []TestFact
}
// LoadTest returns data relevant to testing.
// It should only be called if Runner.TestMode was set to true.
func (r Result) LoadTest() (TestData, error) {
if r.Failed {
panic("Load called on failed Result")
}
if r.results == "" {
// this package was only a dependency
return TestData{}, nil
}
f, err := os.Open(r.testData)
if err != nil {
return TestData{}, fmt.Errorf("failed loading test data: %w", err)
}
defer f.Close()
var out TestData
err = gob.NewDecoder(f).Decode(&out)
return out, err
}
type action interface {
Deps() []action
Triggers() []action
DecrementPending() bool
MarkFailed()
IsFailed() bool
AddError(error)
}
type baseAction struct {
// Action description
deps []action
triggers []action
pending uint32
// Action results
// failed is set to true if the action couldn't be processed. This
// may either be due to an error specific to this action, in
// which case the errors field will be populated, or due to a
// dependency being marked as failed, in which case errors will be
// empty.
failed bool
errors []error
}
func (act *baseAction) Deps() []action { return act.deps }
func (act *baseAction) Triggers() []action { return act.triggers }
func (act *baseAction) DecrementPending() bool {
return atomic.AddUint32(&act.pending, ^uint32(0)) == 0
}
func (act *baseAction) MarkFailed() { act.failed = true }
func (act *baseAction) IsFailed() bool { return act.failed }
func (act *baseAction) AddError(err error) { act.errors = append(act.errors, err) }
// packageAction describes the act of loading a package, fully
// analyzing it, and storing the results.
type packageAction struct {
baseAction
// Action description
Package *loader.PackageSpec
factsOnly bool
hash cache.ActionID
// Action results
cfg config.Config
vetx string
results string
testData string
skipped bool
}
func (act *packageAction) String() string {
return fmt.Sprintf("packageAction(%s)", act.Package)
}
type objectFact struct {
fact analysis.Fact
// TODO(dh): why do we store the objectpath when producing the
// fact? Is it just for the sanity checking, which compares the
// stored path with a path recomputed from objectFactKey.Obj?
path objectpath.Path
}
type objectFactKey struct {
Obj types.Object
Type reflect.Type
}
type packageFactKey struct {
Pkg *types.Package
Type reflect.Type
}
type gobFact struct {
PkgPath string
ObjPath string
Fact analysis.Fact
}
// TestFact is a serialization of facts that is specific to the test mode.
type TestFact struct {
ObjectName string
Position token.Position
FactString string
Analyzer string
}
// analyzerAction describes the act of analyzing a package with a
// single analyzer.
type analyzerAction struct {
baseAction
// Action description
Analyzer *analysis.Analyzer
// Action results
// We can store actual results here without worrying about memory
// consumption because analyzer actions get garbage collected once
// a package has been fully analyzed.
Result interface{}
Diagnostics []Diagnostic
ObjectFacts map[objectFactKey]objectFact
PackageFacts map[packageFactKey]analysis.Fact
Pass *analysis.Pass
}
func (act *analyzerAction) String() string {
return fmt.Sprintf("analyzerAction(%s)", act.Analyzer)
}
// A Runner executes analyzers on packages.
type Runner struct {
Stats Stats
GoVersion string
// if GoVersion == "module", and we couldn't determine the
// module's Go version, use this as the fallback
FallbackGoVersion string
// If set to true, Runner will populate results with data relevant to testing analyzers
TestMode bool
// GoVersion might be "module"; actualGoVersion contains the resolved version
actualGoVersion string
// Config that gets merged with per-package configs
cfg config.Config
cache *cache.Cache
semaphore tsync.Semaphore
}
type subrunner struct {
*Runner
analyzers []*analysis.Analyzer
factAnalyzers []*analysis.Analyzer
analyzerNames string
cache *cache.Cache
}
// New returns a new Runner.
func New(cfg config.Config, c *cache.Cache) (*Runner, error) {
return &Runner{
cfg: cfg,
cache: c,
semaphore: tsync.NewSemaphore(runtime.GOMAXPROCS(0)),
}, nil
}
func newSubrunner(r *Runner, analyzers []*analysis.Analyzer) *subrunner {
analyzerNames := make([]string, len(analyzers))
for i, a := range analyzers {
analyzerNames[i] = a.Name
}
sort.Strings(analyzerNames)
var factAnalyzers []*analysis.Analyzer
for _, a := range analyzers {
if len(a.FactTypes) > 0 {
factAnalyzers = append(factAnalyzers, a)
}
}
return &subrunner{
Runner: r,
analyzers: analyzers,
factAnalyzers: factAnalyzers,
analyzerNames: strings.Join(analyzerNames, ","),
cache: r.cache,
}
}
func newPackageActionRoot(pkg *loader.PackageSpec, cache map[*loader.PackageSpec]*packageAction) *packageAction {
a := newPackageAction(pkg, cache)
a.factsOnly = false
return a
}
func newPackageAction(pkg *loader.PackageSpec, cache map[*loader.PackageSpec]*packageAction) *packageAction {
if a, ok := cache[pkg]; ok {
return a
}
a := &packageAction{
Package: pkg,
factsOnly: true, // will be overwritten by any call to Action
}
cache[pkg] = a
if len(pkg.Errors) > 0 {
a.errors = make([]error, len(pkg.Errors))
for i, err := range pkg.Errors {
a.errors[i] = err
}
a.failed = true
// We don't need to process our imports if this package is
// already broken.
return a
}
a.deps = make([]action, 0, len(pkg.Imports))
for _, dep := range pkg.Imports {
depa := newPackageAction(dep, cache)
depa.triggers = append(depa.triggers, a)
a.deps = append(a.deps, depa)
if depa.failed {
a.failed = true
}
}
// sort dependencies because the list of dependencies is part of
// the cache key
sort.Slice(a.deps, func(i, j int) bool {
return a.deps[i].(*packageAction).Package.ID < a.deps[j].(*packageAction).Package.ID
})
a.pending = uint32(len(a.deps))
return a
}
func newAnalyzerAction(an *analysis.Analyzer, cache map[*analysis.Analyzer]*analyzerAction) *analyzerAction {
if a, ok := cache[an]; ok {
return a
}
a := &analyzerAction{
Analyzer: an,
ObjectFacts: map[objectFactKey]objectFact{},
PackageFacts: map[packageFactKey]analysis.Fact{},
}
cache[an] = a
for _, dep := range an.Requires {
depa := newAnalyzerAction(dep, cache)
depa.triggers = append(depa.triggers, a)
a.deps = append(a.deps, depa)
}
a.pending = uint32(len(a.deps))
return a
}
func getCachedFiles(cache *cache.Cache, ids []cache.ActionID, out []*string) error {
for i, id := range ids {
var err error
*out[i], _, err = cache.GetFile(id)
if err != nil {
return err
}
}
return nil
}
func (r *subrunner) do(act action) error {
a := act.(*packageAction)
defer func() {
r.Stats.finishPackage()
if !a.factsOnly {
r.Stats.finishInitialPackage()
}
}()
// compute hash of action
a.cfg = a.Package.Config.Merge(r.cfg)
h := r.cache.NewHash("staticcheck " + a.Package.PkgPath)
// Note that we do not filter the list of analyzers by the
// package's configuration. We don't allow configuration to
// accidentally break dependencies between analyzers, and it's
// easier to always run all checks and filter the output. This
// also makes cached data more reusable.
// OPT(dh): not all changes in configuration invalidate cached
// data. specifically, when a.factsOnly == true, we only care
// about checks that produce facts, and settings that affect those
// checks.
// Config used for constructing the hash; this config doesn't have
// Checks populated, because we always run all checks.
//
// This even works for users who add custom checks, because we include the binary's hash.
hashCfg := a.cfg
hashCfg.Checks = nil
// note that we don't hash staticcheck's version; it is set as the
// salt by a package main.
fmt.Fprintf(h, "cfg %#v\n", hashCfg)
fmt.Fprintf(h, "pkg %x\n", a.Package.Hash)
fmt.Fprintf(h, "analyzers %s\n", r.analyzerNames)
fmt.Fprintf(h, "go %s\n", r.actualGoVersion)
// OPT(dh): do we actually need to hash vetx? can we not assume
// that for identical inputs, staticcheck will produce identical
// vetx?
for _, dep := range a.deps {
dep := dep.(*packageAction)
vetxHash, err := cache.FileHash(dep.vetx)
if err != nil {
return fmt.Errorf("failed computing hash: %w", err)
}
fmt.Fprintf(h, "vetout %q %x\n", dep.Package.PkgPath, vetxHash)
}
a.hash = cache.ActionID(h.Sum())
// try to fetch hashed data
ids := make([]cache.ActionID, 0, 2)
ids = append(ids, cache.Subkey(a.hash, "vetx"))
if !a.factsOnly {
ids = append(ids, cache.Subkey(a.hash, "results"))
if r.TestMode {
ids = append(ids, cache.Subkey(a.hash, "testdata"))
}
}
if err := getCachedFiles(r.cache, ids, []*string{&a.vetx, &a.results, &a.testData}); err != nil {
result, err := r.doUncached(a)
if err != nil {
return err
}
if a.failed {
return nil
}
a.skipped = result.skipped
// OPT(dh) instead of collecting all object facts and encoding
// them after analysis finishes, we could encode them as we
// go. however, that would require some locking.
//
// OPT(dh): We could sort gobFacts for more consistent output,
// but it doesn't matter. The hash of a package includes all
// of its files, so whether the vetx hash changes or not, a
// change to a package requires re-analyzing all dependents,
// even if the vetx data stayed the same. See also the note at
// the top of loader/hash.go.
tf := &bytes.Buffer{}
enc := gob.NewEncoder(tf)
for _, gf := range result.facts {
if err := enc.Encode(gf); err != nil {
return fmt.Errorf("failed gob encoding data: %w", err)
}
}
a.vetx, err = r.writeCacheReader(a, "vetx", bytes.NewReader(tf.Bytes()))
if err != nil {
return err
}
if a.factsOnly {
return nil
}
var out ResultData
out.Directives = make([]SerializedDirective, len(result.dirs))
for i, dir := range result.dirs {
out.Directives[i] = serializeDirective(dir, result.lpkg.Fset)
}
out.Diagnostics = result.diags
out.Unused = result.unused
a.results, err = r.writeCacheGob(a, "results", out)
if err != nil {
return err
}
if r.TestMode {
out := TestData{
Wants: result.wants,
Facts: result.testFacts,
}
a.testData, err = r.writeCacheGob(a, "testdata", out)
if err != nil {
return err
}
}
}
return nil
}
// ActiveWorkers returns the number of currently running workers.
func (r *Runner) ActiveWorkers() int {
return r.semaphore.Len()
}
// TotalWorkers returns the maximum number of possible workers.
func (r *Runner) TotalWorkers() int {
return r.semaphore.Cap()
}
func (r *Runner) writeCacheReader(a *packageAction, kind string, rs io.ReadSeeker) (string, error) {
h := cache.Subkey(a.hash, kind)
out, _, err := r.cache.Put(h, rs)
if err != nil {
return "", fmt.Errorf("failed caching data: %w", err)
}
return r.cache.OutputFile(out), nil
}
func (r *Runner) writeCacheGob(a *packageAction, kind string, data interface{}) (string, error) {
f, err := ioutil.TempFile("", "staticcheck")
if err != nil {
return "", err
}
defer f.Close()
os.Remove(f.Name())
if err := gob.NewEncoder(f).Encode(data); err != nil {
return "", fmt.Errorf("failed gob encoding data: %w", err)
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return "", err
}
return r.writeCacheReader(a, kind, f)
}
type packageActionResult struct {
facts []gobFact
diags []Diagnostic
unused unused.SerializedResult
dirs []lint.Directive
lpkg *loader.Package
skipped bool
// Only set when using test mode
testFacts []TestFact
wants []Want
}
func (r *subrunner) doUncached(a *packageAction) (packageActionResult, error) {
// OPT(dh): for a -> b; c -> b; if both a and b are being
// processed concurrently, we shouldn't load b's export data
// twice.
pkg, _, err := loader.Load(a.Package)
if err != nil {
return packageActionResult{}, err
}
if len(pkg.Errors) > 0 {
// this handles errors that occurred during type-checking the
// package in loader.Load
for _, err := range pkg.Errors {
a.errors = append(a.errors, err)
}
a.failed = true
return packageActionResult{}, nil
}
if len(pkg.Syntax) == 0 && pkg.PkgPath != "unsafe" {
return packageActionResult{lpkg: pkg, skipped: true}, nil
}
// OPT(dh): instead of parsing directives twice (twice because
// U1000 depends on the facts.Directives analyzer), reuse the
// existing result
var dirs []lint.Directive
if !a.factsOnly {
dirs = lint.ParseDirectives(pkg.Syntax, pkg.Fset)
}
res, err := r.runAnalyzers(a, pkg)
var wants []Want
if r.TestMode {
// Extract 'want' comments from parsed Go files.
for _, f := range pkg.Syntax {
for _, cgroup := range f.Comments {
for _, c := range cgroup.List {
text := strings.TrimPrefix(c.Text, "//")
if text == c.Text { // not a //-comment.
text = strings.TrimPrefix(text, "/*")
text = strings.TrimSuffix(text, "*/")
}
// Hack: treat a comment of the form "//...// want..."
// or "/*...// want... */
// as if it starts at 'want'.
// This allows us to add comments on comments,
// as required when testing the buildtag analyzer.
if i := strings.Index(text, "// want"); i >= 0 {
text = text[i+len("// "):]
}
posn := pkg.Fset.Position(c.Pos())
wants = append(wants, Want{Position: posn, Comment: text})
}
}
}
// TODO(dh): add support for non-Go files
}
return packageActionResult{
facts: res.facts,
testFacts: res.testFacts,
wants: wants,
diags: res.diagnostics,
unused: res.unused,
dirs: dirs,
lpkg: pkg,
}, err
}
func pkgPaths(root *types.Package) map[string]*types.Package {
out := map[string]*types.Package{}
var dfs func(*types.Package)
dfs = func(pkg *types.Package) {
if _, ok := out[pkg.Path()]; ok {
return
}
out[pkg.Path()] = pkg
for _, imp := range pkg.Imports() {
dfs(imp)
}
}
dfs(root)
return out
}
func (r *Runner) loadFacts(root *types.Package, dep *packageAction, objFacts map[objectFactKey]objectFact, pkgFacts map[packageFactKey]analysis.Fact) error {
// Load facts of all imported packages
vetx, err := os.Open(dep.vetx)
if err != nil {
return fmt.Errorf("failed loading cached facts: %w", err)
}
defer vetx.Close()
pathToPkg := pkgPaths(root)
dec := gob.NewDecoder(vetx)
for {
var gf gobFact
err := dec.Decode(&gf)
if err != nil {
if err == io.EOF {
break
}
return fmt.Errorf("failed loading cached facts: %w", err)
}
pkg, ok := pathToPkg[gf.PkgPath]
if !ok {
continue
}
if gf.ObjPath == "" {
pkgFacts[packageFactKey{
Pkg: pkg,
Type: reflect.TypeOf(gf.Fact),
}] = gf.Fact
} else {
obj, err := objectpath.Object(pkg, objectpath.Path(gf.ObjPath))
if err != nil {
continue
}
objFacts[objectFactKey{
Obj: obj,
Type: reflect.TypeOf(gf.Fact),
}] = objectFact{gf.Fact, objectpath.Path(gf.ObjPath)}
}
}
return nil
}
func genericHandle(a action, root action, queue chan action, sem *tsync.Semaphore, exec func(a action) error) {
if a == root {
close(queue)
if sem != nil {
sem.Release()
}
return
}
if !a.IsFailed() {
// the action may have already been marked as failed during
// construction of the action graph, for example because of
// unresolved imports.
for _, dep := range a.Deps() {
if dep.IsFailed() {
// One of our dependencies failed, so mark this package as
// failed and bail. We don't need to record an error for
// this package, the relevant error will have been
// reported by the first package in the chain that failed.
a.MarkFailed()
break
}
}
}
if !a.IsFailed() {
if err := exec(a); err != nil {
a.MarkFailed()
a.AddError(err)
}
}
if sem != nil {
sem.Release()
}
for _, t := range a.Triggers() {
if t.DecrementPending() {
queue <- t
}
}
}
type analyzerRunner struct {
pkg *loader.Package
// object facts of our dependencies; may contain facts of
// analyzers other than the current one
depObjFacts map[objectFactKey]objectFact
// package facts of our dependencies; may contain facts of
// analyzers other than the current one
depPkgFacts map[packageFactKey]analysis.Fact
factsOnly bool
stats *Stats
}
func (ar *analyzerRunner) do(act action) error {
a := act.(*analyzerAction)
results := map[*analysis.Analyzer]interface{}{}
// TODO(dh): does this have to be recursive?
for _, dep := range a.deps {
dep := dep.(*analyzerAction)
results[dep.Analyzer] = dep.Result
}
// OPT(dh): cache factTypes, it is the same for all packages for a given analyzer
//
// OPT(dh): do we need the factTypes map? most analyzers have 0-1
// fact types. iterating over the slice is probably faster than
// indexing a map.
factTypes := map[reflect.Type]struct{}{}
for _, typ := range a.Analyzer.FactTypes {
factTypes[reflect.TypeOf(typ)] = struct{}{}
}
filterFactType := func(typ reflect.Type) bool {
_, ok := factTypes[typ]
return ok
}
a.Pass = &analysis.Pass{
Analyzer: a.Analyzer,
Fset: ar.pkg.Fset,
Files: ar.pkg.Syntax,
OtherFiles: ar.pkg.OtherFiles,
Pkg: ar.pkg.Types,
TypesInfo: ar.pkg.TypesInfo,
TypesSizes: ar.pkg.TypesSizes,
Report: func(diag analysis.Diagnostic) {
if !ar.factsOnly {
if diag.Category == "" {
diag.Category = a.Analyzer.Name
}
d := Diagnostic{
Position: report.DisplayPosition(ar.pkg.Fset, diag.Pos),
End: report.DisplayPosition(ar.pkg.Fset, diag.End),
Category: diag.Category,
Message: diag.Message,
}
for _, sugg := range diag.SuggestedFixes {
s := SuggestedFix{
Message: sugg.Message,
}
for _, edit := range sugg.TextEdits {
s.TextEdits = append(s.TextEdits, TextEdit{
Position: report.DisplayPosition(ar.pkg.Fset, edit.Pos),
End: report.DisplayPosition(ar.pkg.Fset, edit.End),
NewText: edit.NewText,
})
}
d.SuggestedFixes = append(d.SuggestedFixes, s)
}
for _, rel := range diag.Related {
d.Related = append(d.Related, RelatedInformation{
Position: report.DisplayPosition(ar.pkg.Fset, rel.Pos),
End: report.DisplayPosition(ar.pkg.Fset, rel.End),
Message: rel.Message,
})
}
a.Diagnostics = append(a.Diagnostics, d)
}
},
ResultOf: results,
ImportObjectFact: func(obj types.Object, fact analysis.Fact) bool {
key := objectFactKey{
Obj: obj,
Type: reflect.TypeOf(fact),
}
if f, ok := ar.depObjFacts[key]; ok {
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f.fact).Elem())
return true
} else if f, ok := a.ObjectFacts[key]; ok {
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f.fact).Elem())
return true
}
return false
},
ImportPackageFact: func(pkg *types.Package, fact analysis.Fact) bool {
key := packageFactKey{
Pkg: pkg,
Type: reflect.TypeOf(fact),
}
if f, ok := ar.depPkgFacts[key]; ok {
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
return true
} else if f, ok := a.PackageFacts[key]; ok {
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
return true
}
return false
},
ExportObjectFact: func(obj types.Object, fact analysis.Fact) {
key := objectFactKey{
Obj: obj,
Type: reflect.TypeOf(fact),
}
path, _ := objectpath.For(obj)
a.ObjectFacts[key] = objectFact{fact, path}
},
ExportPackageFact: func(fact analysis.Fact) {
key := packageFactKey{
Pkg: ar.pkg.Types,
Type: reflect.TypeOf(fact),
}
a.PackageFacts[key] = fact
},
AllPackageFacts: func() []analysis.PackageFact {
out := make([]analysis.PackageFact, 0, len(ar.depPkgFacts)+len(a.PackageFacts))
for key, fact := range ar.depPkgFacts {
out = append(out, analysis.PackageFact{
Package: key.Pkg,
Fact: fact,
})
}
for key, fact := range a.PackageFacts {
out = append(out, analysis.PackageFact{
Package: key.Pkg,
Fact: fact,
})
}
return out
},
AllObjectFacts: func() []analysis.ObjectFact {
out := make([]analysis.ObjectFact, 0, len(ar.depObjFacts)+len(a.ObjectFacts))
for key, fact := range ar.depObjFacts {
if filterFactType(key.Type) {
out = append(out, analysis.ObjectFact{
Object: key.Obj,
Fact: fact.fact,
})
}
}
for key, fact := range a.ObjectFacts {
if filterFactType(key.Type) {
out = append(out, analysis.ObjectFact{
Object: key.Obj,
Fact: fact.fact,
})
}
}
return out
},
}
t := time.Now()
res, err := a.Analyzer.Run(a.Pass)
ar.stats.measureAnalyzer(a.Analyzer, ar.pkg.PackageSpec, time.Since(t))
if err != nil {
return err
}
a.Result = res
return nil
}
type analysisResult struct {
facts []gobFact
diagnostics []Diagnostic
unused unused.SerializedResult
// Only set when using test mode
testFacts []TestFact
}
func (r *subrunner) runAnalyzers(pkgAct *packageAction, pkg *loader.Package) (analysisResult, error) {
depObjFacts := map[objectFactKey]objectFact{}
depPkgFacts := map[packageFactKey]analysis.Fact{}
for _, dep := range pkgAct.deps {
if err := r.loadFacts(pkg.Types, dep.(*packageAction), depObjFacts, depPkgFacts); err != nil {
return analysisResult{}, err
}
}
root := &analyzerAction{}
var analyzers []*analysis.Analyzer
if pkgAct.factsOnly {
// When analyzing non-initial packages, we only care about
// analyzers that produce facts.
analyzers = r.factAnalyzers
} else {
analyzers = r.analyzers
}
all := map[*analysis.Analyzer]*analyzerAction{}
for _, a := range analyzers {
a := newAnalyzerAction(a, all)
root.deps = append(root.deps, a)
a.triggers = append(a.triggers, root)
}
root.pending = uint32(len(root.deps))
ar := &analyzerRunner{
pkg: pkg,
factsOnly: pkgAct.factsOnly,
depObjFacts: depObjFacts,
depPkgFacts: depPkgFacts,
stats: &r.Stats,
}
queue := make(chan action, len(all))
for _, a := range all {
if len(a.Deps()) == 0 {
queue <- a
}
}
// Don't hang if there are no analyzers to run; for example
// because we are analyzing a dependency but have no analyzers
// that produce facts.
if len(all) == 0 {
close(queue)
}
for item := range queue {
b := r.semaphore.AcquireMaybe()
if b {
go genericHandle(item, root, queue, &r.semaphore, ar.do)
} else {
// the semaphore is exhausted; run the analysis under the
// token we've acquired for analyzing the package.
genericHandle(item, root, queue, nil, ar.do)
}
}
var unusedResult unused.SerializedResult
for _, a := range all {
if a != root && a.Analyzer.Name == "U1000" && !a.failed {
// TODO(dh): figure out a clean abstraction, instead of
// special-casing U1000.
unusedResult = unused.Serialize(a.Pass, a.Result.(unused.Result), pkg.Fset)
}
for key, fact := range a.ObjectFacts {
depObjFacts[key] = fact
}
for key, fact := range a.PackageFacts {
depPkgFacts[key] = fact
}
}
// OPT(dh): cull objects not reachable via the exported closure
var testFacts []TestFact
gobFacts := make([]gobFact, 0, len(depObjFacts)+len(depPkgFacts))
for key, fact := range depObjFacts {
if fact.path == "" {
continue
}
if sanityCheck {
p, _ := objectpath.For(key.Obj)
if p != fact.path {
panic(fmt.Sprintf("got different object paths for %v. old: %q new: %q", key.Obj, fact.path, p))
}
}
gf := gobFact{
PkgPath: key.Obj.Pkg().Path(),
ObjPath: string(fact.path),
Fact: fact.fact,
}
gobFacts = append(gobFacts, gf)
}
for key, fact := range depPkgFacts {
gf := gobFact{
PkgPath: key.Pkg.Path(),
Fact: fact,
}
gobFacts = append(gobFacts, gf)
}
if r.TestMode {
for _, a := range all {
for key, fact := range a.ObjectFacts {
tgf := TestFact{
ObjectName: key.Obj.Name(),
Position: pkg.Fset.Position(key.Obj.Pos()),
FactString: fmt.Sprint(fact.fact),
Analyzer: a.Analyzer.Name,
}
testFacts = append(testFacts, tgf)
}
for _, fact := range a.PackageFacts {
tgf := TestFact{
ObjectName: "",
Position: pkg.Fset.Position(pkg.Syntax[0].Pos()),
FactString: fmt.Sprint(fact),
Analyzer: a.Analyzer.Name,
}
testFacts = append(testFacts, tgf)
}
}
}
var diags []Diagnostic
for _, a := range root.deps {
a := a.(*analyzerAction)
diags = append(diags, a.Diagnostics...)
}
return analysisResult{
facts: gobFacts,
testFacts: testFacts,
diagnostics: diags,
unused: unusedResult,
}, nil
}
func registerGobTypes(analyzers []*analysis.Analyzer) {
for _, a := range analyzers {
for _, typ := range a.FactTypes {
// FIXME(dh): use RegisterName so we can work around collisions
// in names. For pointer-types, gob incorrectly qualifies
// type names with the package name, not the import path.
gob.Register(typ)
}
}
}
func allAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer {
seen := map[*analysis.Analyzer]struct{}{}
out := make([]*analysis.Analyzer, 0, len(analyzers))
var dfs func(*analysis.Analyzer)
dfs = func(a *analysis.Analyzer) {
if _, ok := seen[a]; ok {
return
}
seen[a] = struct{}{}
out = append(out, a)
for _, dep := range a.Requires {
dfs(dep)
}
}
for _, a := range analyzers {
dfs(a)
}
return out
}
// Run loads the packages specified by patterns, runs analyzers on
// them and returns the results. Each result corresponds to a single
// package. Results will be returned for all packages, including
// dependencies. Errors specific to packages will be reported in the
// respective results.
//
// If cfg is nil, a default config will be used. Otherwise, cfg will
// be used, with the exception of the Mode field.
func (r *Runner) Run(cfg *packages.Config, analyzers []*analysis.Analyzer, patterns []string) ([]Result, error) {
analyzers = allAnalyzers(analyzers)
registerGobTypes(analyzers)
r.Stats.setState(StateLoadPackageGraph)
lpkgs, err := loader.Graph(r.cache, cfg, patterns...)
if err != nil {
return nil, err
}
r.Stats.setInitialPackages(len(lpkgs))
if len(lpkgs) == 0 {
return nil, nil
}
var goVersion string
if r.GoVersion == "module" {
for _, lpkg := range lpkgs {
if m := lpkg.Module; m != nil {
if goVersion == "" {
goVersion = m.GoVersion
} else if goVersion != m.GoVersion {
// Theoretically, we should only ever see a single Go
// module. At least that's currently (as of Go 1.15)
// true when using 'go list'.
fmt.Fprintln(os.Stderr, "warning: encountered multiple modules and could not deduce targeted Go version")
goVersion = ""
break
}
}
}
} else {
goVersion = r.GoVersion
}
if goVersion == "" {
if r.FallbackGoVersion == "" {
panic("could not determine Go version of module, and fallback version hasn't been set")
}
goVersion = r.FallbackGoVersion
}
r.actualGoVersion = goVersion
for _, a := range analyzers {
flag := a.Flags.Lookup("go")
if flag == nil {
continue
}
if err := flag.Value.Set(goVersion); err != nil {
return nil, err
}
}
r.Stats.setState(StateBuildActionGraph)
all := map[*loader.PackageSpec]*packageAction{}
root := &packageAction{}
for _, lpkg := range lpkgs {
a := newPackageActionRoot(lpkg, all)
root.deps = append(root.deps, a)
a.triggers = append(a.triggers, root)
}
root.pending = uint32(len(root.deps))
queue := make(chan action)
r.Stats.setTotalPackages(len(all) - 1)
r.Stats.setState(StateProcessing)
go func() {
for _, a := range all {
if len(a.Deps()) == 0 {
queue <- a
}
}
}()
sr := newSubrunner(r, analyzers)
for item := range queue {
r.semaphore.Acquire()
go genericHandle(item, root, queue, &r.semaphore, func(act action) error {
return sr.do(act)
})
}
r.Stats.setState(StateFinalizing)
out := make([]Result, 0, len(all))
for _, item := range all {
if item.Package == nil {
continue
}
out = append(out, Result{
Package: item.Package,
Config: item.cfg,
Initial: !item.factsOnly,
Skipped: item.skipped,
Failed: item.failed,
Errors: item.errors,
results: item.results,
testData: item.testData,
})
}
return out, nil
}