internal/golang: add a fast path for FormatVarType with gopls at 1.23

Now that gopls is only built with Go 1.23, we can rely on
types.TypeString to preserve alias information, and only need the
bespoke logic of FormatVarType to handle the cases where types contain
an invalid type.

This should improve performance for completion, particularly since it
affects the single-threaded construction of candidates.

For example, here is the impact on a relevant benchmark:
Completion/kubernetes_selector/edit=false/unimported=false/budget=100ms

Results:
│ before.txt  │              after.txt              │
│   sec/op    │   sec/op     vs base                │
  81.29m ± 1%   65.83m ± 1%  -19.02% (p=0.000 n=10)

│   before.txt   │               after.txt                │
│ cpu_seconds/op │ cpu_seconds/op  vs base                │
  151.8m ± 15%     101.1m ± 33%  -33.36% (p=0.000 n=10)

Change-Id: I60d890ca102a97cf6b198621ba82afe7eeab7fb9
Reviewed-on: https://go-review.googlesource.com/c/tools/+/611836
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
Rob Findley 2024-09-09 14:43:22 +00:00 коммит произвёл Gopher Robot
Родитель 7398f36f57
Коммит 4bcf6a3b56
3 изменённых файлов: 30 добавлений и 8 удалений

Просмотреть файл

@ -214,6 +214,9 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si
if err != nil {
return nil, err
}
if sig.Variadic() && i == sig.Params().Len()-1 {
typ = strings.Replace(typ, "[]", "...", 1)
}
p := typ
if el.Name() != "" {
p = el.Name() + " " + typ
@ -261,6 +264,10 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si
}, nil
}
// We look for 'invalidTypeString' to determine if we can use the fast path for
// FormatVarType.
var invalidTypeString = types.Typ[types.Invalid].String()
// FormatVarType formats a *types.Var, accounting for type aliases.
// To do this, it looks in the AST of the file in which the object is declared.
// On any errors, it always falls back to types.TypeString.
@ -268,6 +275,21 @@ func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, si
// TODO(rfindley): this function could return the actual name used in syntax,
// for better parameter names.
func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) {
typeString := types.TypeString(obj.Type(), qf)
// Fast path: if the type string does not contain 'invalid type', we no
// longer need to do any special handling, thanks to materialized aliases in
// Go 1.23+.
//
// Unfortunately, due to the handling of invalid types, we can't quite delete
// the rather complicated preexisting logic of FormatVarType--it isn't an
// acceptable regression to start printing "invalid type" in completion or
// signature help. strings.Contains is conservative: the type string of a
// valid type may actually contain "invalid type" (due to struct tags or
// field formatting), but such cases should be exceedingly rare.
if !strings.Contains(typeString, invalidTypeString) {
return typeString, nil
}
// TODO(rfindley): This looks wrong. The previous comment said:
// "If the given expr refers to a type parameter, then use the
// object's Type instead of the type parameter declaration. This helps
@ -280,13 +302,13 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.
//
// Left this during refactoring in order to preserve pre-existing logic.
if typeparams.IsTypeParam(obj.Type()) {
return types.TypeString(obj.Type(), qf), nil
return typeString, nil
}
if isBuiltin(obj) {
// This is defensive, though it is extremely unlikely we'll ever have a
// builtin var.
return types.TypeString(obj.Type(), qf), nil
return typeString, nil
}
// TODO(rfindley): parsing to produce candidates can be costly; consider
@ -309,7 +331,7 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.
// for parameterized decls.
if decl, _ := decl.(*ast.FuncDecl); decl != nil {
if decl.Type.TypeParams.NumFields() > 0 {
return types.TypeString(obj.Type(), qf), nil // in generic function
return typeString, nil // in generic function
}
if decl.Recv != nil && len(decl.Recv.List) > 0 {
rtype := decl.Recv.List[0].Type
@ -317,18 +339,18 @@ func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.
rtype = e.X
}
if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil {
return types.TypeString(obj.Type(), qf), nil // in method of generic type
return typeString, nil // in method of generic type
}
}
}
if spec, _ := spec.(*ast.TypeSpec); spec != nil && spec.TypeParams.NumFields() > 0 {
return types.TypeString(obj.Type(), qf), nil // in generic type decl
return typeString, nil // in generic type decl
}
if field == nil {
// TODO(rfindley): we should never reach here from an ordinary var, so
// should probably return an error here.
return types.TypeString(obj.Type(), qf), nil
return typeString, nil
}
expr := field.Type

Просмотреть файл

@ -1412,7 +1412,7 @@ func snippetMarker(mark marker, src protocol.Location, label completionLabel, wa
return
}
if got != want {
mark.errorf("snippets do not match: got %q, want %q", got, want)
mark.errorf("snippets do not match: got:\n%q\nwant:\n%q", got, want)
}
}

Просмотреть файл

@ -476,7 +476,7 @@ func _() {
const two = 2
var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int)
builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}")
builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [2]bool, m map[string]string, s struct{i int\\}, i2 interface{foo()\\}, c <-chan int) {$0\\}")
var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}")
var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}")