1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140
|
// Copyright 2020 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 source
import (
"context"
"fmt"
"go/parser"
"go/token"
"sort"
"strings"
"sync"
"time"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
)
// KnownPackagePaths returns a new list of package paths of all known
// packages in the package graph that could potentially be imported by
// the given file. The list is ordered lexicographically, except that
// all dot-free paths (standard packages) appear before dotful ones.
//
// It is part of the gopls.list_known_packages command.
func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]PackagePath, error) {
// This algorithm is expressed in terms of Metadata, not Packages,
// so it doesn't cause or wait for type checking.
// Find a Metadata containing the file.
metas, err := snapshot.MetadataForFile(ctx, fh.URI())
if err != nil {
return nil, err // e.g. context cancelled
}
if len(metas) == 0 {
return nil, fmt.Errorf("no loaded package contain file %s", fh.URI())
}
current := metas[0] // pick one arbitrarily (they should all have the same package path)
// Parse the file's imports so we can compute which
// PackagePaths are imported by this specific file.
src, err := fh.Read()
if err != nil {
return nil, err
}
file, err := parser.ParseFile(token.NewFileSet(), fh.URI().Filename(), src, parser.ImportsOnly)
if err != nil {
return nil, err
}
imported := make(map[PackagePath]bool)
for _, imp := range file.Imports {
if id := current.DepsByImpPath[UnquoteImportPath(imp)]; id != "" {
if m := snapshot.Metadata(id); m != nil {
imported[m.PkgPath] = true
}
}
}
// Now find candidates among known packages.
knownPkgs, err := snapshot.AllMetadata(ctx)
if err != nil {
return nil, err
}
seen := make(map[PackagePath]bool)
for _, knownPkg := range knownPkgs {
// package main cannot be imported
if knownPkg.Name == "main" {
continue
}
// test packages cannot be imported
if knownPkg.ForTest != "" {
continue
}
// No need to import what the file already imports.
// This check is based on PackagePath, not PackageID,
// so that all test variants are filtered out too.
if imported[knownPkg.PkgPath] {
continue
}
// make sure internal packages are importable by the file
if !IsValidImport(current.PkgPath, knownPkg.PkgPath) {
continue
}
// naive check on cyclical imports
if isDirectlyCyclical(current, knownPkg) {
continue
}
// AllMetadata may have multiple variants of a pkg.
seen[knownPkg.PkgPath] = true
}
// Augment the set by invoking the goimports algorithm.
if err := snapshot.RunProcessEnvFunc(ctx, func(o *imports.Options) error {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*80)
defer cancel()
var seenMu sync.Mutex
wrapped := func(ifix imports.ImportFix) {
seenMu.Lock()
defer seenMu.Unlock()
// TODO(adonovan): what if the actual package path has a vendor/ prefix?
seen[PackagePath(ifix.StmtInfo.ImportPath)] = true
}
return imports.GetAllCandidates(ctx, wrapped, "", fh.URI().Filename(), string(current.Name), o.Env)
}); err != nil {
// If goimports failed, proceed with just the candidates from the metadata.
event.Error(ctx, "imports.GetAllCandidates", err)
}
// Sort lexicographically, but with std before non-std packages.
paths := make([]PackagePath, 0, len(seen))
for path := range seen {
paths = append(paths, path)
}
sort.Slice(paths, func(i, j int) bool {
importI, importJ := paths[i], paths[j]
iHasDot := strings.Contains(string(importI), ".")
jHasDot := strings.Contains(string(importJ), ".")
if iHasDot != jHasDot {
return jHasDot // dot-free paths (standard packages) compare less
}
return importI < importJ
})
return paths, nil
}
// isDirectlyCyclical checks if imported directly imports pkg.
// It does not (yet) offer a full cyclical check because showing a user
// a list of importable packages already generates a very large list
// and having a few false positives in there could be worth the
// performance snappiness.
//
// TODO(adonovan): ensure that metadata graph is always cyclic!
// Many algorithms will get confused or even stuck in the
// presence of cycles. Then replace this function by 'false'.
func isDirectlyCyclical(pkg, imported *Metadata) bool {
_, ok := imported.DepsByPkgPath[pkg.PkgPath]
return ok
}
|