blob: 53350ef4b85df0ee1404aa4df68e88addc212656 [file] [log] [blame] [edit]
// Copyright 2025 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 completion
// unimported completion is invoked when the user types something like 'foo.xx',
// foo is known to be a package name not yet imported in the current file, and
// xx (or whatever the user has typed) is interpreted as a hint (pattern) for the
// member of foo that the user is looking for.
//
// This code looks for a suitable completion in a number of places. A 'suitable
// completion' is an exported symbol (so a type, const, var, or func) from package
// foo, which, after converting everything to lower case, has the pattern as a
// subsequence.
//
// The code looks for a suitable completion in
// 1. the imports of some other file of the current package,
// 2. the standard library,
// 3. the imports of some other file in the current workspace,
// 4. imports in the current module with 'foo' as the explicit package name,
// 5. the module cache,
// It stops at the first success.
import (
"context"
"fmt"
"go/ast"
"go/printer"
"go/token"
"path"
"slices"
"strings"
"golang.org/x/tools/gopls/internal/cache/metadata"
"golang.org/x/tools/gopls/internal/golang/completion/snippet"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/gopls/internal/util/bug"
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/modindex"
"golang.org/x/tools/internal/stdlib"
"golang.org/x/tools/internal/versions"
)
func (c *completer) unimported(ctx context.Context, pkgname metadata.PackageName, prefix string) {
wsIDs, ourIDs := c.findPackageIDs(pkgname)
stdpkgs := c.stdlibPkgs(pkgname)
if len(ourIDs) > 0 {
// use the one in the current package, if possible
items := c.pkgIDmatches(ctx, ourIDs, pkgname, prefix)
if c.scoreList(items) {
return
}
}
// do the stdlib next.
items := c.stdlibMatches(stdpkgs, pkgname, prefix)
if c.scoreList(items) {
return
}
// look in the rest of the workspace
items = c.pkgIDmatches(ctx, wsIDs, pkgname, prefix)
if c.scoreList(items) {
return
}
// before looking in the module cache, maybe it is an explicit
// package name used in this module
if c.explicitPkgName(ctx, pkgname, prefix) {
return
}
// look in the module cache
items, err := c.modcacheMatches(pkgname, prefix)
items = c.filterGoMod(ctx, items)
if err == nil && c.scoreList(items) {
return
}
// out of things to do
}
// prefer completion items that are referenced in the go.mod file
func (c *completer) filterGoMod(ctx context.Context, items []CompletionItem) []CompletionItem {
if c.pkg.Metadata().Module == nil {
// for std or GOROOT mode
return items
}
gomod := c.pkg.Metadata().Module.GoMod
uri := protocol.URIFromPath(gomod)
fh, err := c.snapshot.ReadFile(ctx, uri)
if err != nil {
return items
}
pm, err := c.snapshot.ParseMod(ctx, fh)
if err != nil || pm == nil {
return items
}
// if any of the items match any of the req, just return those
reqnames := []string{}
for _, req := range pm.File.Require {
reqnames = append(reqnames, req.Mod.Path)
}
better := []CompletionItem{}
for _, compl := range items {
if len(compl.AdditionalTextEdits) == 0 {
continue
}
// import "foof/pkg"
flds := strings.FieldsFunc(compl.AdditionalTextEdits[0].NewText, func(r rune) bool {
return r == '"' || r == '/'
})
if len(flds) < 3 {
continue
}
if slices.Contains(reqnames, flds[1]) {
better = append(better, compl)
}
}
if len(better) > 0 {
return better
}
return items
}
// see if some file in the current package satisfied a foo. import
// because foo is an explicit package name (import foo "a.b.c")
func (c *completer) explicitPkgName(ctx context.Context, pkgname metadata.PackageName, prefix string) bool {
for _, pgf := range c.pkg.CompiledGoFiles() {
imports := pgf.File.Imports
for _, imp := range imports {
if imp.Name != nil && imp.Name.Name == string(pkgname) {
path := strings.Trim(imp.Path.Value, `"`)
if c.tryPath(ctx, metadata.PackagePath(path), string(pkgname), prefix) {
return true // one is enough
}
}
}
}
return false
}
// see if this path contains a usable import with explict package name
func (c *completer) tryPath(ctx context.Context, path metadata.PackagePath, pkgname, prefix string) bool {
packages := c.snapshot.MetadataGraph().ForPackagePath
ids := []metadata.PackageID{}
for _, pkg := range packages[path] { // could there ever be more than one?
ids = append(ids, pkg.ID) // pkg.ID. ID: "math/rand" but Name: "rand"
}
items := c.pkgIDmatches(ctx, ids, metadata.PackageName(pkgname), prefix)
return c.scoreList(items)
}
// find all the packageIDs for packages in the workspace that have the desired name
// thisPkgIDs contains the ones known to the current package, wsIDs contains the others
func (c *completer) findPackageIDs(pkgname metadata.PackageName) (wsIDs, thisPkgIDs []metadata.PackageID) {
g := c.snapshot.MetadataGraph()
for pid, pkg := range c.snapshot.MetadataGraph().Packages {
if pkg.Name != pkgname {
continue
}
imports := g.ImportedBy[pid]
// Metadata is not canonical: it may be held onto by a package. Therefore,
// we must compare by ID.
thisPkg := func(mp *metadata.Package) bool { return mp.ID == c.pkg.Metadata().ID }
if slices.ContainsFunc(imports, thisPkg) {
thisPkgIDs = append(thisPkgIDs, pid)
} else {
wsIDs = append(wsIDs, pid)
}
}
return
}
// find all the stdlib packages that have the desired name
func (c *completer) stdlibPkgs(pkgname metadata.PackageName) []metadata.PackagePath {
var pkgs []metadata.PackagePath // stlib packages that match pkg
for pkgpath := range stdlib.PackageSymbols {
v := metadata.PackageName(path.Base(pkgpath))
if v == pkgname {
pkgs = append(pkgs, metadata.PackagePath(pkgpath))
} else if imports.WithoutVersion(string(pkgpath)) == string(pkgname) {
pkgs = append(pkgs, metadata.PackagePath(pkgpath))
}
}
return pkgs
}
// return CompletionItems for all matching symbols in the packages in ids.
func (c *completer) pkgIDmatches(ctx context.Context, ids []metadata.PackageID, pkgname metadata.PackageName, prefix string) []CompletionItem {
pattern := strings.ToLower(prefix)
allpkgsyms, err := c.snapshot.Symbols(ctx, ids...)
if err != nil {
return nil // would if be worth retrying the ids one by one?
}
if len(allpkgsyms) != len(ids) {
bug.Reportf("Symbols returned %d values for %d pkgIDs", len(allpkgsyms), len(ids))
return nil
}
var got []CompletionItem
for i, pkgID := range ids {
pkg := c.snapshot.MetadataGraph().Packages[pkgID]
if pkg == nil {
bug.Reportf("no metadata for %s", pkgID)
continue // something changed underfoot, otherwise can't happen
}
pkgsyms := allpkgsyms[i]
pkgfname := pkgsyms.Files[0].Path()
if !imports.CanUse(c.filename, pkgfname) {
// avoid unusable internal, etc
continue
}
// are any of these any good?
for np, asym := range pkgsyms.Symbols {
for _, sym := range asym {
if !token.IsExported(sym.Name) {
continue
}
if !usefulCompletion(sym.Name, pattern) {
// for json.U, the existing code finds InvalidUTF8Error
continue
}
var params []string
var kind protocol.CompletionItemKind
var detail string
switch sym.Kind {
case protocol.Function:
foundURI := pkgsyms.Files[np]
fh, _ := c.snapshot.ReadFile(ctx, foundURI)
pgf, err := c.snapshot.ParseGo(ctx, fh, 0)
if err == nil {
params = funcParams(pgf.File, sym.Name)
}
kind = protocol.FunctionCompletion
detail = fmt.Sprintf("func (from %q)", pkg.PkgPath)
case protocol.Variable, protocol.Struct:
kind = protocol.VariableCompletion
detail = fmt.Sprintf("var (from %q)", pkg.PkgPath)
case protocol.Constant:
kind = protocol.ConstantCompletion
detail = fmt.Sprintf("const (from %q)", pkg.PkgPath)
default:
continue
}
got = c.appendNewItem(got, sym.Name,
detail,
pkg.PkgPath,
kind,
pkgname, params)
}
}
}
return got
}
// return CompletionItems for all the matches in packages in pkgs.
func (c *completer) stdlibMatches(pkgs []metadata.PackagePath, pkg metadata.PackageName, prefix string) []CompletionItem {
// check for deprecated symbols someday
got := make([]CompletionItem, 0)
pattern := strings.ToLower(prefix)
// avoid non-determinacy, especially for marker tests
slices.Sort(pkgs)
for _, candpkg := range pkgs {
if std, ok := stdlib.PackageSymbols[string(candpkg)]; ok {
for _, sym := range std {
if !usefulCompletion(sym.Name, pattern) {
continue
}
if !versions.AtLeast(c.goversion, sym.Version.String()) {
continue
}
var kind protocol.CompletionItemKind
var detail string
var params []string
switch sym.Kind {
case stdlib.Func:
params = parseSignature(sym.Signature)
kind = protocol.FunctionCompletion
detail = fmt.Sprintf("func (from %q)", candpkg)
case stdlib.Const:
kind = protocol.ConstantCompletion
detail = fmt.Sprintf("const (from %q)", candpkg)
case stdlib.Var:
kind = protocol.VariableCompletion
detail = fmt.Sprintf("var (from %q)", candpkg)
case stdlib.Type:
kind = protocol.VariableCompletion
detail = fmt.Sprintf("type (from %q)", candpkg)
default:
continue
}
got = c.appendNewItem(got, sym.Name,
detail,
candpkg,
kind,
pkg, params)
}
}
}
return got
}
func (c *completer) modcacheMatches(pkg metadata.PackageName, prefix string) ([]CompletionItem, error) {
ix, err := c.snapshot.View().ModcacheIndex()
if err != nil {
return nil, err
}
// retrieve everything and let usefulCompletion() and the matcher sort them out
cands := ix.Lookup(string(pkg), "", true)
lx := len(cands)
got := make([]CompletionItem, 0, lx)
pattern := strings.ToLower(prefix)
for _, cand := range cands {
if !usefulCompletion(cand.Name, pattern) {
continue
}
var params []string
var kind protocol.CompletionItemKind
var detail string
switch cand.Type {
case modindex.Func:
for _, f := range cand.Sig {
params = append(params, fmt.Sprintf("%s %s", f.Arg, f.Type))
}
kind = protocol.FunctionCompletion
detail = fmt.Sprintf("func (from %s)", cand.ImportPath)
case modindex.Var:
kind = protocol.VariableCompletion
detail = fmt.Sprintf("var (from %s)", cand.ImportPath)
case modindex.Const:
kind = protocol.ConstantCompletion
detail = fmt.Sprintf("const (from %s)", cand.ImportPath)
case modindex.Type: // might be a type alias
kind = protocol.VariableCompletion
detail = fmt.Sprintf("type (from %s)", cand.ImportPath)
default:
continue
}
got = c.appendNewItem(got, cand.Name,
detail,
metadata.PackagePath(cand.ImportPath),
kind,
pkg, params)
}
return got, nil
}
func (c *completer) appendNewItem(got []CompletionItem, name, detail string, path metadata.PackagePath, kind protocol.CompletionItemKind, pkg metadata.PackageName, params []string) []CompletionItem {
item := CompletionItem{
Label: name,
Detail: detail,
InsertText: name,
Kind: kind,
}
imp := importInfo{
importPath: string(path),
name: string(pkg),
}
if imports.ImportPathToAssumedName(string(path)) == string(pkg) {
imp.name = ""
}
item.AdditionalTextEdits, _ = c.importEdits(&imp)
if params != nil {
var sn snippet.Builder
c.functionCallSnippet(name, nil, params, &sn)
item.snippet = &sn
}
got = append(got, item)
return got
}
// score the list. Return true if any item is added to c.items
func (c *completer) scoreList(items []CompletionItem) bool {
ret := false
for _, item := range items {
item.Score = float64(c.matcher.Score(item.Label))
if item.Score > 0 {
c.items = append(c.items, item)
ret = true
}
}
return ret
}
// pattern is always the result of strings.ToLower
func usefulCompletion(name, pattern string) bool {
// this travesty comes from foo.(type) somehow. see issue59096.txt
if pattern == "_" {
return true
}
// convert both to lower case, and then the runes in the pattern have to occur, in order,
// in the name
cand := strings.ToLower(name)
for _, r := range pattern {
ix := strings.IndexRune(cand, r)
if ix < 0 {
return false
}
cand = cand[ix+1:]
}
return true
}
// return a printed version of the function arguments for snippets
func funcParams(f *ast.File, fname string) []string {
var params []string
setParams := func(list *ast.FieldList) {
if list == nil {
return
}
var cfg printer.Config // slight overkill
param := func(name string, typ ast.Expr) {
var buf strings.Builder
buf.WriteString(name)
buf.WriteByte(' ')
cfg.Fprint(&buf, token.NewFileSet(), typ) // ignore error
params = append(params, buf.String())
}
for _, field := range list.List {
if field.Names != nil {
for _, name := range field.Names {
param(name.Name, field.Type)
}
} else {
param("_", field.Type)
}
}
}
for _, n := range f.Decls {
switch x := n.(type) {
case *ast.FuncDecl:
if x.Recv == nil && x.Name.Name == fname {
setParams(x.Type.Params)
}
}
}
return params
}
// extract the formal parameters from the signature.
// func[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2) -> []{"dst M1", "src M2"}
// func[K comparable, V any](seq iter.Seq2[K, V]) map[K]V -> []{"seq iter.Seq2[K, V]"}
// func(args ...any) *Logger -> []{"args ...any"}
// func[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool) -> []{"m M", "del func(K, V) bool"}
func parseSignature(sig string) []string {
var level int // nesting level of delimiters
var processing bool // are we doing the params
var last int // start of current parameter
var params []string
for i := range len(sig) {
switch sig[i] {
case '[', '{':
level++
case ']', '}':
level--
case '(':
level++
if level == 1 {
processing = true
last = i + 1
}
case ')':
level--
if level == 0 && processing { // done
if i > last {
params = append(params, strings.TrimSpace(sig[last:i]))
}
return params
}
case ',':
if level == 1 && processing {
params = append(params, strings.TrimSpace(sig[last:i]))
last = i + 1
}
}
}
return nil
}