blob: e9f1c7f7ce923a13bd4d606bc6ec27d3df9137af [file] [log] [blame]
// Copyright 2019 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 cache
// This file defines gopls' driver for modular static analysis (go/analysis).
import (
"bytes"
"context"
"crypto/sha256"
"encoding/gob"
"errors"
"fmt"
"go/ast"
"go/token"
"go/types"
"log"
"reflect"
"runtime/debug"
"sort"
"strings"
"sync"
"time"
"golang.org/x/sync/errgroup"
"golang.org/x/tools/go/analysis"
"golang.org/x/tools/gopls/internal/lsp/filecache"
"golang.org/x/tools/gopls/internal/lsp/protocol"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/facts"
"golang.org/x/tools/internal/gcimporter"
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
)
/*
DESIGN
An analysis request is for a set of analyzers and an individual
package ID, notated (a*, p). The result is the set of diagnostics
for that package. It could easily be generalized to a set of
packages, (a*, p*), and perhaps should be, to improve performance
versus calling it in a loop.
The snapshot holds a cache (persistent.Map) of entries keyed by
(a*, p) pairs ("analysisKey") that have been requested so far. Some
of these entries may be invalidated during snapshot cloning after a
modification event. The cache maps each (a*, p) to a promise of
the analysis result or "analysisSummary". The summary contains the
results of analysis (e.g. diagnostics) as well as the intermediate
results required by the recursion, such as serialized types and
facts.
The promise represents the result of a call to analyzeImpl, which
type-checks a package and then applies a graph of analyzers to it
in parallel postorder. (These graph edges are "horizontal": within
the same package.) First, analyzeImpl reads the source files of
package p, and obtains (recursively) the results of the "vertical"
dependencies (i.e. analyzers applied to the packages imported by
p). Only the subset of analyzers that use facts need be executed
recursively, but even if this subset is empty, the step is still
necessary because it provides type information. It is possible that
a package may need to be type-checked and analyzed twice, for
different subsets of analyzers, but the overlap is typically
insignificant.
With the file contents and the results of vertical dependencies,
analyzeImpl is then in a position to produce a key representing the
unit of work (parsing, type-checking, and analysis) that it has to
do. The key is a cryptographic hash of the "recipe" for this step,
including the Metadata, the file contents, the set of analyzers,
and the type and fact information from the vertical dependencies.
The key is sought in a machine-global persistent file-system based
cache. If this gopls process, or another gopls process on the same
machine, has already performed this analysis step, analyzeImpl will
make a cache hit and load the serialized summary of the results. If
not, it will have to proceed to type-checking and analysis, and
write a new cache entry. The entry contains serialized types
(export data) and analysis facts.
For types, we use "shallow" export data. Historically, the Go
compiler always produced a summary of the types for a given package
that included types from other packages that it indirectly
referenced: "deep" export data. This had the advantage that the
compiler (and analogous tools such as gopls) need only load one
file per direct import. However, it meant that the files tended to
get larger based on the level of the package in the import
graph. For example, higher-level packages in the kubernetes module
have over 1MB of "deep" export data, even when they have almost no
content of their own, merely because they mention a major type that
references many others. In pathological cases the export data was
300x larger than the source for a package due to this quadratic
growth.
"Shallow" export data means that the serialized types describe only
a single package. If those types mention types from other packages,
the type checker may need to request additional packages beyond
just the direct imports. This means type information for the entire
transitive closure of imports may need to be available just in
case. After a cache hit or a cache miss, the summary is
postprocessed so that it contains the union of export data payloads
of all its direct dependencies.
For correct dependency analysis, the digest used as a cache key
must reflect the "deep" export data, so it is derived recursively
from the transitive closure. As an optimization, we needn't include
every package of the transitive closure in the deep hash, only the
packages that were actually requested by the type checker. This
allows changes to a package that have no effect on its export data
to be "pruned". The direct consumer will need to be re-executed,
but if its export data is unchanged as a result, then indirect
consumers may not need to be re-executed. This allows, for example,
one to insert a print statement in a function and not "rebuild" the
whole application (though export data does record line numbers of
types which may be perturbed by otherwise insignificant changes.)
The summary must record whether a package is transitively
error-free (whether it would compile) because many analyzers are
not safe to run on packages with inconsistent types.
For fact encoding, we use the same fact set as the unitchecker
(vet) to record and serialize analysis facts. The fact
serialization mechanism is analogous to "deep" export data.
*/
// TODO(adonovan):
// - Profile + optimize:
// - on a cold run, mostly type checking + export data, unsurprisingly.
// - on a hot-disk run, mostly type checking the IWL.
// Would be nice to have a benchmark that separates this out.
// - measure and record in the code the typical operation times
// and file sizes (export data + facts = cache entries).
// - Do "port the old logic" tasks (see TODO in actuallyAnalyze).
// - Add a (white-box) test of pruning when a change doesn't affect export data.
// - Optimise pruning based on subset of packages mentioned in exportdata.
// - Better logging so that it is possible to deduce why an analyzer
// is not being run--often due to very indirect failures.
// Even if the ultimate consumer decides to ignore errors,
// tests and other situations want to be assured of freedom from
// errors, not just missing results. This should be recorded.
// - Check that the event trace is intelligible.
// - Split this into a subpackage, gopls/internal/lsp/cache/driver,
// consisting of this file and three helpers from errors.go.
// The (*snapshot).Analyze method would stay behind and make calls
// to the driver package.
// Steps:
// - define a narrow driver.Snapshot interface with only these methods:
// Metadata(PackageID) source.Metadata
// GetFile(Context, URI) (source.FileHandle, error)
// View() *View // for Options
// - define a State type that encapsulates the persistent map
// (with its own mutex), and has methods:
// New() *State
// Clone(invalidate map[PackageID]bool) *State
// Destroy()
// - share cache.{goVersionRx,parseGoImpl}
var born = time.Now()
// Analyze applies a set of analyzers to the package denoted by id,
// and returns their diagnostics for that package.
//
// The analyzers list must be duplicate free; order does not matter.
//
// Precondition: all analyzers within the process have distinct names.
// (The names are relied on by the serialization logic.)
func (s *snapshot) Analyze(ctx context.Context, id PackageID, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) {
if false { // debugging
log.Println("Analyze@", time.Since(born)) // called after the 7s IWL in k8s
}
// Filter and sort enabled root analyzers.
// A disabled analyzer may still be run if required by another.
toSrc := make(map[*analysis.Analyzer]*source.Analyzer)
var enabled []*analysis.Analyzer
for _, a := range analyzers {
if a.IsEnabled(s.view.Options()) {
toSrc[a.Analyzer] = a
enabled = append(enabled, a.Analyzer)
}
}
sort.Slice(enabled, func(i, j int) bool {
return enabled[i].Name < enabled[j].Name
})
// Register fact types of required analyzers.
for _, a := range requiredAnalyzers(enabled) {
for _, f := range a.FactTypes {
gob.Register(f)
}
}
if false { // debugging
// TODO(adonovan): use proper tracing.
t0 := time.Now()
defer func() {
log.Printf("%v for analyze(%s, %s)", time.Since(t0), id, enabled)
}()
}
// Run the analysis.
res, err := s.analyze(ctx, id, enabled)
if err != nil {
return nil, err
}
// Report diagnostics only from enabled actions that succeeded.
// Errors from creating or analyzing packages are ignored.
// Diagnostics are reported in the order of the analyzers argument.
//
// TODO(adonovan): ignoring action errors gives the caller no way
// to distinguish "there are no problems in this code" from
// "the code (or analyzers!) are so broken that we couldn't even
// begin the analysis you asked for".
// Even if current callers choose to discard the
// results, we should propagate the per-action errors.
var results []*source.Diagnostic
for _, a := range enabled {
summary := res.Actions[a.Name]
if summary.Err != "" {
continue // action failed
}
for _, gobDiag := range summary.Diagnostics {
results = append(results, toSourceDiagnostic(toSrc[a], &gobDiag))
}
}
return results, nil
}
// analysisKey is the type of keys in the snapshot.analyses map.
type analysisKey struct {
analyzerNames string
pkgid PackageID
}
func (key analysisKey) String() string {
return fmt.Sprintf("%s@%s", key.analyzerNames, key.pkgid)
}
// analyzeSummary is a gob-serializable summary of successfully
// applying a list of analyzers to a package.
type analyzeSummary struct {
PkgPath PackagePath // types.Package.Path() (needed to decode export data)
Export []byte
DeepExportHash source.Hash // hash of reflexive transitive closure of export data
Compiles bool // transitively free of list/parse/type errors
Actions actionsMap // map from analyzer name to analysis results (*actionSummary)
// Not serialized: populated after the summary is computed or deserialized.
allExport map[PackagePath][]byte // transitive export data
}
// actionsMap defines a stable Gob encoding for a map.
// TODO(adonovan): generalize and move to a library when we can use generics.
type actionsMap map[string]*actionSummary
var _ gob.GobEncoder = (actionsMap)(nil)
var _ gob.GobDecoder = (*actionsMap)(nil)
type actionsMapEntry struct {
K string
V *actionSummary
}
func (m actionsMap) GobEncode() ([]byte, error) {
entries := make([]actionsMapEntry, 0, len(m))
for k, v := range m {
entries = append(entries, actionsMapEntry{k, v})
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].K < entries[j].K
})
var buf bytes.Buffer
err := gob.NewEncoder(&buf).Encode(entries)
return buf.Bytes(), err
}
func (m *actionsMap) GobDecode(data []byte) error {
var entries []actionsMapEntry
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil {
return err
}
*m = make(actionsMap, len(entries))
for _, e := range entries {
(*m)[e.K] = e.V
}
return nil
}
// actionSummary is a gob-serializable summary of one possibly failed analysis action.
// If Err is non-empty, the other fields are undefined.
type actionSummary struct {
Facts []byte // the encoded facts.Set
FactsHash source.Hash // hash(Facts)
Diagnostics []gobDiagnostic
Err string // "" => success
}
// analyze is a memoization of analyzeImpl.
func (s *snapshot) analyze(ctx context.Context, id PackageID, analyzers []*analysis.Analyzer) (*analyzeSummary, error) {
// Use the sorted list of names of analyzers in the key.
//
// TODO(adonovan): opt: account for analysis results at a
// finer grain to avoid duplicate work when a
// a proper subset of analyzers is requested?
// In particular, TypeErrorAnalyzers don't use facts
// but need to request vdeps just for type information.
names := make([]string, 0, len(analyzers))
for _, a := range analyzers {
names = append(names, a.Name)
}
// This key describes the result of applying a list of analyzers to a package.
key := analysisKey{strings.Join(names, ","), id}
// An analysisPromise represents the result of loading, parsing,
// type-checking and analyzing a single package.
type analysisPromise struct {
promise *memoize.Promise // [analyzeImplResult]
}
type analyzeImplResult struct {
summary *analyzeSummary
err error
}
// Access the map once, briefly, and atomically.
s.mu.Lock()
entry, hit := s.analyses.Get(key)
if !hit {
entry = analysisPromise{
promise: memoize.NewPromise("analysis", func(ctx context.Context, arg interface{}) interface{} {
summary, err := analyzeImpl(ctx, arg.(*snapshot), analyzers, id)
return analyzeImplResult{summary, err}
}),
}
s.analyses.Set(key, entry, nil) // nothing needs releasing
}
s.mu.Unlock()
// Await result.
ap := entry.(analysisPromise)
v, err := s.awaitPromise(ctx, ap.promise)
if err != nil {
return nil, err // e.g. cancelled
}
res := v.(analyzeImplResult)
return res.summary, res.err
}
// analyzeImpl applies a list of analyzers (plus any others
// transitively required by them) to a package. It succeeds as long
// as it could produce a types.Package, even if there were direct or
// indirect list/parse/type errors, and even if all the analysis
// actions failed. It usually fails only if the package was unknown,
// a file was missing, or the operation was cancelled.
//
// Postcondition: analyzeImpl must not continue to use the snapshot
// (in background goroutines) after it has returned; see memoize.RefCounted.
func analyzeImpl(ctx context.Context, snapshot *snapshot, analyzers []*analysis.Analyzer, id PackageID) (*analyzeSummary, error) {
m := snapshot.Metadata(id)
if m == nil {
return nil, fmt.Errorf("no metadata for %s", id)
}
// Recursively analyze each "vertical" dependency
// for its types.Package and (perhaps) analysis.Facts.
// If any of them fails to produce a package, we cannot continue.
// We request only the analyzers that produce facts.
//
// Also, load the contents of each "compiled" Go file through
// the snapshot's cache.
//
// Both loops occur in parallel, and parallel with each other.
vdeps := make(map[PackageID]*analyzeSummary)
compiledGoFiles := make([]source.FileHandle, len(m.CompiledGoFiles))
{
var group errgroup.Group
// Analyze vertical dependencies.
// We request only the required analyzers that use facts.
var useFacts []*analysis.Analyzer
for _, a := range requiredAnalyzers(analyzers) {
if len(a.FactTypes) > 0 {
useFacts = append(useFacts, a)
}
}
var vdepsMu sync.Mutex
for _, id := range m.DepsByPkgPath {
id := id
group.Go(func() error {
res, err := snapshot.analyze(ctx, id, useFacts)
if err != nil {
return err // cancelled, or failed to produce a package
}
vdepsMu.Lock()
vdeps[id] = res
vdepsMu.Unlock()
return nil
})
}
// Read file contents.
// (In practice these will be cache hits
// on reads done by the initial workspace load
// or after a change modification event.)
for i, uri := range m.CompiledGoFiles {
i, uri := i, uri
group.Go(func() error {
fh, err := snapshot.GetFile(ctx, uri) // ~25us
compiledGoFiles[i] = fh
return err // e.g. cancelled
})
}
if err := group.Wait(); err != nil {
return nil, err
}
}
// Inv: analyze() of all vdeps succeeded (though some actions may have failed).
// We no longer depend on the snapshot.
snapshot = nil
// At this point we have the action results (serialized
// packages and facts) of our immediate dependencies,
// and the metadata and content of this package.
//
// We now compute a hash for all our inputs, and consult a
// global cache of promised results. If nothing material
// has changed, we'll make a hit in the shared cache.
//
// The hash of our inputs is based on the serialized export
// data and facts so that immaterial changes can be pruned
// without decoding.
key := analysisCacheKey(analyzers, m, compiledGoFiles, vdeps)
// Access the cache.
var summary *analyzeSummary
const cacheKind = "analysis"
if data, err := filecache.Get(cacheKind, key); err == nil {
// cache hit
mustDecode(data, &summary)
} else if err != filecache.ErrNotFound {
return nil, bug.Errorf("internal error reading shared cache: %v", err)
} else {
// Cache miss: do the work.
var err error
summary, err = actuallyAnalyze(ctx, analyzers, m, vdeps, compiledGoFiles)
if err != nil {
return nil, err
}
data := mustEncode(summary)
if false {
log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), id)
}
if err := filecache.Set(cacheKind, key, data); err != nil {
return nil, fmt.Errorf("internal error updating shared cache: %v", err)
}
}
// Hit or miss, we need to merge the export data from
// dependencies so that it includes all the types
// that might be summoned by the type checker.
//
// TODO(adonovan): opt: reduce this set by recording
// which packages were actually summoned by insert().
// (Just makes map smaller; probably marginal?)
allExport := make(map[PackagePath][]byte)
for _, vdep := range vdeps {
for k, v := range vdep.allExport {
allExport[k] = v
}
}
allExport[m.PkgPath] = summary.Export
summary.allExport = allExport
return summary, nil
}
// analysisCacheKey returns a cache key that is a cryptographic digest
// of the all the values that might affect type checking and analysis:
// the analyzer names, package metadata, names and contents of
// compiled Go files, and vdeps information (export data and facts).
//
// TODO(adonovan): safety: define our own flavor of Metadata
// containing just the fields we need, and using it in the subsequent
// logic, to keep us honest about hashing all parts that matter?
func analysisCacheKey(analyzers []*analysis.Analyzer, m *source.Metadata, compiledGoFiles []source.FileHandle, vdeps map[PackageID]*analyzeSummary) [sha256.Size]byte {
hasher := sha256.New()
// In principle, a key must be the hash of an
// unambiguous encoding of all the relevant data.
// If it's ambiguous, we risk collisons.
// analyzers
fmt.Fprintf(hasher, "analyzers: %d\n", len(analyzers))
for _, a := range analyzers {
fmt.Fprintln(hasher, a.Name)
}
// package metadata
fmt.Fprintf(hasher, "package: %s %s %s\n", m.ID, m.Name, m.PkgPath)
// We can ignore m.DepsBy{Pkg,Import}Path: although the logic
// uses those fields, we account for them by hashing vdeps.
// type sizes
// This assertion is safe, but if a black-box implementation
// is ever needed, record Sizeof(*int) and Alignof(int64).
sz := m.TypesSizes.(*types.StdSizes)
fmt.Fprintf(hasher, "sizes: %d %d\n", sz.WordSize, sz.MaxAlign)
// metadata errors
for _, err := range m.Errors {
fmt.Fprintf(hasher, "error: %q", err)
}
// module Go version
if m.Module != nil && m.Module.GoVersion != "" {
fmt.Fprintf(hasher, "go %s\n", m.Module.GoVersion)
}
// file names and contents
fmt.Fprintf(hasher, "files: %d\n", len(compiledGoFiles))
for _, fh := range compiledGoFiles {
fmt.Fprintln(hasher, fh.FileIdentity())
}
// vdeps, in PackageID order
depIDs := make([]string, 0, len(vdeps))
for depID := range vdeps {
depIDs = append(depIDs, string(depID))
}
sort.Strings(depIDs)
for _, id := range depIDs {
vdep := vdeps[PackageID(id)]
fmt.Fprintf(hasher, "dep: %s\n", vdep.PkgPath)
fmt.Fprintf(hasher, "export: %s\n", vdep.DeepExportHash)
// action results: errors and facts
names := make([]string, 0, len(vdep.Actions))
for name := range vdep.Actions {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
summary := vdep.Actions[name]
fmt.Fprintf(hasher, "action %s\n", name)
if summary.Err != "" {
fmt.Fprintf(hasher, "error %s\n", summary.Err)
} else {
fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash)
// We can safely omit summary.diagnostics
// from the key since they have no downstream effect.
}
}
}
var hash [sha256.Size]byte
hasher.Sum(hash[:0])
return hash
}
// actuallyAnalyze implements the cache-miss case.
// This function does not access the snapshot.
func actuallyAnalyze(ctx context.Context, analyzers []*analysis.Analyzer, m *source.Metadata, vdeps map[PackageID]*analyzeSummary, compiledGoFiles []source.FileHandle) (*analyzeSummary, error) {
// Create a local FileSet for processing this package only.
fset := token.NewFileSet()
// Parse only the "compiled" Go files.
// Do the computation in parallel.
parsed := make([]*source.ParsedGoFile, len(compiledGoFiles))
{
var group errgroup.Group
for i, fh := range compiledGoFiles {
i, fh := i, fh
group.Go(func() error {
// Call parseGoImpl directly, not the caching wrapper,
// as cached ASTs require the global FileSet.
pgf, err := parseGoImpl(ctx, fset, fh, source.ParseFull)
parsed[i] = pgf
return err
})
}
if err := group.Wait(); err != nil {
return nil, err // cancelled, or catastrophic error (e.g. missing file)
}
}
// Type-check the package.
pkg := typeCheckForAnalysis(fset, parsed, m, vdeps)
// Build a map of PkgPath to *Package for all packages mentioned
// in exportdata for use by facts.
pkg.factsDecoder = facts.NewDecoder(pkg.types)
// Poll cancellation state.
if err := ctx.Err(); err != nil {
return nil, err
}
// TODO(adonovan): port the old logic to:
// - gather go/packages diagnostics from m.Errors? (port goPackagesErrorDiagnostics)
// - record unparseable file URIs so we can suppress type errors for these files.
// - gather diagnostics from expandErrors + typeErrorDiagnostics + depsErrors.
// -- analysis --
// Build action graph for this package.
// Each graph node (action) is one unit of analysis.
actions := make(map[*analysis.Analyzer]*action)
var mkAction func(a *analysis.Analyzer) *action
mkAction = func(a *analysis.Analyzer) *action {
act, ok := actions[a]
if !ok {
var hdeps []*action
for _, req := range a.Requires {
hdeps = append(hdeps, mkAction(req))
}
act = &action{a: a, pkg: pkg, vdeps: vdeps, hdeps: hdeps}
actions[a] = act
}
return act
}
// Build actions for initial package.
var roots []*action
for _, a := range analyzers {
roots = append(roots, mkAction(a))
}
// Execute the graph in parallel.
execActions(roots)
// Don't return (or cache) the result in case of cancellation.
if err := ctx.Err(); err != nil {
return nil, err // cancelled
}
// Return summaries only for the requested actions.
summaries := make(map[string]*actionSummary)
for _, act := range roots {
summaries[act.a.Name] = act.summary
}
return &analyzeSummary{
PkgPath: PackagePath(pkg.types.Path()),
Export: pkg.export,
DeepExportHash: pkg.deepExportHash,
Compiles: pkg.compiles,
Actions: summaries,
}, nil
}
func typeCheckForAnalysis(fset *token.FileSet, parsed []*source.ParsedGoFile, m *source.Metadata, vdeps map[PackageID]*analyzeSummary) *analysisPackage {
if false { // debugging
log.Println("typeCheckForAnalysis", m.PkgPath)
}
pkg := &analysisPackage{
m: m,
fset: fset,
parsed: parsed,
files: make([]*ast.File, len(parsed)),
compiles: len(m.Errors) == 0, // false => list error
types: types.NewPackage(string(m.PkgPath), string(m.Name)),
typesInfo: &types.Info{
Types: make(map[ast.Expr]types.TypeAndValue),
Defs: make(map[*ast.Ident]types.Object),
Uses: make(map[*ast.Ident]types.Object),
Implicits: make(map[ast.Node]types.Object),
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
},
typesSizes: m.TypesSizes,
}
typeparams.InitInstanceInfo(pkg.typesInfo)
for i, p := range parsed {
pkg.files[i] = p.File
if p.ParseErr != nil {
pkg.compiles = false // parse error
}
}
// Unsafe is special.
if m.PkgPath == "unsafe" {
pkg.types = types.Unsafe
return pkg
}
// Compute the union of transitive export data.
// (The actual values are shared, and not serialized.)
allExport := make(map[PackagePath][]byte)
for _, vdep := range vdeps {
for k, v := range vdep.allExport {
allExport[k] = v
}
if !vdep.Compiles {
pkg.compiles = false // transitive error
}
}
// exportHasher computes a hash of the names and export data of
// each package that was actually loaded during type checking.
//
// Because we use shallow export data, the hash for dependency
// analysis must incorporate indirect dependencies. As an
// optimization, we include only those that were actually
// used, which may be a small subset of those available.
//
// TODO(adonovan): opt: even better would be to implement a
// traversal over the package API like facts.NewDecoder does
// and only mention that set of packages in the hash.
// Perhaps there's a way to do that more efficiently.
//
// TODO(adonovan): opt: record the shallow hash alongside the
// shallow export data in the allExport map to avoid repeatedly
// hashing the export data.
//
// The writes to hasher below assume that type checking imports
// packages in a deterministic order.
exportHasher := sha256.New()
hashExport := func(pkgPath PackagePath, export []byte) {
fmt.Fprintf(exportHasher, "%s %d ", pkgPath, len(export))
exportHasher.Write(export)
}
// importer state
var (
insert func(p *types.Package, name string)
importMap = make(map[string]*types.Package) // keys are PackagePaths
)
loadFromExportData := func(pkgPath PackagePath) (*types.Package, error) {
export, ok := allExport[pkgPath]
if !ok {
return nil, bug.Errorf("missing export data for %q", pkgPath)
}
hashExport(pkgPath, export)
imported, err := gcimporter.IImportShallow(fset, importMap, export, string(pkgPath), insert)
if err != nil {
return nil, bug.Errorf("invalid export data for %q: %v", pkgPath, err)
}
return imported, nil
}
insert = func(p *types.Package, name string) {
imported, err := loadFromExportData(PackagePath(p.Path()))
if err != nil {
log.Fatalf("internal error: %v", err)
}
if imported != p {
log.Fatalf("internal error: inconsistent packages")
}
}
cfg := &types.Config{
Error: func(e error) {
pkg.compiles = false // type error
pkg.typeErrors = append(pkg.typeErrors, e.(types.Error))
},
Importer: importerFunc(func(importPath string) (*types.Package, error) {
if importPath == "unsafe" {
return types.Unsafe, nil // unsafe has no export data
}
// Beware that returning an error from this function
// will cause the type checker to synthesize a fake
// package whose Path is importPath, potentially
// losing a vendor/ prefix. If type-checking errors
// are swallowed, these packages may be confusing.
id, ok := m.DepsByImpPath[ImportPath(importPath)]
if !ok {
// The import syntax is inconsistent with the metadata.
// This could be because the import declaration was
// incomplete and the metadata only includes complete
// imports; or because the metadata ignores import
// edges that would lead to cycles in the graph.
return nil, fmt.Errorf("missing metadata for import of %q", importPath)
}
depResult, ok := vdeps[id] // id may be ""
if !ok {
// Analogous to (*snapshot).missingPkgError
// in the logic for regular type-checking,
// but without a snapshot we can't provide
// such detail, and anyway most analysis
// failures aren't surfaced in the UI.
return nil, fmt.Errorf("no required module provides package %q (id=%q)", importPath, id)
}
// (Duplicates logic from check.go.)
if !source.IsValidImport(m.PkgPath, depResult.PkgPath) {
return nil, fmt.Errorf("invalid use of internal package %s", importPath)
}
return loadFromExportData(depResult.PkgPath)
}),
}
// Set Go dialect.
if m.Module != nil && m.Module.GoVersion != "" {
goVersion := "go" + m.Module.GoVersion
// types.NewChecker panics if GoVersion is invalid.
// An unparsable mod file should probably stop us
// before we get here, but double check just in case.
if goVersionRx.MatchString(goVersion) {
typesinternal.SetGoVersion(cfg, goVersion)
}
}
// We want to type check cgo code if go/types supports it.
// We passed typecheckCgo to go/packages when we Loaded.
// TODO(adonovan): do we actually need this??
typesinternal.SetUsesCgo(cfg)
check := types.NewChecker(cfg, fset, pkg.types, pkg.typesInfo)
// Type checking errors are handled via the config, so ignore them here.
_ = check.Files(pkg.files)
// debugging (type errors are quite normal)
if false {
if pkg.typeErrors != nil {
log.Printf("package %s has type errors: %v", pkg.types.Path(), pkg.typeErrors)
}
}
// Emit the export data and compute the deep hash.
export, err := gcimporter.IExportShallow(pkg.fset, pkg.types)
if err != nil {
// TODO(adonovan): in light of exporter bugs such as #57729,
// consider using bug.Report here and retrying the IExportShallow
// call here using an empty types.Package.
log.Fatalf("internal error writing shallow export data: %v", err)
}
pkg.export = export
hashExport(m.PkgPath, export)
exportHasher.Sum(pkg.deepExportHash[:0])
return pkg
}
// analysisPackage contains information about a package, including
// syntax trees, used transiently during its type-checking and analysis.
type analysisPackage struct {
m *source.Metadata
fset *token.FileSet // local to this package
parsed []*source.ParsedGoFile
files []*ast.File // same as parsed[i].File
types *types.Package
compiles bool // package is transitively free of list/parse/type errors
factsDecoder *facts.Decoder
export []byte // encoding of types.Package
deepExportHash source.Hash // reflexive transitive hash of export data
typesInfo *types.Info
typeErrors []types.Error
typesSizes types.Sizes
}
// 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 {
once sync.Once
a *analysis.Analyzer
pkg *analysisPackage
hdeps []*action // horizontal dependencies
vdeps map[PackageID]*analyzeSummary // vertical dependencies
// results of action.exec():
result interface{} // result of Run function, of type a.ResultType
summary *actionSummary
err error
}
func (act *action) String() string {
return fmt.Sprintf("%s@%s", act.a.Name, act.pkg.m.ID)
}
// execActions executes a set of action graph nodes in parallel.
func execActions(actions []*action) {
var wg sync.WaitGroup
for _, act := range actions {
act := act
wg.Add(1)
go func() {
defer wg.Done()
act.once.Do(func() {
execActions(act.hdeps) // analyze "horizontal" dependencies
act.result, act.summary, act.err = act.exec()
if act.err != nil {
act.summary = &actionSummary{Err: act.err.Error()}
// TODO(adonovan): suppress logging. But
// shouldn't the root error's causal chain
// include this information?
if false { // debugging
log.Printf("act.exec(%v) failed: %v", act, act.err)
}
}
})
}()
}
wg.Wait()
}
// exec defines the execution of a single action.
// It returns the (ephemeral) result of the analyzer's Run function,
// along with its (serializable) facts and diagnostics.
// Or it returns an error if the analyzer did not run to
// completion and deliver a valid result.
func (act *action) exec() (interface{}, *actionSummary, error) {
analyzer := act.a
pkg := act.pkg
hasFacts := len(analyzer.FactTypes) > 0
// Report an error if any action dependency (vertical or horizontal) failed.
// To avoid long error messages describing chains of failure,
// we return the dependencies' error' unadorned.
if hasFacts {
// TODO(adonovan): use deterministic order.
for _, res := range act.vdeps {
if vdep := res.Actions[analyzer.Name]; vdep.Err != "" {
return nil, nil, errors.New(vdep.Err)
}
}
}
for _, dep := range act.hdeps {
if dep.err != nil {
return nil, nil, dep.err
}
}
// Inv: all action dependencies succeeded.
// Were there list/parse/type errors that might prevent analysis?
if !pkg.compiles && !analyzer.RunDespiteErrors {
return nil, nil, fmt.Errorf("skipping analysis %q because package %q does not compile", analyzer.Name, pkg.m.ID)
}
// Inv: package is well-formed enough to proceed with analysis.
if false { // debugging
log.Println("action.exec", act)
}
// Gather analysis Result values from horizontal dependencies.
var inputs = make(map[*analysis.Analyzer]interface{})
for _, dep := range act.hdeps {
inputs[dep.a] = dep.result
}
// TODO(adonovan): opt: facts.Set works but it may be more
// efficient to fork and tailor it to our precise needs.
//
// We've already sharded the fact encoding by action
// so that it can be done in parallel (hoisting the
// ImportMap call so that we build the map once per package).
// We could eliminate locking.
// We could also dovetail more closely with the export data
// decoder to obtain a more compact representation of
// packages and objects (e.g. its internal IDs, instead
// of PkgPaths and objectpaths.)
// Read and decode analysis facts for each imported package.
factset, err := pkg.factsDecoder.Decode(func(imp *types.Package) ([]byte, error) {
if !hasFacts {
return nil, nil // analyzer doesn't use facts, so no vdeps
}
// Package.Imports() may contain a fake "C" package. Ignore it.
if imp.Path() == "C" {
return nil, nil
}
id, ok := pkg.m.DepsByPkgPath[PackagePath(imp.Path())]
if !ok {
// This may mean imp was synthesized by the type
// checker because it failed to import it for any reason
// (e.g. bug processing export data; metadata ignoring
// a cycle-forming import).
// In that case, the fake package's imp.Path
// is set to the failed importPath (and thus
// it may lack a "vendor/" prefix).
//
// For now, silently ignore it on the assumption
// that the error is already reported elsewhere.
// return nil, fmt.Errorf("missing metadata")
return nil, nil
}
vdep, ok := act.vdeps[id]
if !ok {
return nil, bug.Errorf("internal error in %s: missing vdep for id=%s", pkg.types.Path(), id)
}
return vdep.Actions[analyzer.Name].Facts, nil
})
if err != nil {
return nil, nil, fmt.Errorf("internal error decoding analysis facts: %w", err)
}
// TODO(adonovan): make Export*Fact panic rather than discarding
// undeclared fact types, so that we discover bugs in analyzers.
factFilter := make(map[reflect.Type]bool)
for _, f := range analyzer.FactTypes {
factFilter[reflect.TypeOf(f)] = true
}
// posToLocation converts from token.Pos to protocol form.
// TODO(adonovan): improve error messages.
posToLocation := func(start, end token.Pos) (protocol.Location, error) {
tokFile := pkg.fset.File(start)
for _, p := range pkg.parsed {
if p.Tok == tokFile {
if end == token.NoPos {
end = start
}
return p.PosLocation(start, end)
}
}
return protocol.Location{},
bug.Errorf("internal error: token.Pos not within package")
}
// Now run the (pkg, analyzer) action.
var diagnostics []gobDiagnostic
pass := &analysis.Pass{
Analyzer: analyzer,
Fset: pkg.fset,
Files: pkg.files,
Pkg: pkg.types,
TypesInfo: pkg.typesInfo,
TypesSizes: pkg.typesSizes,
TypeErrors: pkg.typeErrors,
ResultOf: inputs,
Report: func(d analysis.Diagnostic) {
// Prefix the diagnostic category with the analyzer's name.
if d.Category == "" {
d.Category = analyzer.Name
} else {
d.Category = analyzer.Name + "." + d.Category
}
diagnostic, err := toGobDiagnostic(posToLocation, d)
if err != nil {
bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err)
return
}
diagnostics = append(diagnostics, diagnostic)
},
ImportObjectFact: factset.ImportObjectFact,
ExportObjectFact: factset.ExportObjectFact,
ImportPackageFact: factset.ImportPackageFact,
ExportPackageFact: factset.ExportPackageFact,
AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) },
AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) },
}
// Recover from panics (only) within the analyzer logic.
// (Use an anonymous function to limit the recover scope.)
var result interface{}
func() {
defer func() {
if r := recover(); r != nil {
// An Analyzer panicked, likely due to a bug.
//
// In general we want to discover and fix such panics quickly,
// so we don't suppress them, but some bugs in third-party
// analyzers cannot be quickly fixed, so we use an allowlist
// to suppress panics.
const strict = true
if strict && bug.PanicOnBugs &&
analyzer.Name != "buildir" { // see https://github.com/dominikh/go-tools/issues/1343
// Uncomment this when debugging suspected failures
// in the driver, not the analyzer.
if false {
debug.SetTraceback("all") // show all goroutines
}
panic(r)
} else {
// In production, suppress the panic and press on.
err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pass.Pkg.Path(), r)
}
}
}()
result, err = pass.Analyzer.Run(pass)
}()
if err != nil {
return nil, nil, err
}
if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want {
return nil, nil, bug.Errorf(
"internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v",
pass.Pkg.Path(), pass.Analyzer, got, want)
}
// Disallow Export*Fact calls after Run.
// (A panic means the Analyzer is abusing concurrency.)
pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) {
panic(fmt.Sprintf("%v: Pass.ExportObjectFact(%s, %T) called after Run", act, obj, fact))
}
pass.ExportPackageFact = func(fact analysis.Fact) {
panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact))
}
factsdata := factset.Encode()
return result, &actionSummary{
Diagnostics: diagnostics,
Facts: factsdata,
FactsHash: source.HashOf(factsdata),
}, nil
}
// requiredAnalyzers returns the transitive closure of required analyzers in preorder.
func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer {
var result []*analysis.Analyzer
seen := make(map[*analysis.Analyzer]bool)
var visitAll func([]*analysis.Analyzer)
visitAll = func(analyzers []*analysis.Analyzer) {
for _, a := range analyzers {
if !seen[a] {
seen[a] = true
result = append(result, a)
visitAll(a.Requires)
}
}
}
visitAll(analyzers)
return result
}
func mustEncode(x interface{}) []byte {
var buf bytes.Buffer
if err := gob.NewEncoder(&buf).Encode(x); err != nil {
log.Fatalf("internal error encoding %T: %v", x, err)
}
return buf.Bytes()
}
func mustDecode(data []byte, ptr interface{}) {
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(ptr); err != nil {
log.Fatalf("internal error decoding %T: %v", ptr, err)
}
}
// -- data types for serialization of analysis.Diagnostic --
type gobDiagnostic struct {
Location protocol.Location
Category string
Message string
SuggestedFixes []gobSuggestedFix
Related []gobRelatedInformation
}
type gobRelatedInformation struct {
Location protocol.Location
Message string
}
type gobSuggestedFix struct {
Message string
TextEdits []gobTextEdit
}
type gobTextEdit struct {
Location protocol.Location
NewText []byte
}
// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic,
// which requires expanding token.Pos positions into protocol.Location form.
func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), diag analysis.Diagnostic) (gobDiagnostic, error) {
var fixes []gobSuggestedFix
for _, fix := range diag.SuggestedFixes {
var gobEdits []gobTextEdit
for _, textEdit := range fix.TextEdits {
loc, err := posToLocation(textEdit.Pos, textEdit.End)
if err != nil {
return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err)
}
gobEdits = append(gobEdits, gobTextEdit{
Location: loc,
NewText: textEdit.NewText,
})
}
fixes = append(fixes, gobSuggestedFix{
Message: fix.Message,
TextEdits: gobEdits,
})
}
var related []gobRelatedInformation
for _, r := range diag.Related {
loc, err := posToLocation(r.Pos, r.End)
if err != nil {
return gobDiagnostic{}, fmt.Errorf("in Related: %w", err)
}
related = append(related, gobRelatedInformation{
Location: loc,
Message: r.Message,
})
}
loc, err := posToLocation(diag.Pos, diag.End)
if err != nil {
return gobDiagnostic{}, err
}
return gobDiagnostic{
Location: loc,
Category: diag.Category,
Message: diag.Message,
Related: related,
SuggestedFixes: fixes,
}, nil
}