| // 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 |
| |
| import ( |
| "context" |
| "crypto/sha256" |
| "fmt" |
| "go/ast" |
| "go/build" |
| "go/parser" |
| "go/token" |
| "go/types" |
| "regexp" |
| "runtime" |
| "slices" |
| "sort" |
| "strings" |
| "sync" |
| "sync/atomic" |
| |
| "golang.org/x/mod/module" |
| "golang.org/x/sync/errgroup" |
| "golang.org/x/tools/go/ast/astutil" |
| "golang.org/x/tools/gopls/internal/bloom" |
| "golang.org/x/tools/gopls/internal/cache/metadata" |
| "golang.org/x/tools/gopls/internal/cache/parsego" |
| "golang.org/x/tools/gopls/internal/cache/typerefs" |
| "golang.org/x/tools/gopls/internal/file" |
| "golang.org/x/tools/gopls/internal/filecache" |
| "golang.org/x/tools/gopls/internal/label" |
| "golang.org/x/tools/gopls/internal/protocol" |
| "golang.org/x/tools/gopls/internal/util/bug" |
| "golang.org/x/tools/gopls/internal/util/moremaps" |
| "golang.org/x/tools/gopls/internal/util/safetoken" |
| "golang.org/x/tools/gopls/internal/util/tokeninternal" |
| "golang.org/x/tools/internal/analysisinternal" |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/gcimporter" |
| "golang.org/x/tools/internal/packagesinternal" |
| "golang.org/x/tools/internal/typesinternal" |
| "golang.org/x/tools/internal/versions" |
| ) |
| |
| type unit = struct{} |
| |
| // A typeCheckBatch holds data for a logical type-checking operation, which may |
| // type check many unrelated packages. |
| // |
| // It shares state such as parsed files and imports, to optimize type-checking |
| // for packages with overlapping dependency graphs. |
| type typeCheckBatch struct { |
| // handleMu guards _handles, which must only be accessed via addHandles or |
| // getHandle. |
| // |
| // An alternative would be to simply verify that package handles are present |
| // on the Snapshot, and access them directly, rather than copying maps for |
| // each caller. However, handles are accessed very frequently during type |
| // checking, and ordinary go maps are measurably faster than the |
| // persistent.Map used to store handles on the snapshot. |
| handleMu sync.Mutex |
| _handles map[PackageID]*packageHandle |
| |
| parseCache *parseCache |
| fset *token.FileSet // describes all parsed or imported files |
| cpulimit chan unit // concurrency limiter for CPU-bound operations |
| syntaxPackages *futureCache[PackageID, *Package] // transient cache of in-progress syntax futures |
| importPackages *futureCache[PackageID, *types.Package] // persistent cache of imports |
| gopackagesdriver bool // for bug reporting: were packages loaded with a driver? |
| } |
| |
| // addHandles is called by each goroutine joining the type check batch, to |
| // ensure that the batch has all inputs necessary for type checking. |
| func (b *typeCheckBatch) addHandles(handles map[PackageID]*packageHandle) { |
| b.handleMu.Lock() |
| defer b.handleMu.Unlock() |
| for id, ph := range handles { |
| assert(ph.state >= validKey, "invalid handle") |
| |
| if alt, ok := b._handles[id]; !ok || alt.state < ph.state { |
| b._handles[id] = ph |
| } |
| } |
| } |
| |
| // getHandle retrieves the packageHandle for the given id. |
| func (b *typeCheckBatch) getHandle(id PackageID) *packageHandle { |
| b.handleMu.Lock() |
| defer b.handleMu.Unlock() |
| return b._handles[id] |
| } |
| |
| // TypeCheck parses and type-checks the specified packages, |
| // and returns them in the same order as the ids. |
| // The resulting packages' types may belong to different importers, |
| // so types from different packages are incommensurable. |
| // |
| // The resulting packages slice always contains len(ids) entries, though some |
| // of them may be nil if (and only if) the resulting error is non-nil. |
| // |
| // An error is returned if any of the requested packages fail to type-check. |
| // This is different from having type-checking errors: a failure to type-check |
| // indicates context cancellation or otherwise significant failure to perform |
| // the type-checking operation. |
| // |
| // In general, clients should never need to type-checked syntax for an |
| // intermediate test variant (ITV) package. Callers should apply |
| // RemoveIntermediateTestVariants (or equivalent) before this method, or any |
| // of the potentially type-checking methods below. |
| func (s *Snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]*Package, error) { |
| pkgs := make([]*Package, len(ids)) |
| post := func(i int, pkg *Package) { |
| pkgs[i] = pkg |
| } |
| return pkgs, s.forEachPackage(ctx, ids, nil, post) |
| } |
| |
| // Package visiting functions used by forEachPackage; see the documentation of |
| // forEachPackage for details. |
| type ( |
| preTypeCheck = func(int, *packageHandle) bool // false => don't type check |
| postTypeCheck = func(int, *Package) |
| ) |
| |
| // forEachPackage does a pre- and post- order traversal of the packages |
| // specified by ids using the provided pre and post functions. |
| // |
| // The pre func is optional. If set, pre is evaluated after the package |
| // handle has been constructed, but before type-checking. If pre returns false, |
| // type-checking is skipped for this package handle. |
| // |
| // post is called with a syntax package after type-checking completes |
| // successfully. It is only called if pre returned true. |
| // |
| // Both pre and post may be called concurrently. |
| func (s *Snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preTypeCheck, post postTypeCheck) error { |
| ctx, done := event.Start(ctx, "cache.forEachPackage", label.PackageCount.Of(len(ids))) |
| defer done() |
| |
| var ( |
| needIDs []PackageID // ids to type-check |
| indexes []int // original index of requested ids |
| ) |
| |
| // Check for existing active packages. |
| // |
| // Since gopls can't depend on package identity, any instance of the |
| // requested package must be ok to return. |
| // |
| // This is an optimization to avoid redundant type-checking: following |
| // changes to an open package many LSP clients send several successive |
| // requests for package information for the modified package (semantic |
| // tokens, code lens, inlay hints, etc.) |
| for i, id := range ids { |
| s.mu.Lock() |
| ph, ok := s.packages.Get(id) |
| s.mu.Unlock() |
| if ok && ph.state >= validPackage { |
| post(i, ph.pkgData.pkg) |
| } else { |
| needIDs = append(needIDs, id) |
| indexes = append(indexes, i) |
| } |
| } |
| |
| if len(needIDs) == 0 { |
| return nil // short cut: many call sites do not handle empty ids |
| } |
| |
| b, release := s.acquireTypeChecking() |
| defer release() |
| |
| handles, err := s.getPackageHandles(ctx, needIDs) |
| if err != nil { |
| return err |
| } |
| |
| // Wrap the pre- and post- funcs to translate indices. |
| var pre2 preTypeCheck |
| if pre != nil { |
| pre2 = func(i int, ph *packageHandle) bool { |
| return pre(indexes[i], ph) |
| } |
| } |
| post2 := func(i int, pkg *Package) { |
| id := pkg.metadata.ID |
| if ph := handles[id]; ph.isOpen && ph.state < validPackage { |
| // Cache open type checked packages. |
| ph = ph.clone() |
| ph.pkgData = &packageData{ |
| fset: pkg.FileSet(), |
| imports: pkg.Types().Imports(), |
| pkg: pkg, |
| } |
| ph.state = validPackage |
| |
| s.mu.Lock() |
| if alt, ok := s.packages.Get(id); !ok || alt.state < ph.state { |
| s.packages.Set(id, ph, nil) |
| } |
| s.mu.Unlock() |
| } |
| |
| post(indexes[i], pkg) |
| } |
| |
| return b.query(ctx, needIDs, pre2, post2, handles) |
| } |
| |
| // acquireTypeChecking joins or starts a concurrent type checking batch. |
| // |
| // The batch may be queried for package information using [typeCheckBatch.query]. |
| // The second result must be called when the batch is no longer needed, to |
| // release the resource. |
| func (s *Snapshot) acquireTypeChecking() (*typeCheckBatch, func()) { |
| s.typeCheckMu.Lock() |
| defer s.typeCheckMu.Unlock() |
| |
| if s.batch == nil { |
| assert(s.batchRef == 0, "miscounted type checking") |
| s.batch = newTypeCheckBatch(s.view.parseCache, s.view.typ == GoPackagesDriverView) |
| } |
| s.batchRef++ |
| |
| return s.batch, func() { |
| s.typeCheckMu.Lock() |
| defer s.typeCheckMu.Unlock() |
| assert(s.batchRef > 0, "miscounted type checking 2") |
| s.batchRef-- |
| if s.batchRef == 0 { |
| s.batch = nil |
| } |
| } |
| } |
| |
| // newTypeCheckBatch creates a new type checking batch using the provided |
| // shared parseCache. |
| // |
| // If a non-nil importGraph is provided, imports in this graph will be reused. |
| func newTypeCheckBatch(parseCache *parseCache, gopackagesdriver bool) *typeCheckBatch { |
| return &typeCheckBatch{ |
| _handles: make(map[PackageID]*packageHandle), |
| parseCache: parseCache, |
| fset: fileSetWithBase(reservedForParsing), |
| cpulimit: make(chan unit, runtime.GOMAXPROCS(0)), |
| syntaxPackages: newFutureCache[PackageID, *Package](false), // don't persist syntax packages |
| importPackages: newFutureCache[PackageID, *types.Package](true), // ...but DO persist imports |
| gopackagesdriver: gopackagesdriver, |
| } |
| } |
| |
| // query executes a traversal of package information in the given typeCheckBatch. |
| // For each package in importIDs, the package will be loaded "for import" (sans |
| // syntax). |
| // |
| // For each package in syntaxIDs, the package will be handled following the |
| // pre- and post- traversal logic of [Snapshot.forEachPackage]. |
| // |
| // Package handles must be provided for each package in the forward transitive |
| // closure of either importIDs or syntaxIDs. |
| // |
| // TODO(rfindley): simplify this API by clarifying shared import graph and |
| // package handle logic. |
| func (b *typeCheckBatch) query(ctx context.Context, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) error { |
| b.addHandles(handles) |
| |
| // Start a single goroutine for each requested package. |
| // |
| // Other packages are reached recursively, and will not be evaluated if they |
| // are not needed. |
| var g errgroup.Group |
| for i, id := range syntaxIDs { |
| g.Go(func() error { |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| return b.handleSyntaxPackage(ctx, i, id, pre, post) |
| }) |
| } |
| return g.Wait() |
| } |
| |
| // TODO(rfindley): re-order the declarations below to read better from top-to-bottom. |
| |
| // getImportPackage returns the *types.Package to use for importing the |
| // package referenced by id. |
| // |
| // This may be the package produced by type-checking syntax (as in the case |
| // where id is in the set of requested IDs), a package loaded from export data, |
| // or a package type-checked for import only. |
| func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pkg *types.Package, err error) { |
| return b.importPackages.get(ctx, id, func(ctx context.Context) (*types.Package, error) { |
| ph := b.getHandle(id) |
| |
| // "unsafe" cannot be imported or type-checked. |
| // |
| // We check PkgPath, not id, as the structure of the ID |
| // depends on the build system (in particular, |
| // Bazel+gopackagesdriver appears to use something other than |
| // "unsafe", though we aren't sure what; even 'go list' can |
| // use "p [q.test]" for testing or if PGO is enabled. |
| // See golang/go#60890. |
| if ph.mp.PkgPath == "unsafe" { |
| return types.Unsafe, nil |
| } |
| |
| data, err := filecache.Get(exportDataKind, ph.key) |
| if err == filecache.ErrNotFound { |
| // No cached export data: type-check as fast as possible. |
| return b.checkPackageForImport(ctx, ph) |
| } |
| if err != nil { |
| return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) |
| } |
| return b.importPackage(ctx, ph.mp, data) |
| }) |
| } |
| |
| // handleSyntaxPackage handles one package from the ids slice. |
| // |
| // If type checking occurred while handling the package, it returns the |
| // resulting types.Package so that it may be used for importing. |
| // |
| // handleSyntaxPackage returns (nil, nil) if pre returned false. |
| func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID, pre preTypeCheck, post postTypeCheck) error { |
| ph := b.getHandle(id) |
| if pre != nil && !pre(i, ph) { |
| return nil // skip: not needed |
| } |
| |
| // Check if we have a syntax package stored on ph. |
| // |
| // This was checked in [Snapshot.forEachPackage], but may have since changed. |
| if ph.state >= validPackage { |
| post(i, ph.pkgData.pkg) |
| return nil |
| } |
| |
| pkg, err := b.getPackage(ctx, ph) |
| if err != nil { |
| return err |
| } |
| |
| post(i, pkg) |
| return nil |
| } |
| |
| // getPackage type checks one [Package] in the batch. |
| func (b *typeCheckBatch) getPackage(ctx context.Context, ph *packageHandle) (*Package, error) { |
| return b.syntaxPackages.get(ctx, ph.mp.ID, func(ctx context.Context) (*Package, error) { |
| // Wait for predecessors. |
| // Record imports of this package to avoid redundant work in typesConfig. |
| imports := make(map[PackagePath]*types.Package) |
| fset := b.fset |
| if ph.state >= validImports { |
| for _, imp := range ph.pkgData.imports { |
| imports[PackagePath(imp.Path())] = imp |
| } |
| // Reusing imports requires that their positions are mapped by the FileSet. |
| fset = tokeninternal.CloneFileSet(ph.pkgData.fset) |
| } else { |
| var impMu sync.Mutex |
| var g errgroup.Group |
| for depPath, depID := range ph.mp.DepsByPkgPath { |
| g.Go(func() error { |
| imp, err := b.getImportPackage(ctx, depID) |
| if err == nil { |
| impMu.Lock() |
| imports[depPath] = imp |
| impMu.Unlock() |
| } |
| return err |
| }) |
| } |
| if err := g.Wait(); err != nil { |
| // Failure to import a package should not abort the whole operation. |
| // Stop only if the context was cancelled, a likely cause. |
| // Import errors will be reported as type diagnostics. |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| } |
| } |
| |
| // Wait to acquire a CPU token. |
| // |
| // Note: it is important to acquire this token only after awaiting |
| // predecessors, to avoid starvation. |
| select { |
| case <-ctx.Done(): |
| return nil, ctx.Err() |
| case b.cpulimit <- unit{}: |
| defer func() { |
| <-b.cpulimit // release CPU token |
| }() |
| } |
| |
| // Compute the syntax package. |
| p, err := b.checkPackage(ctx, fset, ph, imports) |
| if err != nil { |
| return nil, err // e.g. I/O error, cancelled |
| } |
| |
| // Update caches. |
| go storePackageResults(ctx, ph, p) // ...and write all packages to disk |
| return p, nil |
| }) |
| } |
| |
| // storePackageResults serializes and writes information derived from p to the |
| // file cache. |
| // The context is used only for logging; cancellation does not affect the operation. |
| func storePackageResults(ctx context.Context, ph *packageHandle, p *Package) { |
| toCache := map[string][]byte{ |
| xrefsKind: p.pkg.xrefs(), |
| methodSetsKind: p.pkg.methodsets().Encode(), |
| testsKind: p.pkg.tests().Encode(), |
| diagnosticsKind: encodeDiagnostics(p.pkg.diagnostics), |
| } |
| |
| if p.metadata.PkgPath != "unsafe" { // unsafe cannot be exported |
| exportData, err := gcimporter.IExportShallow(p.pkg.fset, p.pkg.types, bug.Reportf) |
| if err != nil { |
| bug.Reportf("exporting package %v: %v", p.metadata.ID, err) |
| } else { |
| toCache[exportDataKind] = exportData |
| } |
| } |
| |
| for kind, data := range toCache { |
| if err := filecache.Set(kind, ph.key, data); err != nil { |
| event.Error(ctx, fmt.Sprintf("storing %s data for %s", kind, ph.mp.ID), err) |
| } |
| } |
| } |
| |
| // Metadata implements the [metadata.Source] interface. |
| func (b *typeCheckBatch) Metadata(id PackageID) *metadata.Package { |
| ph := b.getHandle(id) |
| if ph == nil { |
| return nil |
| } |
| return ph.mp |
| } |
| |
| // importPackage loads the given package from its export data in p.exportData |
| // (which must already be populated). |
| func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package, data []byte) (*types.Package, error) { |
| ctx, done := event.Start(ctx, "cache.typeCheckBatch.importPackage", label.Package.Of(string(mp.ID))) |
| defer done() |
| |
| importLookup := importLookup(mp, b) |
| |
| thisPackage := types.NewPackage(string(mp.PkgPath), string(mp.Name)) |
| getPackages := func(items []gcimporter.GetPackagesItem) error { |
| for i, item := range items { |
| var id PackageID |
| var pkg *types.Package |
| if item.Path == string(mp.PkgPath) { |
| id = mp.ID |
| pkg = thisPackage |
| |
| // debugging issues #60904, #64235 |
| if pkg.Name() != item.Name { |
| // This would mean that mp.Name != item.Name, so the |
| // manifest in the export data of mp.PkgPath is |
| // inconsistent with mp.Name. Or perhaps there |
| // are duplicate PkgPath items in the manifest? |
| if b.gopackagesdriver { |
| return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904) (using GOPACKAGESDRIVER)", |
| pkg.Name(), item.Name, id, item.Path) |
| } else { |
| // There's a package in the export data with the same path as the |
| // imported package, but a different name. |
| // |
| // This is observed to occur (very frequently!) in telemetry, yet |
| // we don't yet have a plausible explanation: any self import or |
| // circular import should have resulted in a broken import, which |
| // can't be referenced by export data. (Any type qualified by the |
| // broken import name will be invalid.) |
| // |
| // However, there are some mechanisms that could potentially be |
| // involved: |
| // 1. go/types will synthesize package names based on the import |
| // path for fake packages (but as mentioned above, I don't think |
| // these can be referenced by export data.) |
| // 2. Test variants have the same path as non-test variant. Could |
| // that somehow be involved? (I don't see how, particularly using |
| // the go list driver, but nevertheless it's worth considering.) |
| // 3. Command-line arguments and main packages may have special |
| // handling that we don't fully understand. |
| // Try to sort these potential causes into unique stacks, as well |
| // as a few other pathological scenarios. |
| report := func() error { |
| return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", |
| pkg.Name(), item.Name, id, item.Path) |
| } |
| impliedName := "" |
| if i := strings.LastIndex(item.Path, "/"); i >= 0 { |
| impliedName = item.Path[i+1:] |
| } |
| switch { |
| case pkg.Name() == "": |
| return report() |
| case item.Name == "": |
| return report() |
| case metadata.IsCommandLineArguments(mp.ID): |
| return report() |
| case mp.ForTest != "": |
| return report() |
| case len(mp.CompiledGoFiles) == 0: |
| return report() |
| case len(mp.Errors) > 0: |
| return report() |
| case impliedName != "" && impliedName != string(mp.Name): |
| return report() |
| case len(mp.CompiledGoFiles) != len(mp.GoFiles): |
| return report() |
| case mp.Module == nil: |
| return report() |
| case mp.Name == "main": |
| return report() |
| default: |
| return report() |
| } |
| } |
| } |
| } else { |
| var alt PackageID |
| id, alt = importLookup(PackagePath(item.Path)) |
| if alt != "" { |
| // Any bug leading to this scenario would have already been reported |
| // in importLookup. |
| return fmt.Errorf("inconsistent metadata during import: for package path %q, found both IDs %q and %q", item.Path, id, alt) |
| } |
| var err error |
| pkg, err = b.getImportPackage(ctx, id) |
| if err != nil { |
| return err |
| } |
| |
| // We intentionally duplicate the bug.Errorf calls because |
| // telemetry tells us only the program counter, not the message. |
| |
| // debugging issues #60904, #64235 |
| if pkg.Name() != item.Name { |
| // This means that, while reading the manifest of the |
| // export data of mp.PkgPath, one of its indirect |
| // dependencies had a name that differs from the |
| // Metadata.Name |
| return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", |
| pkg.Name(), item.Name, id, item.Path) |
| } |
| } |
| items[i].Pkg = pkg |
| |
| } |
| return nil |
| } |
| |
| // Importing is potentially expensive, and might not encounter cancellations |
| // via dependencies (e.g. if they have already been evaluated). |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| |
| imported, err := gcimporter.IImportShallow(b.fset, getPackages, data, string(mp.PkgPath), bug.Reportf) |
| if err != nil { |
| return nil, fmt.Errorf("import failed for %q: %v", mp.ID, err) |
| } |
| return imported, nil |
| } |
| |
| // checkPackageForImport type checks, but skips function bodies and does not |
| // record syntax information. |
| func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageHandle) (*types.Package, error) { |
| ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackageForImport", label.Package.Of(string(ph.mp.ID))) |
| defer done() |
| |
| onError := func(e error) { |
| // Ignore errors for exporting. |
| } |
| cfg := b.typesConfig(ctx, ph.localInputs, nil, onError) |
| cfg.IgnoreFuncBodies = true |
| |
| // Parse the compiled go files, bypassing the parse cache as packages checked |
| // for import are unlikely to get cache hits. Additionally, we can optimize |
| // parsing slightly by not passing parser.ParseComments. |
| pgfs := make([]*parsego.File, len(ph.localInputs.compiledGoFiles)) |
| { |
| var group errgroup.Group |
| // Set an arbitrary concurrency limit; we want some parallelism but don't |
| // need GOMAXPROCS, as there is already a lot of concurrency among calls to |
| // checkPackageForImport. |
| // |
| // TODO(rfindley): is there a better way to limit parallelism here? We could |
| // have a global limit on the type-check batch, but would have to be very |
| // careful to avoid starvation. |
| group.SetLimit(4) |
| for i, fh := range ph.localInputs.compiledGoFiles { |
| i, fh := i, fh |
| group.Go(func() error { |
| pgf, err := parseGoImpl(ctx, b.fset, fh, parser.SkipObjectResolution, false) |
| pgfs[i] = pgf |
| return err |
| }) |
| } |
| if err := group.Wait(); err != nil { |
| return nil, err // cancelled, or catastrophic error (e.g. missing file) |
| } |
| } |
| pkg := types.NewPackage(string(ph.localInputs.pkgPath), string(ph.localInputs.name)) |
| check := types.NewChecker(cfg, b.fset, pkg, nil) |
| |
| files := make([]*ast.File, len(pgfs)) |
| for i, pgf := range pgfs { |
| files[i] = pgf.File |
| } |
| |
| // Type checking is expensive, and we may not have encountered cancellations |
| // via parsing (e.g. if we got nothing but cache hits for parsed files). |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| |
| _ = check.Files(files) // ignore errors |
| |
| // If the context was cancelled, we may have returned a ton of transient |
| // errors to the type checker. Swallow them. |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| |
| // Asynchronously record export data. |
| go func() { |
| exportData, err := gcimporter.IExportShallow(b.fset, pkg, bug.Reportf) |
| if err != nil { |
| // Internal error; the stack will have been reported via |
| // bug.Reportf within IExportShallow, so there's not much |
| // to do here (issue #71067). |
| event.Error(ctx, "IExportShallow failed", err, label.Package.Of(string(ph.mp.ID))) |
| return |
| } |
| if err := filecache.Set(exportDataKind, ph.key, exportData); err != nil { |
| event.Error(ctx, fmt.Sprintf("storing export data for %s", ph.mp.ID), err) |
| } |
| }() |
| return pkg, nil |
| } |
| |
| // importLookup returns a function that may be used to look up a package ID for |
| // a given package path, based on the forward transitive closure of the initial |
| // package (id). |
| // |
| // If the second result is non-empty, it is another ID discovered in the import |
| // graph for the same package path. This means the import graph is |
| // incoherent--see #63822 and the long comment below. |
| // |
| // The resulting function is not concurrency safe. |
| func importLookup(mp *metadata.Package, source metadata.Source) func(PackagePath) (id, altID PackageID) { |
| assert(mp != nil, "nil metadata") |
| |
| // This function implements an incremental depth first scan through the |
| // package imports. Previous implementations of import mapping built the |
| // entire PackagePath->PackageID mapping eagerly, but that resulted in a |
| // large amount of unnecessary work: most imports are either directly |
| // imported, or found through a shallow scan. |
| |
| // impMap memoizes the lookup of package paths. |
| impMap := map[PackagePath]PackageID{ |
| mp.PkgPath: mp.ID, |
| } |
| |
| // altIDs records alternative IDs for the given path, to report inconsistent |
| // metadata. |
| var altIDs map[PackagePath]PackageID |
| |
| // pending is a FIFO queue of package metadata that has yet to have its |
| // dependencies fully scanned. |
| // Invariant: all entries in pending are already mapped in impMap. |
| pending := []*metadata.Package{mp} |
| |
| // search scans children the next package in pending, looking for pkgPath. |
| // Invariant: whenever search is called, pkgPath is not yet mapped. |
| search := func(pkgPath PackagePath) (id PackageID, found bool) { |
| pkg := pending[0] |
| pending = pending[1:] |
| for depPath, depID := range pkg.DepsByPkgPath { |
| if prevID, ok := impMap[depPath]; ok { |
| // debugging #63822 |
| if prevID != depID { |
| if altIDs == nil { |
| altIDs = make(map[PackagePath]PackageID) |
| } |
| if _, ok := altIDs[depPath]; !ok { |
| altIDs[depPath] = depID |
| } |
| prev := source.Metadata(prevID) |
| curr := source.Metadata(depID) |
| switch { |
| case prev == nil || curr == nil: |
| bug.Reportf("inconsistent view of dependencies (missing dep)") |
| case prev.ForTest != curr.ForTest: |
| // This case is unfortunately understood to be possible. |
| // |
| // To explain this, consider a package a_test testing the package |
| // a, and for brevity denote by b' the intermediate test variant of |
| // the package b, which is created for the import graph of a_test, |
| // if b imports a. |
| // |
| // Now imagine that we have the following import graph, where |
| // higher packages import lower ones. |
| // |
| // a_test |
| // / \ |
| // b' c |
| // / \ / |
| // a d |
| // |
| // In this graph, there is one intermediate test variant b', |
| // because b imports a and so b' must hold the test variant import. |
| // |
| // Now, imagine that an on-disk change (perhaps due to a branch |
| // switch) affects the above import graph such that d imports a. |
| // |
| // a_test |
| // / \ |
| // b' c* |
| // / \ / |
| // / d* |
| // a---/ |
| // |
| // In this case, c and d should really be intermediate test |
| // variants, because they reach a. However, suppose that gopls does |
| // not know this yet (as indicated by '*'). |
| // |
| // Now suppose that the metadata of package c is invalidated, for |
| // example due to a change in an unrelated import or an added file. |
| // This will invalidate the metadata of c and a_test (but NOT b), |
| // and now gopls observes this graph: |
| // |
| // a_test |
| // / \ |
| // b' c' |
| // /| | |
| // / d d' |
| // a-----/ |
| // |
| // That is: a_test now sees c', which sees d', but since b was not |
| // invalidated, gopls still thinks that b' imports d (not d')! |
| // |
| // The problem, of course, is that gopls never observed the change |
| // to d, which would have invalidated b. This may be due to racing |
| // file watching events, in which case the problem should |
| // self-correct when gopls sees the change to d, or it may be due |
| // to d being outside the coverage of gopls' file watching glob |
| // patterns, or it may be due to buggy or entirely absent |
| // client-side file watching. |
| // |
| // TODO(rfindley): fix this, one way or another. It would be hard |
| // or impossible to repair gopls' state here, during type checking. |
| // However, we could perhaps reload metadata in Snapshot.load until |
| // we achieve a consistent state, or better, until the loaded state |
| // is consistent with our view of the filesystem, by making the Go |
| // command report digests of the files it reads. Both of those are |
| // tricker than they may seem, and have significant performance |
| // implications. |
| default: |
| bug.Reportf("inconsistent view of dependencies") |
| } |
| } |
| continue |
| } |
| impMap[depPath] = depID |
| |
| dep := source.Metadata(depID) |
| assert(dep != nil, "missing dep metadata") |
| |
| pending = append(pending, dep) |
| if depPath == pkgPath { |
| // Don't return early; finish processing pkg's deps. |
| id = depID |
| found = true |
| } |
| } |
| return id, found |
| } |
| |
| return func(pkgPath PackagePath) (id, altID PackageID) { |
| if id, ok := impMap[pkgPath]; ok { |
| return id, altIDs[pkgPath] |
| } |
| for len(pending) > 0 { |
| if id, found := search(pkgPath); found { |
| return id, altIDs[pkgPath] |
| } |
| } |
| return "", "" |
| } |
| } |
| |
| // A packageState is the state of a [packageHandle]; see below for details. |
| type packageState uint8 |
| |
| const ( |
| validMetadata packageState = iota // the package has valid metadata |
| validLocalData // local package files have been analyzed |
| validKey // dependencies have been analyzed, and key produced |
| validImports // pkgData.fset and pkgData.imports are valid |
| validPackage // pkgData.pkg is valid |
| ) |
| |
| // A packageHandle holds information derived from a metadata.Package, and |
| // records its degree of validity as state changes occur: successful analysis |
| // causes the state to progress; invalidation due to changes causes it to |
| // regress. |
| // |
| // In the initial state (validMetadata), all we know is the metadata for the |
| // package itself. This is the lowest state, and it cannot become invalid |
| // because the metadata for a given snapshot never changes. (Each handle is |
| // implicitly associated with a Snapshot.) |
| // |
| // After the files of the package have been read (validLocalData), we can |
| // perform computations that are local to that package, such as parsing, or |
| // building the symbol reference graph (SRG). This information is invalidated |
| // by a change to any file in the package. The local information is thus |
| // sufficient to form a cache key for saved parsed trees or the SRG. |
| // |
| // Once all dependencies have been analyzed (validKey), we can type-check the |
| // package. This information is invalidated by any change to the package |
| // itself, or to any dependency that is transitively reachable through the SRG. |
| // The cache key for saved type information must thus incorporate information |
| // from all reachable dependencies. This reachability analysis implements what |
| // we sometimes refer to as "precise pruning", or fine-grained invalidation: |
| // https://go.dev/blog/gopls-scalability#invalidation |
| // |
| // After type checking, package information for open packages is cached in the |
| // pkgData field (validPackage), to optimize subsequent requests oriented |
| // around open files. |
| // |
| // Following a change, the packageHandle is cloned in the new snapshot with a |
| // new state set to its least known valid state, as described above: if package |
| // files changed, it is reset to validMetadata; if dependencies changed, it is |
| // reset to validLocalData. However, the derived data from its previous state |
| // is not yet removed, as keys may not have changed after they are reevaluated, |
| // in which case we can avoid recomputing the derived data. In particular, if |
| // the cache key did not change, the pkgData field (if set) remains valid. As a |
| // special case, if the cache key did change, but none of the keys of |
| // dependencies changed, the pkgData.fset and pkgData.imports fields are still |
| // valid, though the pkgData.pkg field is not (validImports). |
| // |
| // See [packageHandleBuilder.evaluatePackageHandle] for more details of the |
| // reevaluation algorithm. |
| // |
| // packageHandles are immutable once they are stored in the Snapshot.packages |
| // map: any changes to packageHandle fields evaluatePackageHandle must be made |
| // to a cloned packageHandle, and inserted back into Snapshot.packages. Data |
| // referred to by the packageHandle may be shared by multiple clones, and so |
| // referents must not be mutated. |
| type packageHandle struct { |
| mp *metadata.Package |
| |
| // state indicates which data below are still valid. |
| state packageState |
| |
| // Local data: |
| |
| // loadDiagnostics memoizes the result of processing error messages from |
| // go/packages (i.e. `go list`). |
| // |
| // These are derived from metadata using a snapshot. Since they depend on |
| // file contents (for translating positions), they should theoretically be |
| // invalidated by file changes, but historically haven't been. In practice |
| // they are rare and indicate a fundamental error that needs to be corrected |
| // before development can continue, so it may not be worth significant |
| // engineering effort to implement accurate invalidation here. |
| // |
| // TODO(rfindley): loadDiagnostics are out of place here, as they don't |
| // directly relate to type checking. We should perhaps move the caching of |
| // load diagnostics to an entirely separate component, so that Packages need |
| // only be concerned with parsing and type checking. |
| // (Nevertheless, since the lifetime of load diagnostics matches that of the |
| // Metadata, it is convenient to memoize them here.) |
| loadDiagnostics []*Diagnostic |
| // localInputs holds all local type-checking localInputs, excluding |
| // dependencies. |
| localInputs *typeCheckInputs |
| // isOpen reports whether the package has any open files. |
| isOpen bool |
| // localKey is a hash of localInputs. |
| localKey file.Hash |
| // refs is the result of syntactic dependency analysis produced by the |
| // typerefs package. Derived from localInputs. |
| refs map[string][]typerefs.Symbol |
| |
| // Keys, computed through reachability analysis of dependencies. |
| |
| // depKeys records the key of each dependency that was used to calculate the |
| // key below. If state < validKey, we must re-check that each still matches. |
| depKeys map[PackageID]file.Hash |
| |
| // reachable is used to filter reachable package paths for go/analysis fact |
| // importing. |
| reachable *bloom.Filter |
| |
| // key is the hashed key for the package. |
| // |
| // It includes the all bits of the transitive closure of |
| // dependencies's sources. |
| key file.Hash |
| |
| // pkgData caches data derived from type checking the package. |
| // This data is set during [Snapshot.forEachPackage], and may be partially |
| // invalidated in [packageHandleBuilder.evaluatePackageHandle]. |
| // |
| // If state == validPackage, all fields of pkgData are valid. If state == |
| // validImports, only fset and imports are valid. |
| pkgData *packageData |
| } |
| |
| // packageData holds the (possibly partial) result of type checking this |
| // package. See the pkgData field of [packageHandle]. |
| // |
| // packageData instances are immutable. |
| type packageData struct { |
| fset *token.FileSet // pkg.FileSet() |
| imports []*types.Package // pkg.Types().Imports() |
| pkg *Package // pkg, if state==validPackage; nil in lower states |
| } |
| |
| // clone returns a shallow copy of the receiver. |
| func (ph *packageHandle) clone() *packageHandle { |
| clone := *ph |
| return &clone |
| } |
| |
| // getPackageHandles gets package handles for all given ids and their |
| // dependencies, recursively. The resulting [packageHandle] values are fully |
| // evaluated (their state will be at least validKey). |
| func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) { |
| // perform a two-pass traversal. |
| // |
| // On the first pass, build up a bidirectional graph of handle nodes, and collect leaves. |
| // Then build package handles from bottom up. |
| b := &packageHandleBuilder{ |
| s: s, |
| transitiveRefs: make(map[typerefs.IndexID]*partialRefs), |
| nodes: make(map[typerefs.IndexID]*handleNode), |
| } |
| |
| meta := s.MetadataGraph() |
| |
| var leaves []*handleNode |
| var makeNode func(*handleNode, PackageID) *handleNode |
| makeNode = func(from *handleNode, id PackageID) *handleNode { |
| idxID := s.view.pkgIndex.IndexID(id) |
| n, ok := b.nodes[idxID] |
| if !ok { |
| mp := meta.Packages[id] |
| if mp == nil { |
| panic(fmt.Sprintf("nil metadata for %q", id)) |
| } |
| n = &handleNode{ |
| mp: mp, |
| idxID: idxID, |
| unfinishedSuccs: int32(len(mp.DepsByPkgPath)), |
| } |
| if n.unfinishedSuccs == 0 { |
| leaves = append(leaves, n) |
| } else { |
| n.succs = make(map[PackageID]*handleNode, n.unfinishedSuccs) |
| } |
| b.nodes[idxID] = n |
| for _, depID := range mp.DepsByPkgPath { |
| n.succs[depID] = makeNode(n, depID) |
| } |
| } |
| // Add edge from predecessor. |
| if from != nil { |
| n.preds = append(n.preds, from) |
| } |
| return n |
| } |
| for _, id := range ids { |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| makeNode(nil, id) |
| } |
| |
| g, ctx := errgroup.WithContext(ctx) |
| |
| // files are preloaded, so building package handles is CPU-bound. |
| // |
| // Note that we can't use g.SetLimit, as that could result in starvation: |
| // g.Go blocks until a slot is available, and so all existing goroutines |
| // could be blocked trying to enqueue a predecessor. |
| limiter := make(chan unit, runtime.GOMAXPROCS(0)) |
| |
| var enqueue func(*handleNode) |
| enqueue = func(n *handleNode) { |
| g.Go(func() error { |
| limiter <- unit{} |
| defer func() { <-limiter }() |
| |
| if ctx.Err() != nil { |
| return ctx.Err() |
| } |
| |
| if err := b.evaluatePackageHandle(ctx, n); err != nil { |
| return err |
| } |
| |
| for _, pred := range n.preds { |
| if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { |
| enqueue(pred) |
| } |
| } |
| return nil |
| }) |
| } |
| for _, leaf := range leaves { |
| enqueue(leaf) |
| } |
| |
| if err := g.Wait(); err != nil { |
| return nil, err |
| } |
| |
| // Copy handles into the result map. |
| handles := make(map[PackageID]*packageHandle, len(b.nodes)) |
| for _, v := range b.nodes { |
| assert(v.ph != nil, "nil handle") |
| handles[v.mp.ID] = v.ph |
| } |
| |
| return handles, nil |
| } |
| |
| // A packageHandleBuilder computes a batch of packageHandles concurrently, |
| // sharing computed transitive reachability sets used to compute package keys. |
| type packageHandleBuilder struct { |
| s *Snapshot |
| |
| // nodes are assembled synchronously. |
| nodes map[typerefs.IndexID]*handleNode |
| |
| // transitiveRefs is incrementally evaluated as package handles are built. |
| transitiveRefsMu sync.Mutex |
| transitiveRefs map[typerefs.IndexID]*partialRefs // see getTransitiveRefs |
| } |
| |
| // A handleNode represents a to-be-computed packageHandle within a graph of |
| // predecessors and successors. |
| // |
| // It is used to implement a bottom-up construction of packageHandles. |
| type handleNode struct { |
| mp *metadata.Package |
| idxID typerefs.IndexID |
| ph *packageHandle |
| preds []*handleNode |
| succs map[PackageID]*handleNode |
| unfinishedSuccs int32 |
| } |
| |
| // partialRefs maps names declared by a given package to their set of |
| // transitive references. |
| // |
| // If complete is set, refs is known to be complete for the package in |
| // question. Otherwise, it may only map a subset of all names declared by the |
| // package. |
| type partialRefs struct { |
| refs map[string]*typerefs.PackageSet |
| complete bool |
| } |
| |
| // getTransitiveRefs gets or computes the set of transitively reachable |
| // packages for each exported name in the package specified by id. |
| // |
| // The operation may fail if building a predecessor failed. If and only if this |
| // occurs, the result will be nil. |
| func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*typerefs.PackageSet { |
| b.transitiveRefsMu.Lock() |
| defer b.transitiveRefsMu.Unlock() |
| |
| idxID := b.s.view.pkgIndex.IndexID(pkgID) |
| trefs, ok := b.transitiveRefs[idxID] |
| if !ok { |
| trefs = &partialRefs{ |
| refs: make(map[string]*typerefs.PackageSet), |
| } |
| b.transitiveRefs[idxID] = trefs |
| } |
| |
| if !trefs.complete { |
| trefs.complete = true |
| node := b.nodes[idxID] |
| for name := range node.ph.refs { |
| if ('A' <= name[0] && name[0] <= 'Z') || token.IsExported(name) { |
| if _, ok := trefs.refs[name]; !ok { |
| pkgs := b.s.view.pkgIndex.NewSet() |
| for _, sym := range node.ph.refs[name] { |
| pkgs.Add(sym.Package) |
| otherSet := b.getOneTransitiveRefLocked(sym) |
| pkgs.Union(otherSet) |
| } |
| trefs.refs[name] = pkgs |
| } |
| } |
| } |
| } |
| |
| return trefs.refs |
| } |
| |
| // getOneTransitiveRefLocked computes the full set packages transitively |
| // reachable through the given sym reference. |
| // |
| // It may return nil if the reference is invalid (i.e. the referenced name does |
| // not exist). |
| func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *typerefs.PackageSet { |
| assert(token.IsExported(sym.Name), "expected exported symbol") |
| |
| trefs := b.transitiveRefs[sym.Package] |
| if trefs == nil { |
| trefs = &partialRefs{ |
| refs: make(map[string]*typerefs.PackageSet), |
| complete: false, |
| } |
| b.transitiveRefs[sym.Package] = trefs |
| } |
| |
| pkgs, ok := trefs.refs[sym.Name] |
| if ok && pkgs == nil { |
| // See below, where refs is set to nil before recursing. |
| bug.Reportf("cycle detected to %q in reference graph", sym.Name) |
| } |
| |
| // Note that if (!ok && trefs.complete), the name does not exist in the |
| // referenced package, and we should not write to trefs as that may introduce |
| // a race. |
| if !ok && !trefs.complete { |
| n := b.nodes[sym.Package] |
| if n == nil { |
| // We should always have IndexID in our node set, because symbol references |
| // should only be recorded for packages that actually exist in the import graph. |
| // |
| // However, it is not easy to prove this (typerefs are serialized and |
| // deserialized), so make this code temporarily defensive while we are on a |
| // point release. |
| // |
| // TODO(rfindley): in the future, we should turn this into an assertion. |
| bug.Reportf("missing reference to package %s", b.s.view.pkgIndex.PackageID(sym.Package)) |
| return nil |
| } |
| |
| // Break cycles. This is perhaps overly defensive as cycles should not |
| // exist at this point: metadata cycles should have been broken at load |
| // time, and intra-package reference cycles should have been contracted by |
| // the typerefs algorithm. |
| // |
| // See the "cycle detected" bug report above. |
| trefs.refs[sym.Name] = nil |
| |
| pkgs := b.s.view.pkgIndex.NewSet() |
| for _, sym2 := range n.ph.refs[sym.Name] { |
| pkgs.Add(sym2.Package) |
| otherSet := b.getOneTransitiveRefLocked(sym2) |
| pkgs.Union(otherSet) |
| } |
| trefs.refs[sym.Name] = pkgs |
| } |
| |
| return pkgs |
| } |
| |
| // evaluatePackageHandle recomputes the derived information in the package handle. |
| // On success, the handle's state is validKey. |
| // |
| // evaluatePackageHandle must only be called from getPackageHandles. |
| func (b *packageHandleBuilder) evaluatePackageHandle(ctx context.Context, n *handleNode) (err error) { |
| b.s.mu.Lock() |
| ph, hit := b.s.packages.Get(n.mp.ID) |
| b.s.mu.Unlock() |
| |
| defer func() { |
| if err == nil { |
| assert(ph.state >= validKey, "invalid handle") |
| |
| // Record the now valid key in the snapshot. |
| // There may be a race, so avoid the write if the recorded handle is |
| // already valid. |
| b.s.mu.Lock() |
| if alt, ok := b.s.packages.Get(n.mp.ID); !ok || alt.state < ph.state { |
| b.s.packages.Set(n.mp.ID, ph, nil) |
| } else { |
| ph = alt |
| } |
| b.s.mu.Unlock() |
| |
| // Initialize n.ph. |
| n.ph = ph |
| } |
| }() |
| |
| if hit && ph.state >= validKey { |
| return nil // already valid |
| } else { |
| // We'll need to update the package handle. Since this could happen |
| // concurrently, make a copy. |
| if hit { |
| ph = ph.clone() // state < validKey |
| } else { |
| ph = &packageHandle{ |
| mp: n.mp, |
| state: validMetadata, |
| } |
| } |
| } |
| |
| // Invariant: ph is either |
| // - a new handle in state validMetadata, or |
| // - a clone of an existing handle in state validMetadata or validLocalData. |
| |
| // State transition: validMetadata -> validLocalInputs. |
| localKeyChanged := false |
| if ph.state < validLocalData { |
| prevLocalKey := ph.localKey // may be zero |
| // No package handle: read and analyze the package syntax. |
| inputs, err := b.s.typeCheckInputs(ctx, n.mp) |
| if err != nil { |
| return err |
| } |
| refs, err := b.s.typerefs(ctx, n.mp, inputs.compiledGoFiles) |
| if err != nil { |
| return err |
| } |
| ph.loadDiagnostics = computeLoadDiagnostics(ctx, b.s, n.mp) |
| ph.localInputs = inputs |
| |
| checkOpen: |
| for _, files := range [][]file.Handle{inputs.goFiles, inputs.compiledGoFiles} { |
| for _, fh := range files { |
| if _, ok := fh.(*overlay); ok { |
| ph.isOpen = true |
| break checkOpen |
| } |
| } |
| } |
| if !ph.isOpen { |
| // ensure we don't hold data for closed packages |
| ph.pkgData = nil |
| } |
| ph.localKey = localPackageKey(inputs) |
| ph.refs = refs |
| ph.state = validLocalData |
| localKeyChanged = ph.localKey != prevLocalKey |
| } |
| |
| assert(ph.state == validLocalData, "unexpected handle state") |
| |
| // State transition: validLocalInputs -> validKey |
| |
| // Check if any dependencies have actually changed. |
| depsChanged := true |
| if ph.depKeys != nil { // ph was previously evaluated |
| depsChanged = len(ph.depKeys) != len(n.succs) |
| if !depsChanged { |
| for id, succ := range n.succs { |
| oldKey, ok := ph.depKeys[id] |
| assert(ok, "missing dep") |
| if oldKey != succ.ph.key { |
| depsChanged = true |
| break |
| } |
| } |
| } |
| } |
| |
| // Optimization: if the local package information did not change, nor did any |
| // of the dependencies, we don't need to re-run the reachability algorithm. |
| // |
| // Concretely: suppose A -> B -> C -> D, where '->' means "imports". If I |
| // type in a function body of D, I will probably invalidate types in D that C |
| // uses, because positions change, and therefore the package key of C will |
| // change. But B probably doesn't reach any types in D, and therefore the |
| // package key of B will not change. We still need to re-run the reachability |
| // algorithm on B to confirm. But if the key of B did not change, we don't |
| // even need to run the reachability algorithm on A. |
| if !localKeyChanged && !depsChanged { |
| ph.state = validKey |
| } |
| |
| keyChanged := false |
| if ph.state < validKey { |
| prevKey := ph.key |
| |
| // If we get here, it must be the case that deps have changed, so we must |
| // run the reachability algorithm. |
| ph.depKeys = make(map[PackageID]file.Hash) |
| |
| // See the typerefs package: the reachable set of packages is defined to be |
| // the set of packages containing syntax that is reachable through the |
| // symbol reference graph starting at the exported symbols in the |
| // dependencies of ph. |
| reachable := b.s.view.pkgIndex.NewSet() |
| for depID, succ := range n.succs { |
| ph.depKeys[depID] = succ.ph.key |
| reachable.Add(succ.idxID) |
| trefs := b.getTransitiveRefs(succ.mp.ID) |
| assert(trefs != nil, "nil trefs") |
| for _, set := range trefs { |
| reachable.Union(set) |
| } |
| } |
| |
| // Collect reachable nodes. |
| var reachableNodes []*handleNode |
| // In the presence of context cancellation, any package may be missing. |
| // We need all dependencies to produce a key. |
| reachable.Elems(func(id typerefs.IndexID) { |
| dh := b.nodes[id] |
| if dh == nil { |
| // Previous code reported an error (not a bug) here. |
| bug.Reportf("missing reachable node for %q", id) |
| } else { |
| reachableNodes = append(reachableNodes, dh) |
| } |
| }) |
| |
| // Sort for stability. |
| sort.Slice(reachableNodes, func(i, j int) bool { |
| return reachableNodes[i].mp.ID < reachableNodes[j].mp.ID |
| }) |
| |
| // Key is the hash of the local key of this package, and the local key of |
| // all reachable packages. |
| depHasher := sha256.New() |
| depHasher.Write(ph.localKey[:]) |
| reachablePaths := make([]string, len(reachableNodes)) |
| for i, dh := range reachableNodes { |
| depHasher.Write(dh.ph.localKey[:]) |
| reachablePaths[i] = string(dh.ph.mp.PkgPath) |
| } |
| depHasher.Sum(ph.key[:0]) |
| ph.reachable = bloom.NewFilter(reachablePaths) |
| ph.state = validKey |
| keyChanged = ph.key != prevKey |
| } |
| |
| assert(ph.state == validKey, "unexpected handle state") |
| |
| // Validate ph.pkgData, upgrading state if the package or its imports are |
| // still valid. |
| if ph.pkgData != nil { |
| pkgData := *ph.pkgData // make a copy |
| ph.pkgData = &pkgData |
| ph.state = validPackage |
| if keyChanged || ph.pkgData.pkg == nil { |
| ph.pkgData.pkg = nil // ensure we don't hold on to stale packages |
| ph.state = validImports |
| } |
| if depsChanged { |
| ph.pkgData = nil |
| ph.state = validKey |
| } |
| } |
| |
| // Postcondition: state >= validKey |
| |
| return nil |
| } |
| |
| // typerefs returns typerefs for the package described by m and cgfs, after |
| // either computing it or loading it from the file cache. |
| func (s *Snapshot) typerefs(ctx context.Context, mp *metadata.Package, cgfs []file.Handle) (map[string][]typerefs.Symbol, error) { |
| imports := make(map[ImportPath]*metadata.Package) |
| for impPath, id := range mp.DepsByImpPath { |
| if id != "" { |
| imports[impPath] = s.Metadata(id) |
| } |
| } |
| |
| data, err := s.typerefData(ctx, mp.ID, imports, cgfs) |
| if err != nil { |
| return nil, err |
| } |
| classes := typerefs.Decode(s.view.pkgIndex, data) |
| refs := make(map[string][]typerefs.Symbol) |
| for _, class := range classes { |
| for _, decl := range class.Decls { |
| refs[decl] = class.Refs |
| } |
| } |
| return refs, nil |
| } |
| |
| // typerefData retrieves encoded typeref data from the filecache, or computes it on |
| // a cache miss. |
| func (s *Snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*metadata.Package, cgfs []file.Handle) ([]byte, error) { |
| key := typerefsKey(id, imports, cgfs) |
| if data, err := filecache.Get(typerefsKind, key); err == nil { |
| return data, nil |
| } else if err != filecache.ErrNotFound { |
| bug.Reportf("internal error reading typerefs data: %v", err) |
| // Unexpected error: treat as cache miss, and fall through. |
| } |
| |
| pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, true, cgfs...) |
| if err != nil { |
| return nil, err |
| } |
| data := typerefs.Encode(pgfs, imports) |
| |
| // Store the resulting data in the cache. |
| go func() { |
| if err := filecache.Set(typerefsKind, key, data); err != nil { |
| event.Error(ctx, fmt.Sprintf("storing typerefs data for %s", id), err) |
| } |
| }() |
| |
| return data, nil |
| } |
| |
| // typerefsKey produces a key for the reference information produced by the |
| // typerefs package. |
| func typerefsKey(id PackageID, imports map[ImportPath]*metadata.Package, compiledGoFiles []file.Handle) file.Hash { |
| hasher := sha256.New() |
| |
| fmt.Fprintf(hasher, "typerefs: %s\n", id) |
| |
| for importPath, imp := range moremaps.Sorted(imports) { |
| // TODO(rfindley): strength reduce the typerefs.Export API to guarantee |
| // that it only depends on these attributes of dependencies. |
| fmt.Fprintf(hasher, "import %s %s %s", importPath, imp.ID, imp.Name) |
| } |
| |
| fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(compiledGoFiles)) |
| for _, fh := range compiledGoFiles { |
| fmt.Fprintln(hasher, fh.Identity()) |
| } |
| |
| var hash [sha256.Size]byte |
| hasher.Sum(hash[:0]) |
| return hash |
| } |
| |
| // typeCheckInputs contains the inputs of a call to typeCheckImpl, which |
| // type-checks a package. |
| // |
| // Part of the purpose of this type is to keep type checking in-sync with the |
| // package handle key, by explicitly identifying the inputs to type checking. |
| type typeCheckInputs struct { |
| id PackageID |
| |
| // Used for type checking: |
| pkgPath PackagePath |
| name PackageName |
| goFiles, compiledGoFiles []file.Handle |
| sizes types.Sizes |
| depsByImpPath map[ImportPath]PackageID |
| goVersion string // packages.Module.GoVersion, e.g. "1.18" |
| |
| // Used for type check diagnostics: |
| // TODO(rfindley): consider storing less data in gobDiagnostics, and |
| // interpreting each diagnostic in the context of a fixed set of options. |
| // Then these fields need not be part of the type checking inputs. |
| supportsRelatedInformation bool |
| linkTarget string |
| viewType ViewType |
| } |
| |
| func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (*typeCheckInputs, error) { |
| // Read both lists of files of this package. |
| // |
| // Parallelism is not necessary here as the files will have already been |
| // pre-read at load time. |
| // |
| // goFiles aren't presented to the type checker--nor |
| // are they included in the key, unsoundly--but their |
| // syntax trees are available from (*pkg).File(URI). |
| // TODO(adonovan): consider parsing them on demand? |
| // The need should be rare. |
| goFiles, err := readFiles(ctx, s, mp.GoFiles) |
| if err != nil { |
| return nil, err |
| } |
| compiledGoFiles, err := readFiles(ctx, s, mp.CompiledGoFiles) |
| if err != nil { |
| return nil, err |
| } |
| |
| goVersion := "" |
| if mp.Module != nil && mp.Module.GoVersion != "" { |
| goVersion = mp.Module.GoVersion |
| } |
| |
| return &typeCheckInputs{ |
| id: mp.ID, |
| pkgPath: mp.PkgPath, |
| name: mp.Name, |
| goFiles: goFiles, |
| compiledGoFiles: compiledGoFiles, |
| sizes: mp.TypesSizes, |
| depsByImpPath: mp.DepsByImpPath, |
| goVersion: goVersion, |
| |
| supportsRelatedInformation: s.Options().RelatedInformationSupported, |
| linkTarget: s.Options().LinkTarget, |
| viewType: s.view.typ, |
| }, nil |
| } |
| |
| // readFiles reads the content of each file URL from the source |
| // (e.g. snapshot or cache). |
| func readFiles(ctx context.Context, fs file.Source, uris []protocol.DocumentURI) (_ []file.Handle, err error) { |
| fhs := make([]file.Handle, len(uris)) |
| for i, uri := range uris { |
| fhs[i], err = fs.ReadFile(ctx, uri) |
| if err != nil { |
| return nil, err |
| } |
| } |
| return fhs, nil |
| } |
| |
| // localPackageKey returns a key for local inputs into type-checking, excluding |
| // dependency information: files, metadata, and configuration. |
| func localPackageKey(inputs *typeCheckInputs) file.Hash { |
| 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 collisions. |
| |
| // package identifiers |
| fmt.Fprintf(hasher, "package: %s %s %s\n", inputs.id, inputs.name, inputs.pkgPath) |
| |
| // module Go version |
| fmt.Fprintf(hasher, "go %s\n", inputs.goVersion) |
| |
| // import map |
| for impPath, depID := range moremaps.Sorted(inputs.depsByImpPath) { |
| fmt.Fprintf(hasher, "import %s %s", impPath, depID) |
| } |
| |
| // file names and contents |
| fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(inputs.compiledGoFiles)) |
| for _, fh := range inputs.compiledGoFiles { |
| fmt.Fprintln(hasher, fh.Identity()) |
| } |
| fmt.Fprintf(hasher, "goFiles: %d\n", len(inputs.goFiles)) |
| for _, fh := range inputs.goFiles { |
| fmt.Fprintln(hasher, fh.Identity()) |
| } |
| |
| // types sizes |
| wordSize := inputs.sizes.Sizeof(types.Typ[types.Int]) |
| maxAlign := inputs.sizes.Alignof(types.NewPointer(types.Typ[types.Int64])) |
| fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign) |
| |
| fmt.Fprintf(hasher, "relatedInformation: %t\n", inputs.supportsRelatedInformation) |
| fmt.Fprintf(hasher, "linkTarget: %s\n", inputs.linkTarget) |
| fmt.Fprintf(hasher, "viewType: %d\n", inputs.viewType) |
| |
| var hash [sha256.Size]byte |
| hasher.Sum(hash[:0]) |
| return hash |
| } |
| |
| // checkPackage type checks the parsed source files in compiledGoFiles. |
| // (The resulting pkg also holds the parsed but not type-checked goFiles.) |
| // deps holds the future results of type-checking the direct dependencies. |
| func (b *typeCheckBatch) checkPackage(ctx context.Context, fset *token.FileSet, ph *packageHandle, imports map[PackagePath]*types.Package) (*Package, error) { |
| inputs := ph.localInputs |
| ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackage", label.Package.Of(string(inputs.id))) |
| defer done() |
| |
| pkg := &syntaxPackage{ |
| id: inputs.id, |
| fset: fset, // must match parse call below |
| types: types.NewPackage(string(inputs.pkgPath), string(inputs.name)), |
| typesSizes: inputs.sizes, |
| 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), |
| Instances: make(map[*ast.Ident]types.Instance), |
| Selections: make(map[*ast.SelectorExpr]*types.Selection), |
| Scopes: make(map[ast.Node]*types.Scope), |
| FileVersions: make(map[*ast.File]string), |
| }, |
| } |
| |
| // Collect parsed files from the type check pass, capturing parse errors from |
| // compiled files. |
| var err error |
| pkg.goFiles, err = b.parseCache.parseFiles(ctx, pkg.fset, parsego.Full, false, inputs.goFiles...) |
| if err != nil { |
| return nil, err |
| } |
| pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, pkg.fset, parsego.Full, false, inputs.compiledGoFiles...) |
| if err != nil { |
| return nil, err |
| } |
| for _, pgf := range pkg.compiledGoFiles { |
| if pgf.ParseErr != nil { |
| pkg.parseErrors = append(pkg.parseErrors, pgf.ParseErr) |
| } |
| } |
| |
| // Use the default type information for the unsafe package. |
| if inputs.pkgPath == "unsafe" { |
| // Don't type check Unsafe: it's unnecessary, and doing so exposes a data |
| // race to Unsafe.completed. |
| pkg.types = types.Unsafe |
| } else { |
| |
| if len(pkg.compiledGoFiles) == 0 { |
| // No files most likely means go/packages failed. |
| // |
| // TODO(rfindley): in the past, we would capture go list errors in this |
| // case, to present go list errors to the user. However we had no tests for |
| // this behavior. It is unclear if anything better can be done here. |
| return nil, fmt.Errorf("no parsed files for package %s", inputs.pkgPath) |
| } |
| |
| onError := func(e error) { |
| pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) |
| } |
| cfg := b.typesConfig(ctx, inputs, imports, onError) |
| check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) |
| |
| var files []*ast.File |
| for _, cgf := range pkg.compiledGoFiles { |
| files = append(files, cgf.File) |
| } |
| |
| // Type checking is expensive, and we may not have encountered cancellations |
| // via parsing (e.g. if we got nothing but cache hits for parsed files). |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| |
| // Type checking errors are handled via the config, so ignore them here. |
| _ = check.Files(files) // 50us-15ms, depending on size of package |
| |
| // If the context was cancelled, we may have returned a ton of transient |
| // errors to the type checker. Swallow them. |
| if ctx.Err() != nil { |
| return nil, ctx.Err() |
| } |
| |
| // Collect imports by package path for the DependencyTypes API. |
| pkg.importMap = make(map[PackagePath]*types.Package) |
| var collectDeps func(*types.Package) |
| collectDeps = func(p *types.Package) { |
| pkgPath := PackagePath(p.Path()) |
| if _, ok := pkg.importMap[pkgPath]; ok { |
| return |
| } |
| pkg.importMap[pkgPath] = p |
| for _, imp := range p.Imports() { |
| collectDeps(imp) |
| } |
| } |
| collectDeps(pkg.types) |
| |
| // Work around golang/go#61561: interface instances aren't concurrency-safe |
| // as they are not completed by the type checker. |
| for _, inst := range pkg.typesInfo.Instances { |
| if iface, _ := inst.Type.Underlying().(*types.Interface); iface != nil { |
| iface.Complete() |
| } |
| } |
| } |
| |
| // Our heuristic for whether to show type checking errors is: |
| // + If there is a parse error _in the current file_, suppress type |
| // errors in that file. |
| // + Otherwise, show type errors even in the presence of parse errors in |
| // other package files. go/types attempts to suppress follow-on errors |
| // due to bad syntax, so on balance type checking errors still provide |
| // a decent signal/noise ratio as long as the file in question parses. |
| |
| // Track URIs with parse errors so that we can suppress type errors for these |
| // files. |
| unparsable := map[protocol.DocumentURI]bool{} |
| for _, e := range pkg.parseErrors { |
| diags, err := parseErrorDiagnostics(pkg, e) |
| if err != nil { |
| event.Error(ctx, "unable to compute positions for parse errors", err, label.Package.Of(string(inputs.id))) |
| continue |
| } |
| for _, diag := range diags { |
| unparsable[diag.URI] = true |
| pkg.diagnostics = append(pkg.diagnostics, diag) |
| } |
| } |
| |
| diags := typeErrorsToDiagnostics(pkg, inputs, pkg.typeErrors) |
| for _, diag := range diags { |
| // If the file didn't parse cleanly, it is highly likely that type |
| // checking errors will be confusing or redundant. But otherwise, type |
| // checking usually provides a good enough signal to include. |
| if !unparsable[diag.URI] { |
| pkg.diagnostics = append(pkg.diagnostics, diag) |
| } |
| } |
| |
| return &Package{ph.mp, ph.loadDiagnostics, pkg}, nil |
| } |
| |
| // e.g. "go1" or "go1.2" or "go1.2.3" |
| var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*(?:\.(0|[1-9][0-9]*)){0,2}$`) |
| |
| func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs *typeCheckInputs, imports map[PackagePath]*types.Package, onError func(e error)) *types.Config { |
| cfg := &types.Config{ |
| Sizes: inputs.sizes, |
| Error: onError, |
| Importer: importerFunc(func(path string) (*types.Package, error) { |
| // While all of the import errors could be reported |
| // based on the metadata before we start type checking, |
| // reporting them via types.Importer places the errors |
| // at the correct source location. |
| id, ok := inputs.depsByImpPath[ImportPath(path)] |
| if !ok { |
| // If the import declaration is broken, |
| // go list may fail to report metadata about it. |
| // See TestFixImportDecl for an example. |
| return nil, fmt.Errorf("missing metadata for import of %q", path) |
| } |
| depPH := b.getHandle(id) |
| if depPH == nil { |
| // e.g. missing metadata for dependencies in buildPackageHandle |
| return nil, missingPkgError(inputs.id, path, inputs.viewType) |
| } |
| if !metadata.IsValidImport(inputs.pkgPath, depPH.mp.PkgPath, inputs.viewType != GoPackagesDriverView) { |
| return nil, fmt.Errorf("invalid use of internal package %q", path) |
| } |
| // For syntax packages, the set of required imports is known and |
| // precomputed. For import packages (checkPackageForImport), imports are |
| // constructed lazily, because they may not have been needed if we could |
| // have imported from export data. |
| // |
| // TODO(rfindley): refactor to move this logic to the callsite. |
| if imports != nil { |
| imp, ok := imports[depPH.mp.PkgPath] |
| if !ok { |
| return nil, fmt.Errorf("missing import %s", id) |
| } |
| return imp, nil |
| } |
| return b.getImportPackage(ctx, id) |
| }), |
| } |
| |
| if inputs.goVersion != "" { |
| goVersion := "go" + inputs.goVersion |
| if validGoVersion(goVersion) { |
| cfg.GoVersion = goVersion |
| } |
| } |
| |
| // We want to type check cgo code if go/types supports it. |
| // We passed typecheckCgo to go/packages when we Loaded. |
| typesinternal.SetUsesCgo(cfg) |
| return cfg |
| } |
| |
| // validGoVersion reports whether goVersion is a valid Go version for go/types. |
| // types.NewChecker panics if GoVersion is invalid. |
| // |
| // Note that, prior to go1.21, go/types required exactly two components to the |
| // version number. For example, go types would panic with the Go version |
| // go1.21.1. validGoVersion handles this case when built with go1.20 or earlier. |
| func validGoVersion(goVersion string) bool { |
| if !goVersionRx.MatchString(goVersion) { |
| return false // malformed version string |
| } |
| |
| if relVer := releaseVersion(); relVer != "" && versions.Before(versions.Lang(relVer), versions.Lang(goVersion)) { |
| return false // 'go list' is too new for go/types |
| } |
| |
| // TODO(rfindley): remove once we no longer support building gopls with Go |
| // 1.20 or earlier. |
| if !slices.Contains(build.Default.ReleaseTags, "go1.21") && strings.Count(goVersion, ".") >= 2 { |
| return false // unsupported patch version |
| } |
| |
| return true |
| } |
| |
| // releaseVersion reports the Go language version used to compile gopls, or "" |
| // if it cannot be determined. |
| func releaseVersion() string { |
| if len(build.Default.ReleaseTags) > 0 { |
| v := build.Default.ReleaseTags[len(build.Default.ReleaseTags)-1] |
| var dummy int |
| if _, err := fmt.Sscanf(v, "go1.%d", &dummy); err == nil { |
| return v |
| } |
| } |
| return "" |
| } |
| |
| // depsErrors creates diagnostics for each metadata error (e.g. import cycle). |
| // These may be attached to import declarations in the transitive source files |
| // of pkg, or to 'requires' declarations in the package's go.mod file. |
| // |
| // TODO(rfindley): move this to load.go |
| func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ([]*Diagnostic, error) { |
| // Select packages that can't be found, and were imported in non-workspace packages. |
| // Workspace packages already show their own errors. |
| var relevantErrors []*packagesinternal.PackageError |
| for _, depsError := range mp.DepsErrors { |
| // Up to Go 1.15, the missing package was included in the stack, which |
| // was presumably a bug. We want the next one up. |
| directImporterIdx := len(depsError.ImportStack) - 1 |
| if directImporterIdx < 0 { |
| continue |
| } |
| |
| directImporter := depsError.ImportStack[directImporterIdx] |
| if snapshot.IsWorkspacePackage(PackageID(directImporter)) { |
| continue |
| } |
| relevantErrors = append(relevantErrors, depsError) |
| } |
| |
| // Don't build the import index for nothing. |
| if len(relevantErrors) == 0 { |
| return nil, nil |
| } |
| |
| // Subsequent checks require Go files. |
| if len(mp.CompiledGoFiles) == 0 { |
| return nil, nil |
| } |
| |
| // Build an index of all imports in the package. |
| type fileImport struct { |
| cgf *parsego.File |
| imp *ast.ImportSpec |
| } |
| allImports := map[string][]fileImport{} |
| for _, uri := range mp.CompiledGoFiles { |
| pgf, err := parseGoURI(ctx, snapshot, uri, parsego.Header) |
| if err != nil { |
| return nil, err |
| } |
| fset := tokeninternal.FileSetFor(pgf.Tok) |
| // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). |
| for _, group := range astutil.Imports(fset, pgf.File) { |
| for _, imp := range group { |
| if imp.Path == nil { |
| continue |
| } |
| path := strings.Trim(imp.Path.Value, `"`) |
| allImports[path] = append(allImports[path], fileImport{pgf, imp}) |
| } |
| } |
| } |
| |
| // Apply a diagnostic to any import involved in the error, stopping once |
| // we reach the workspace. |
| var errors []*Diagnostic |
| for _, depErr := range relevantErrors { |
| for i := len(depErr.ImportStack) - 1; i >= 0; i-- { |
| item := depErr.ImportStack[i] |
| if snapshot.IsWorkspacePackage(PackageID(item)) { |
| break |
| } |
| |
| for _, imp := range allImports[item] { |
| rng, err := imp.cgf.NodeRange(imp.imp) |
| if err != nil { |
| return nil, err |
| } |
| diag := &Diagnostic{ |
| URI: imp.cgf.URI, |
| Range: rng, |
| Severity: protocol.SeverityError, |
| Source: TypeError, |
| Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), |
| SuggestedFixes: goGetQuickFixes(mp.Module != nil, imp.cgf.URI, item), |
| } |
| if !bundleLazyFixes(diag) { |
| bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) |
| } |
| errors = append(errors, diag) |
| } |
| } |
| } |
| |
| modFile, err := findRootPattern(ctx, mp.CompiledGoFiles[0].Dir(), "go.mod", snapshot) |
| if err != nil { |
| return nil, err |
| } |
| pm, err := parseModURI(ctx, snapshot, modFile) |
| if err != nil { |
| return nil, err |
| } |
| |
| // Add a diagnostic to the module that contained the lowest-level import of |
| // the missing package. |
| for _, depErr := range relevantErrors { |
| for i := len(depErr.ImportStack) - 1; i >= 0; i-- { |
| item := depErr.ImportStack[i] |
| mp := snapshot.Metadata(PackageID(item)) |
| if mp == nil || mp.Module == nil { |
| continue |
| } |
| modVer := module.Version{Path: mp.Module.Path, Version: mp.Module.Version} |
| reference := findModuleReference(pm.File, modVer) |
| if reference == nil { |
| continue |
| } |
| rng, err := pm.Mapper.OffsetRange(reference.Start.Byte, reference.End.Byte) |
| if err != nil { |
| return nil, err |
| } |
| diag := &Diagnostic{ |
| URI: pm.URI, |
| Range: rng, |
| Severity: protocol.SeverityError, |
| Source: TypeError, |
| Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), |
| SuggestedFixes: goGetQuickFixes(true, pm.URI, item), |
| } |
| if !bundleLazyFixes(diag) { |
| bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) |
| } |
| errors = append(errors, diag) |
| break |
| } |
| } |
| return errors, nil |
| } |
| |
| // missingPkgError returns an error message for a missing package that varies |
| // based on the user's workspace mode. |
| func missingPkgError(from PackageID, pkgPath string, viewType ViewType) error { |
| switch viewType { |
| case GoModView, GoWorkView: |
| if metadata.IsCommandLineArguments(from) { |
| return fmt.Errorf("current file is not included in a workspace module") |
| } else { |
| // Previously, we would present the initialization error here. |
| return fmt.Errorf("no required module provides package %q", pkgPath) |
| } |
| case AdHocView: |
| return fmt.Errorf("cannot find package %q in GOROOT", pkgPath) |
| case GoPackagesDriverView: |
| return fmt.Errorf("go/packages driver could not load %q", pkgPath) |
| case GOPATHView: |
| return fmt.Errorf("cannot find package %q in GOROOT or GOPATH", pkgPath) |
| default: |
| return fmt.Errorf("unable to load package") |
| } |
| } |
| |
| // typeErrorsToDiagnostics translates a slice of types.Errors into a slice of |
| // Diagnostics. |
| // |
| // In addition to simply mapping data such as position information and error |
| // codes, this function interprets related go/types "continuation" errors as |
| // protocol.DiagnosticRelatedInformation. Continuation errors are go/types |
| // errors whose messages starts with "\t". By convention, these errors relate |
| // to the previous error in the errs slice (such as if they were printed in |
| // sequence to a terminal). |
| // |
| // Fields in typeCheckInputs may affect the resulting diagnostics. |
| func typeErrorsToDiagnostics(pkg *syntaxPackage, inputs *typeCheckInputs, errs []types.Error) []*Diagnostic { |
| var result []*Diagnostic |
| |
| // batch records diagnostics for a set of related types.Errors. |
| // (related[0] is the primary error.) |
| batch := func(related []types.Error) { |
| var diags []*Diagnostic |
| for i, e := range related { |
| code, start, end, ok := typesinternal.ErrorCodeStartEnd(e) |
| if !ok || !start.IsValid() || !end.IsValid() { |
| start, end = e.Pos, e.Pos |
| code = 0 |
| } |
| if !start.IsValid() { |
| // Type checker errors may be missing position information if they |
| // relate to synthetic syntax, such as if the file were fixed. In that |
| // case, we should have a parse error anyway, so skipping the type |
| // checker error is likely benign. |
| // |
| // TODO(golang/go#64335): we should eventually verify that all type |
| // checked syntax has valid positions, and promote this skip to a bug |
| // report. |
| continue |
| } |
| |
| // Invariant: both start and end are IsValid. |
| if !end.IsValid() { |
| panic("end is invalid") |
| } |
| |
| posn := safetoken.StartPosition(e.Fset, start) |
| if !posn.IsValid() { |
| // All valid positions produced by the type checker should described by |
| // its fileset, yet since type checker errors are associated with |
| // positions in the AST, and AST nodes can overflow the file |
| // (golang/go#48300), we can't rely on this. |
| // |
| // We should fix the parser, but in the meantime type errors are not |
| // significant if there are parse errors, so we can safely ignore this |
| // case. |
| if len(pkg.parseErrors) == 0 { |
| bug.Reportf("internal error: type checker error %q outside its Fset", e) |
| } |
| continue |
| } |
| pgf, err := pkg.File(protocol.URIFromPath(posn.Filename)) |
| if err != nil { |
| // Sometimes type-checker errors refer to positions in other packages, |
| // such as when a declaration duplicates a dot-imported name. |
| // |
| // In these cases, we don't want to report an error in the other |
| // package (the message would be rather confusing), but we do want to |
| // report an error in the current package (golang/go#59005). |
| if i == 0 { |
| if pkg.hasFixedFiles() { |
| bug.Reportf("internal error: could not locate file for primary type checker error %v: %v (fixed files)", e, err) |
| } else { |
| bug.Reportf("internal error: could not locate file for primary type checker error %v: %v", e, err) |
| } |
| } |
| continue |
| } |
| |
| // debugging golang/go#65960 |
| // |
| // At this point, we know 'start' IsValid, and |
| // StartPosition(start) worked (with e.Fset). |
| // |
| // If the asserted condition is true, 'start' |
| // is also in range for pgf.Tok, which means |
| // the PosRange failure must be caused by 'end'. |
| if pgf.Tok != e.Fset.File(start) { |
| if pkg.hasFixedFiles() { |
| bug.Reportf("internal error: inconsistent token.Files for pos (fixed files)") |
| } else { |
| bug.Reportf("internal error: inconsistent token.Files for pos") |
| } |
| } |
| |
| if end == start { |
| // Expand the end position to a more meaningful span. |
| // |
| // TODO(adonovan): It is the type checker's responsibility |
| // to ensure that (start, end) are meaningful; see #71803. |
| end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start) |
| |
| // debugging golang/go#65960 |
| if _, err := safetoken.Offset(pgf.Tok, end); err != nil { |
| if pkg.hasFixedFiles() { |
| bug.Reportf("TypeErrorEndPos returned invalid end: %v (fixed files)", err) |
| } else { |
| bug.Reportf("TypeErrorEndPos returned invalid end: %v", err) |
| } |
| } |
| } else { |
| // TODO(adonovan): check File(start)==File(end). |
| |
| // debugging golang/go#65960 |
| if _, err := safetoken.Offset(pgf.Tok, end); err != nil { |
| if pkg.hasFixedFiles() { |
| bug.Reportf("ErrorCodeStartEnd returned invalid end: %v (fixed files)", err) |
| } else { |
| bug.Reportf("ErrorCodeStartEnd returned invalid end: %v", err) |
| } |
| } |
| } |
| |
| rng, err := pgf.Mapper.PosRange(pgf.Tok, start, end) |
| if err != nil { |
| bug.Reportf("internal error: could not compute pos to range for %v: %v", e, err) |
| continue |
| } |
| msg := related[0].Msg // primary |
| if i > 0 { |
| if inputs.supportsRelatedInformation { |
| msg += " (see details)" |
| } else { |
| msg += fmt.Sprintf(" (this error: %v)", e.Msg) |
| } |
| } |
| diag := &Diagnostic{ |
| URI: pgf.URI, |
| Range: rng, |
| Severity: protocol.SeverityError, |
| Source: TypeError, |
| Message: msg, |
| } |
| if code != 0 { |
| diag.Code = code.String() |
| diag.CodeHref = typesCodeHref(inputs.linkTarget, code) |
| } |
| if code == typesinternal.UnusedVar || code == typesinternal.UnusedImport { |
| diag.Tags = append(diag.Tags, protocol.Unnecessary) |
| } |
| if match := importErrorRe.FindStringSubmatch(e.Msg); match != nil { |
| diag.SuggestedFixes = append(diag.SuggestedFixes, goGetQuickFixes(inputs.viewType.usesModules(), pgf.URI, match[1])...) |
| } |
| if match := unsupportedFeatureRe.FindStringSubmatch(e.Msg); match != nil { |
| diag.SuggestedFixes = append(diag.SuggestedFixes, editGoDirectiveQuickFix(inputs.viewType.usesModules(), pgf.URI, match[1])...) |
| } |
| |
| // Link up related information. For the primary error, all related errors |
| // are treated as related information. For secondary errors, only the |
| // primary is related. |
| // |
| // This is because go/types assumes that errors are read top-down, such as |
| // in the cycle error "A refers to...". The structure of the secondary |
| // error set likely only makes sense for the primary error. |
| // |
| // NOTE: len(diags) == 0 if the primary diagnostic has invalid positions. |
| // See also golang/go#66731. |
| if i > 0 && len(diags) > 0 { |
| primary := diags[0] |
| primary.Related = append(primary.Related, protocol.DiagnosticRelatedInformation{ |
| Location: diag.URI.Location(diag.Range), |
| Message: related[i].Msg, // use the unmodified secondary error for related errors. |
| }) |
| diag.Related = []protocol.DiagnosticRelatedInformation{{ |
| Location: primary.URI.Location(primary.Range), |
| }} |
| } |
| diags = append(diags, diag) |
| } |
| result = append(result, diags...) |
| } |
| |
| // Process batches of related errors. |
| for len(errs) > 0 { |
| related := []types.Error{errs[0]} |
| for i := 1; i < len(errs); i++ { |
| spl := errs[i] |
| if len(spl.Msg) == 0 || spl.Msg[0] != '\t' { |
| break |
| } |
| spl.Msg = spl.Msg[len("\t"):] |
| related = append(related, spl) |
| } |
| batch(related) |
| errs = errs[len(related):] |
| } |
| |
| return result |
| } |
| |
| // An importFunc is an implementation of the single-method |
| // types.Importer interface based on a function value. |
| type importerFunc func(path string) (*types.Package, error) |
| |
| func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } |