// 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" "fmt" "go/types" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/telemetry" "golang.org/x/tools/internal/span" "golang.org/x/tools/internal/telemetry/log" "golang.org/x/tools/internal/telemetry/tag" "golang.org/x/tools/internal/telemetry/trace" errors "golang.org/x/xerrors" ) type metadata struct { id packageID pkgPath packagePath name string files []span.URI typesSizes types.Sizes errors []packages.Error deps []packageID missingDeps map[packagePath]struct{} // config is the *packages.Config associated with the loaded package. config *packages.Config } func (s *snapshot) load(ctx context.Context, uri span.URI) ([]*metadata, error) { ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.URI.Of(uri)) defer done() cfg := s.view.Config(ctx) pkgs, err := packages.Load(cfg, fmt.Sprintf("file=%s", uri.Filename())) // If the context was canceled, return early. // Otherwise, we might be type-checking an incomplete result. if err == context.Canceled { return nil, errors.Errorf("no metadata for %s: %v", uri.Filename(), err) } log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs))) if len(pkgs) == 0 { if err == nil { err = errors.Errorf("go/packages.Load: no packages found for %s", uri) } // Return this error as a diagnostic to the user. return nil, err } m, prevMissingImports, err := s.updateMetadata(ctx, uri, pkgs, cfg) if err != nil { return nil, err } meta, err := validateMetadata(ctx, m, prevMissingImports) if err != nil { return nil, err } return meta, nil } func validateMetadata(ctx context.Context, metadata []*metadata, prevMissingImports map[packageID]map[packagePath]struct{}) ([]*metadata, error) { // If we saw incorrect metadata for this package previously, don't both rechecking it. for _, m := range metadata { if len(m.missingDeps) > 0 { prev, ok := prevMissingImports[m.id] // There are missing imports that we previously hadn't seen before. if !ok { return metadata, nil } // The set of missing imports has changed. if !sameSet(prev, m.missingDeps) { return metadata, nil } } else { // There are no missing imports. return metadata, nil } } return nil, nil } func sameSet(x, y map[packagePath]struct{}) bool { if len(x) != len(y) { return false } for k := range x { if _, ok := y[k]; !ok { return false } } return true } // shouldLoad reparses a file's package and import declarations to // determine if they have changed. func (c *cache) shouldLoad(ctx context.Context, s *snapshot, originalFH, currentFH source.FileHandle) bool { if originalFH == nil { return true } // Get the original and current parsed files in order to check package name and imports. original, _, _, originalErr := c.ParseGoHandle(originalFH, source.ParseHeader).Parse(ctx) current, _, _, currentErr := c.ParseGoHandle(currentFH, source.ParseHeader).Parse(ctx) if originalErr != nil || currentErr != nil { return (originalErr == nil) != (currentErr == nil) } // Check if the package's metadata has changed. The cases handled are: // // 1. A package's name has changed // 2. A file's imports have changed // if original.Name.Name != current.Name.Name { return true } // If the package's imports have changed, re-run `go list`. if len(original.Imports) != len(current.Imports) { return true } for i, importSpec := range original.Imports { // TODO: Handle the case where the imports have just been re-ordered. if importSpec.Path.Value != current.Imports[i].Path.Value { return true } } return false } func (s *snapshot) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package, cfg *packages.Config) ([]*metadata, map[packageID]map[packagePath]struct{}, error) { // Clear metadata since we are re-running go/packages. prevMissingImports := make(map[packageID]map[packagePath]struct{}) m := s.getMetadataForURI(uri) for _, m := range m { if len(m.missingDeps) > 0 { prevMissingImports[m.id] = m.missingDeps } } var results []*metadata for _, pkg := range pkgs { log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles)) // Set the metadata for this package. if err := s.updateImports(ctx, packagePath(pkg.PkgPath), pkg, cfg); err != nil { return nil, nil, err } m := s.getMetadata(packageID(pkg.ID)) if m != nil { results = append(results, m) } } // Rebuild the import graph when the metadata is updated. s.clearAndRebuildImportGraph() if len(results) == 0 { return nil, nil, errors.Errorf("no metadata for %s", uri) } return results, prevMissingImports, nil } func (s *snapshot) updateImports(ctx context.Context, pkgPath packagePath, pkg *packages.Package, cfg *packages.Config) error { // Recreate the metadata rather than reusing it to avoid locking. m := &metadata{ id: packageID(pkg.ID), pkgPath: pkgPath, name: pkg.Name, typesSizes: pkg.TypesSizes, errors: pkg.Errors, config: cfg, } for _, filename := range pkg.CompiledGoFiles { uri := span.FileURI(filename) m.files = append(m.files, uri) s.addID(uri, m.id) } // Add the metadata to the cache. s.setMetadata(m) for importPath, importPkg := range pkg.Imports { importPkgPath := packagePath(importPath) importID := packageID(importPkg.ID) if importPkgPath == pkgPath { return errors.Errorf("cycle detected in %s", importPath) } m.deps = append(m.deps, importID) // Don't remember any imports with significant errors. if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 { if m.missingDeps == nil { m.missingDeps = make(map[packagePath]struct{}) } m.missingDeps[importPkgPath] = struct{}{} continue } dep := s.getMetadata(importID) if dep == nil { if err := s.updateImports(ctx, importPkgPath, importPkg, cfg); err != nil { log.Error(ctx, "error in dependency", err) } } } return nil }