golang.org/x/tools/gopls@v0.15.3/internal/golang/known_packages.go (about)

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