Skip to content

Commit 91311ab

Browse files
committed
gopls/internal/lsp/cache: better import path hygiene
An import path is not the same as a package path. For example, the declaration: import "example.com/foo" may load a package whose PkgPath() is "vendor/example.com/foo". There was a comment hinting at this in load.go. This change introduces the concept of ImportPath as a distinct type from PackagePath, and documents clearly throughout the relevant APIs which one is expected. Notes: - Metadata.PkgPath is consistently set from the PackagePath, not sometimes (when a root) from PackagePath and sometimes (when indirectly loaded) from an arbitrary ImportPath that resolves to it. I expect this means some packages will now (correctly) appear to be called "vendor/example.com/foo" in the user interface where previously the vendor prefix was omitted. - Metadata.Deps is gone. - Metadata.Imports is a new map from ImportPath to ID. - Metadata.MissingDeps is now a set of ImportPath. - the package loading key is now based on a hash of unique imports. (Duplicates would cancel out.) - The ImporterFunc no longer needs the guesswork of resolveImportPath, since 'go list' already told us the correct answer. - Package.GetImport is renamed DirectDep(packagePath) to avoid the suggestion that it accepts an ImportPath. - Package.ResolveImportPath is the analogous method for import paths. Most clients seem to want this. Change-Id: I4999709120fff4663ba8669358fe149f1626bb8e Reviewed-on: https://go-review.googlesource.com/c/tools/+/443636 Run-TryBot: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 9eda97b commit 91311ab

18 files changed

+213
-150
lines changed

gopls/internal/lsp/cache/analysis.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A
132132
}
133133

