blob: 49ede162b3a9c845d30cfa80617caf00916feb81 [file] [log] [blame]
// 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"
"sort"
"strings"
"sync"
"time"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/imports"
errors "golang.org/x/xerrors"
)
// KnownPackages returns a list of all known packages
// in the package graph that could potentially be imported
// by the given file.
func KnownPackages(ctx context.Context, snapshot Snapshot, fh VersionedFileHandle) ([]string, error) {
pkg, pgf, err := GetParsedFile(ctx, snapshot, fh, NarrowestPackage)
if err != nil {
return nil, errors.Errorf("GetParsedFile: %w", err)
}
alreadyImported := map[string]struct{}{}
for _, imp := range pgf.File.Imports {
alreadyImported[imp.Path.Value] = struct{}{}
}
pkgs, err := snapshot.CachedImportPaths(ctx)
if err != nil {
return nil, err
}
var (
seen = make(map[string]struct{})
paths []string
)
for path, knownPkg := range pkgs {
gofiles := knownPkg.CompiledGoFiles()
if len(gofiles) == 0 || gofiles[0].File.Name == nil {
continue
}
pkgName := gofiles[0].File.Name.Name
// package main cannot be imported
if pkgName == "main" {
continue
}
// test packages cannot be imported
if knownPkg.ForTest() != "" {
continue
}
// no need to import what the file already imports
if _, ok := alreadyImported[path]; ok {
continue
}
// snapshot.KnownPackages could have multiple versions of a pkg
if _, ok := seen[path]; ok {
continue
}
seen[path] = struct{}{}
// make sure internal packages are importable by the file
if !IsValidImport(pkg.PkgPath(), path) {
continue
}
// naive check on cyclical imports
if isDirectlyCyclical(pkg, knownPkg) {
continue
}
paths = append(paths, path)
seen[path] = struct{}{}
}
err = snapshot.RunProcessEnvFunc(ctx, func(o *imports.Options) error {
var mu sync.Mutex
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*80)
defer cancel()
return imports.GetAllCandidates(ctx, func(ifix imports.ImportFix) {
mu.Lock()
defer mu.Unlock()
if _, ok := seen[ifix.StmtInfo.ImportPath]; ok {
return
}
paths = append(paths, ifix.StmtInfo.ImportPath)
}, "", pgf.URI.Filename(), pkg.GetTypes().Name(), o.Env)
})
if err != nil {
// if an error occurred, we stil have a decent list we can
// show to the user through snapshot.CachedImportPaths
event.Error(ctx, "imports.GetAllCandidates", err)
}
sort.Slice(paths, func(i, j int) bool {
importI, importJ := paths[i], paths[j]
iHasDot := strings.Contains(importI, ".")
jHasDot := strings.Contains(importJ, ".")
if iHasDot && !jHasDot {
return false
}
if jHasDot && !iHasDot {
return true
}
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.
func isDirectlyCyclical(pkg, imported Package) bool {
for _, imp := range imported.Imports() {
if imp.PkgPath() == pkg.PkgPath() {
return true
}
}
return false
}