// 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 }