134134
// Add a dependency on each required analyzer.
135-
var deps []*actionHandle
135+
var deps []*actionHandle // unordered
136136
for _, req := range a.Requires {
137137
// TODO(adonovan): opt: there's no need to repeat the package-handle
138138
// portion of the recursion here, since we have the pkg already.
@@ -150,7 +150,7 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A
150150
// An analysis that consumes/produces facts
151151
// must run on the package's dependencies too.
152152
if len(a.FactTypes) > 0 {
153-
for _, importID := range ph.m.Deps {
153+
for _, importID := range ph.m.Imports {
154154
depActionHandle, err := s.actionHandle(ctx, importID, a)
155155
if err != nil {
156156
return nil, err

gopls/internal/lsp/cache/check.go

+36-62
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"fmt"
1212
"go/ast"
1313
"go/types"
14-
"path"
1514
"path/filepath"
1615
"regexp"
1716
"strings"
@@ -99,9 +98,13 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
9998
// TODO(adonovan): use a promise cache to ensure that the key
10099
// for each package is computed by at most one thread, then do
101100
// the recursive key building of dependencies in parallel.
102-
deps := make(map[PackagePath]*packageHandle)
103-
depKeys := make([]packageHandleKey, len(m.Deps))
104-
for i, depID := range m.Deps {
101+
deps := make(map[PackageID]*packageHandle)
102+
var depKey source.Hash // XOR of all unique deps
103+
for _, depID := range m.Imports {
104+
depHandle, ok := deps[depID]
105+
if ok {
106+
continue // e.g. duplicate import
107+
}
105108
depHandle, err := s.buildPackageHandle(ctx, depID, s.workspaceParseMode(depID))
106109
// Don't use invalid metadata for dependencies if the top-level
107110
// metadata is valid. We only load top-level packages, so if the
@@ -125,8 +128,9 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
125128
continue
126129
}
127130

128-
deps[depHandle.m.PkgPath] = depHandle
129-
depKeys[i] = depHandle.key
131+
depKey.XORWith(source.Hash(depHandle.key))
132+
133+
deps[depID] = depHandle
130134
}
131135

132136
// Read both lists of files of this package, in parallel.
@@ -144,7 +148,7 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
144148
// All the file reading has now been done.
145149
// Create a handle for the result of type checking.
146150
experimentalKey := s.View().Options().ExperimentalPackageCacheKey
147-
phKey := computePackageKey(m.ID, compiledGoFiles, m, depKeys, mode, experimentalKey)
151+
phKey := computePackageKey(m.ID, compiledGoFiles, m, depKey, mode, experimentalKey)
148152
promise, release := s.store.Promise(phKey, func(ctx context.Context, arg interface{}) interface{} {
149153

150154
pkg, err := typeCheckImpl(ctx, arg.(*snapshot), goFiles, compiledGoFiles, m.Metadata, mode, deps)
@@ -225,8 +229,8 @@ func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode {
225229

226230
// computePackageKey returns a key representing the act of type checking
227231
// a package named id containing the specified files, metadata, and
228-
// dependency hashes.
229-
func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, deps []packageHandleKey, mode source.ParseMode, experimentalKey bool) packageHandleKey {
232+
// combined dependency hash.
233+
func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata, depsKey source.Hash, mode source.ParseMode, experimentalKey bool) packageHandleKey {
230234
// TODO(adonovan): opt: no need to materalize the bytes; hash them directly.
231235
// Also, use field separators to avoid spurious collisions.
232236
b := bytes.NewBuffer(nil)
@@ -243,9 +247,7 @@ func computePackageKey(id PackageID, files []source.FileHandle, m *KnownMetadata
243247
b.Write(hc[:])
244248
}
245249
b.WriteByte(byte(mode))
246-
for _, dep := range deps {
247-
b.Write(dep[:])
248-
}
250+
b.Write(depsKey[:])
249251
for _, file := range files {
250252
b.WriteString(file.FileIdentity().String())
251253
}
@@ -311,7 +313,7 @@ func (ph *packageHandle) cached() (*pkg, error) {
311313
// typeCheckImpl type checks the parsed source files in compiledGoFiles.
312314
// (The resulting pkg also holds the parsed but not type-checked goFiles.)
313315
// deps holds the future results of type-checking the direct dependencies.
314-
func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle) (*pkg, error) {
316+
func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle) (*pkg, error) {
315317
// Start type checking of direct dependencies,
316318
// in parallel and asynchronously.
317319
// As the type checker imports each of these
@@ -446,15 +448,15 @@ func typeCheckImpl(ctx context.Context, snapshot *snapshot, goFiles, compiledGoF
446448

447449
var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
448450

449-
func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackagePath]*packageHandle, astFilter *unexportedFilter) (*pkg, error) {
451+
func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFiles []source.FileHandle, m *Metadata, mode source.ParseMode, deps map[PackageID]*packageHandle, astFilter *unexportedFilter) (*pkg, error) {
450452
ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(m.ID)))
451453
defer done()
452454

453455
pkg := &pkg{
454-
m: m,
455-
mode: mode,
456-
imports: make(map[PackagePath]*pkg),
457-
types: types.NewPackage(string(m.PkgPath), string(m.Name)),
456+
m: m,
457+
mode: mode,
458+
depsByPkgPath: make(map[PackagePath]*pkg),
459+
types: types.NewPackage(string(m.PkgPath), string(m.Name)),
458460
typesInfo: &types.Info{
459461
Types: make(map[ast.Expr]types.TypeAndValue),
460462
Defs: make(map[*ast.Ident]types.Object),
@@ -522,23 +524,30 @@ func doTypeCheck(ctx context.Context, snapshot *snapshot, goFiles, compiledGoFil
522524
Error: func(e error) {
523525
pkg.typeErrors = append(pkg.typeErrors, e.(types.Error))
524526
},
525-
Importer: importerFunc(func(pkgPath string) (*types.Package, error) {
526-
// If the context was cancelled, we should abort.
527-
if ctx.Err() != nil {
528-
return nil, ctx.Err()
527+
Importer: importerFunc(func(path string) (*types.Package, error) {
528+
// While all of the import errors could be reported
529+
// based on the metadata before we start type checking,
530+
// reporting them via types.Importer places the errors
531+
// at the correct source location.
532+
id, ok := pkg.m.Imports[ImportPath(path)]
533+
if !ok {
534+
// If the import declaration is broken,
535+
// go list may fail to report metadata about it.
536+
// See TestFixImportDecl for an example.
537+
return nil, fmt.Errorf("missing metadata for import of %q", path)
529538
}
530-
dep := resolveImportPath(pkgPath, pkg, deps)
531-
if dep == nil {
532-
return nil, snapshot.missingPkgError(ctx, pkgPath)
539+
dep, ok := deps[id]
540+
if !ok {
541+
return nil, snapshot.missingPkgError(ctx, path)
533542
}
534543
if !source.IsValidImport(string(m.PkgPath), string(dep.m.PkgPath)) {
535-
return nil, fmt.Errorf("invalid use of internal package %s", pkgPath)
544+
return nil, fmt.Errorf("invalid use of internal package %s", path)
536545
}
537546
depPkg, err := dep.await(ctx, snapshot)
538547
if err != nil {
539548
return nil, err
540549
}
541-
pkg.imports[depPkg.m.PkgPath] = depPkg
550+
pkg.depsByPkgPath[depPkg.m.PkgPath] = depPkg
542551
return depPkg.types, nil
543552
}),
544553
}
@@ -840,41 +849,6 @@ func expandErrors(errs []types.Error, supportsRelatedInformation bool) []extende
840849
return result
841850
}
842851

843-
// resolveImportPath resolves an import path in pkg to a package from deps.
844-
// It should produce the same results as resolveImportPath:
845-
// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/load/pkg.go;drc=641918ee09cb44d282a30ee8b66f99a0b63eaef9;l=990.
846-
func resolveImportPath(importPath string, pkg *pkg, deps map[PackagePath]*packageHandle) *packageHandle {
847-
if dep := deps[PackagePath(importPath)]; dep != nil {
848-
return dep
849-
}
850-
// We may be in GOPATH mode, in which case we need to check vendor dirs.
851-
searchDir := path.Dir(pkg.PkgPath())
852-
for {
853-
vdir := PackagePath(path.Join(searchDir, "vendor", importPath))
854-
if vdep := deps[vdir]; vdep != nil {
855-
return vdep
856-
}
857-
858-
// Search until Dir doesn't take us anywhere new, e.g. "." or "/".
859-
next := path.Dir(searchDir)
860-
if searchDir == next {
861-
break
862-
}
863-
searchDir = next
864-
}
865-
866-
// Vendor didn't work. Let's try minimal module compatibility mode.
867-
// In MMC, the packagePath is the canonical (.../vN/...) path, which
868-
// is hard to calculate. But the go command has already resolved the ID
869-
// to the non-versioned path, and we can take advantage of that.
870-
for _, dep := range deps {
871-
if dep.ID() == importPath {
872-
return dep
873-
}
874-
}
875-
return nil
876-
}
877-
878852
// An importFunc is an implementation of the single-method
879853
// types.Importer interface based on a function value.
880854
type importerFunc func(path string) (*types.Package, error)

gopls/internal/lsp/cache/graph.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (g *metadataGraph) build() {
5353
// Build the import graph.
5454
g.importedBy = make(map[PackageID][]PackageID)
5555
for id, m := range g.metadata {
56-
for _, importID := range m.Deps {
56+
for _, importID := range uniqueDeps(m.Imports) {
5757
g.importedBy[importID] = append(g.importedBy[importID], id)
5858
}
5959
}
@@ -129,8 +129,27 @@ func (g *metadataGraph) build() {
129129
}
130130
}
131131

132+
// uniqueDeps returns a new sorted and duplicate-free slice containing the
133+
// IDs of the package's direct dependencies.
134+
func uniqueDeps(imports map[ImportPath]PackageID) []PackageID {
135+
// TODO(adonovan): use generic maps.SortedUniqueValues(m.Imports) when available.
136+
ids := make([]PackageID, 0, len(imports))
137+
for _, id := range imports {
138+
ids = append(ids, id)
139+
}
140+
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
141+
// de-duplicate in place
142+
out := ids[:0]
143+
for _, id := range ids {
144+
if len(out) == 0 || id != out[len(out)-1] {
145+
out = append(out, id)
146+
}
147+
}
148+
return out
149+
}
150+
132151
// reverseTransitiveClosure calculates the set of packages that transitively
133-
// reach an id in ids via their Deps. The result also includes given ids.
152+
// import an id in ids. The result also includes given ids.
134153
//
135154
// If includeInvalid is false, the algorithm ignores packages with invalid
136155
// metadata (including those in the given list of ids).

gopls/internal/lsp/cache/load.go

+54-19
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadSc
207207
if s.view.allFilesExcluded(pkg, filterer) {
208208
continue
209209
}
210-
if err := buildMetadata(ctx, PackagePath(pkg.PkgPath), pkg, cfg, query, newMetadata, nil); err != nil {
210+
if err := buildMetadata(ctx, pkg, cfg, query, newMetadata, nil); err != nil {
211211
return err
212212
}
213213
}
@@ -476,7 +476,9 @@ func makeWorkspaceDir(ctx context.Context, workspace *workspace, fs source.FileS
476476
// buildMetadata populates the updates map with metadata updates to
477477
// apply, based on the given pkg. It recurs through pkg.Imports to ensure that
478478
// metadata exists for all dependencies.
479-
func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error {
479+
func buildMetadata(ctx context.Context, pkg *packages.Package, cfg *packages.Config, query []string, updates map[PackageID]*KnownMetadata, path []PackageID) error {
480+
// Allow for multiple ad-hoc packages in the workspace (see #47584).
481+
pkgPath := PackagePath(pkg.PkgPath)
480482
id := PackageID(pkg.ID)
481483
if source.IsCommandLineArguments(pkg.ID) {
482484
suffix := ":" + strings.Join(query, ",")
@@ -540,33 +542,66 @@ func buildMetadata(ctx context.Context, pkgPath PackagePath, pkg *packages.Packa
540542
m.GoFiles = append(m.GoFiles, uri)
541543
}
542544

543-
for importPath, importPkg := range pkg.Imports {
544-
// TODO(rfindley): in rare cases it is possible that the import package
545-
// path is not the same as the package path of the import. That is to say
546-
// (quoting adonovan):
547-
// "The importPath string is the path by which one package is imported from
548-
// another, but that needn't be the same as its internal name (sometimes
549-
// called the "package path") used to prefix its linker symbols"
550-
//
551-
// We should not set this package path on the metadata of the dep.
552-
importPkgPath := PackagePath(importPath)
553-
importID := PackageID(importPkg.ID)
545+
imports := make(map[ImportPath]PackageID)
546+
for importPath, imported := range pkg.Imports {
547+
importPath := ImportPath(importPath)
548+
imports[importPath] = PackageID(imported.ID)
554549

555-
m.Deps = append(m.Deps, importID)
550+
// It is not an invariant that importPath == imported.PkgPath.
551+
// For example, package "net" imports "golang.org/x/net/dns/dnsmessage"
552+
// which refers to the package whose ID and PkgPath are both
553+
// "vendor/golang.org/x/net/dns/dnsmessage". Notice the ImportMap,
554+
// which maps ImportPaths to PackagePaths:
555+
//
556+
// $ go list -json net vendor/golang.org/x/net/dns/dnsmessage
557+
// {
558+
// "ImportPath": "net",
559+
// "Name": "net",
560+
// "Imports": [
561+
// "C",
562+
// "vendor/golang.org/x/net/dns/dnsmessage",
563+
// "vendor/golang.org/x/net/route",
564+
// ...
565+
// ],
566+
// "ImportMap": {
567+
// "golang.org/x/net/dns/dnsmessage": "vendor/golang.org/x/net/dns/dnsmessage",
568+
// "golang.org/x/net/route": "vendor/golang.org/x/net/route"
569+
// },
570+
// ...
571+
// }
572+
// {
573+
// "ImportPath": "vendor/golang.org/x/net/dns/dnsmessage",
574+
// "Name": "dnsmessage",
575+
// ...
576+
// }
577+
//
578+
// (Beware that, for historical reasons, go list uses
579+
// the JSON field "ImportPath" for the package's
580+
// path--effectively the linker symbol prefix.)
556581

557582
// Don't remember any imports with significant errors.
558-
if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
583+
//
584+
// The len=0 condition is a heuristic check for imports of
585+
// non-existent packages (for which go/packages will create
586+
// an edge to a synthesized node). The heuristic is unsound
587+
// because some valid packages have zero files, for example,
588+
// a directory containing only the file p_test.go defines an
589+
// empty package p.
590+
// TODO(adonovan): clarify this. Perhaps go/packages should
591+
// report which nodes were synthesized.
592+
if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 {
559593
if m.MissingDeps == nil {
560-
m.MissingDeps = make(map[PackagePath]struct{})
594+
m.MissingDeps = make(map[ImportPath]struct{})
561595
}
562-
m.MissingDeps[importPkgPath] = struct{}{}
596+
m.MissingDeps[importPath] = struct{}{}
563597
continue
564598
}
565-
if err := buildMetadata(ctx, importPkgPath, importPkg, cfg, query, updates, append(path, id)); err != nil {
599+
600+
if err := buildMetadata(ctx, imported, cfg, query, updates, append(path, id)); err != nil {
566601
event.Error(ctx, "error in dependency", err)
567602
}
568603
}
569-
sort.Slice(m.Deps, func(i, j int) bool { return m.Deps[i] < m.Deps[j] }) // for determinism
604+
m.Imports = imports
570605

571606
return nil
572607
}

gopls/internal/lsp/cache/metadata.go

+6-5
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import (
1717
// it would result in confusing errors because package IDs often look like
1818
// package paths.
1919
type (
20-
PackageID string
21-
PackagePath string
22-
PackageName string
20+
PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]")
21+
PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo")
22+
PackageName string // identifier in 'package' declaration (e.g. "foo")
23+
ImportPath string // path that appears in an import declaration (e.g. "example.com/foo")
2324
)
2425

2526
// Metadata holds package Metadata extracted from a call to packages.Load.
@@ -32,8 +33,8 @@ type Metadata struct {
3233
ForTest PackagePath // package path under test, or ""
3334
TypesSizes types.Sizes
3435
Errors []packages.Error
35-
Deps []PackageID // direct dependencies, in string order
36-
MissingDeps map[PackagePath]struct{}
36+
Imports map[ImportPath]PackageID // may contain duplicate IDs
37+
MissingDeps map[ImportPath]struct{}
3738
Module *packages.Module
3839
depsErrors []*packagesinternal.PackageError
3940

gopls/internal/lsp/cache/mod_tidy.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -466,9 +466,8 @@ func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Sp
466466
// the set of strings that appear in import declarations within
467467
// GoFiles. Errors are ignored.
468468
//
469-
// (We can't simply use ph.m.Metadata.Deps because it contains
470-
// PackageIDs--not import paths--and is based on CompiledGoFiles,
471-
// after cgo processing.)
469+
// (We can't simply use Metadata.Imports because it is based on
470+
// CompiledGoFiles, after cgo processing.)
472471
func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool {
473472
s.mu.Lock() // peekOrParse requires a locked snapshot (!)
474473
defer s.mu.Unlock()

0 commit comments

Comments
 (0)