internal/lsp/source: add inferred types to generic function hover
As an experiment, this CL introduces the first gopls feature that is
specific to generics: enriching function hover information with inferred
types. This is done with no additional gating on build constraints by
using the new internal/typeparams package.
The marker tests are updated to allow tests that rely on type parameters
being enabled.
Change-Id: Ic627d64b61a6211389196814edd0abe1484491eb
Reviewed-on: https://go-review.googlesource.com/c/tools/+/317452
Trust: Robert Findley <rfindley@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Go Bot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/cache/check.go b/internal/lsp/cache/check.go
index d7447b2..6bd4135 100644
--- a/internal/lsp/cache/check.go
+++ b/internal/lsp/cache/check.go
@@ -26,6 +26,7 @@
"golang.org/x/tools/internal/memoize"
"golang.org/x/tools/internal/packagesinternal"
"golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/internal/typesinternal"
errors "golang.org/x/xerrors"
)
@@ -343,7 +344,6 @@
}
}
}
-
// If this is a replaced module in the workspace, the version is
// meaningless, and we don't want clients to access it.
if m.module != nil {
@@ -447,6 +447,7 @@
},
typesSizes: m.typesSizes,
}
+ typeparams.InitInferred(pkg.typesInfo)
for _, gf := range pkg.m.goFiles {
// In the presence of line directives, we may need to report errors in
diff --git a/internal/lsp/source/hover.go b/internal/lsp/source/hover.go
index ee38dd7..be2bfe2 100644
--- a/internal/lsp/source/hover.go
+++ b/internal/lsp/source/hover.go
@@ -19,6 +19,7 @@
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
+ "golang.org/x/tools/internal/typeparams"
errors "golang.org/x/xerrors"
)
@@ -125,10 +126,10 @@
break
}
}
- h.Signature = objectString(x, i.qf)
+ h.Signature = objectString(x, i.qf, i.Inferred)
}
if obj := i.Declaration.obj; obj != nil {
- h.SingleLine = objectString(obj, i.qf)
+ h.SingleLine = objectString(obj, i.qf, nil)
}
obj := i.Declaration.obj
if obj == nil {
@@ -237,7 +238,21 @@
// objectString is a wrapper around the types.ObjectString function.
// It handles adding more information to the object string.
-func objectString(obj types.Object, qf types.Qualifier) string {
+func objectString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string {
+ // If the signature type was inferred, prefer the preferred signature with a
+ // comment showing the generic signature.
+ if sig, _ := obj.Type().(*types.Signature); sig != nil && len(typeparams.ForSignature(sig)) > 0 && inferred != nil {
+ obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred)
+ str := types.ObjectString(obj2, qf)
+ // Try to avoid overly long lines.
+ if len(str) > 60 {
+ str += "\n"
+ } else {
+ str += " "
+ }
+ str += "// " + types.TypeString(sig, qf)
+ return str
+ }
str := types.ObjectString(obj, qf)
switch obj := obj.(type) {
case *types.Const:
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go
index 9fb3daa..ff86eaa 100644
--- a/internal/lsp/source/identifier.go
+++ b/internal/lsp/source/identifier.go
@@ -18,6 +18,7 @@
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/lsp/protocol"
"golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/typeparams"
errors "golang.org/x/xerrors"
)
@@ -32,6 +33,8 @@
Object types.Object
}
+ Inferred *types.Signature
+
Declaration Declaration
ident *ast.Ident
@@ -292,6 +295,8 @@
return result, nil
}
+ result.Inferred = inferredSignature(pkg.GetTypesInfo(), path)
+
result.Type.Object = typeToObject(typ)
if result.Type.Object != nil {
// Identifiers with the type "error" are a special case with no position.
@@ -337,6 +342,52 @@
return nil, nil
}
+// inferredSignature determines the resolved non-generic signature for an
+// identifier with a generic signature that is the operand of an IndexExpr or
+// CallExpr.
+//
+// If no such signature exists, it returns nil.
+func inferredSignature(info *types.Info, path []ast.Node) *types.Signature {
+ if len(path) < 2 {
+ return nil
+ }
+ // There are four ways in which a signature may be resolved:
+ // 1. It has no explicit type arguments, but the CallExpr can be fully
+ // inferred from function arguments.
+ // 2. It has full type arguments, and the IndexExpr has a non-generic type.
+ // 3. For a partially instantiated IndexExpr representing a function-valued
+ // expression (i.e. not part of a CallExpr), type arguments may be
+ // inferred using constraint type inference.
+ // 4. For a partially instantiated IndexExpr that is part of a CallExpr,
+ // type arguments may be inferred using both constraint type inference
+ // and function argument inference.
+ //
+ // These branches are handled below.
+ switch n := path[1].(type) {
+ case *ast.CallExpr:
+ _, sig := typeparams.GetInferred(info, n)
+ return sig
+ case *ast.IndexExpr:
+ // If the IndexExpr is fully instantiated, we consider that 'inference' for
+ // gopls' purposes.
+ sig, _ := info.TypeOf(n).(*types.Signature)
+ if sig != nil && len(typeparams.ForSignature(sig)) == 0 {
+ return sig
+ }
+ _, sig = typeparams.GetInferred(info, n)
+ if sig != nil {
+ return sig
+ }
+ if len(path) >= 2 {
+ if call, _ := path[2].(*ast.CallExpr); call != nil {
+ _, sig := typeparams.GetInferred(info, call)
+ return sig
+ }
+ }
+ }
+ return nil
+}
+
func searchForEnclosing(info *types.Info, path []ast.Node) types.Type {
for _, n := range path {
switch n := n.(type) {
diff --git a/internal/lsp/testdata/godef/a/a.go.golden b/internal/lsp/testdata/godef/a/a.go.golden
index 2f7d8de..c268293 100644
--- a/internal/lsp/testdata/godef/a/a.go.golden
+++ b/internal/lsp/testdata/godef/a/a.go.golden
@@ -76,6 +76,42 @@
[`a.Random2` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#Random2)
-- aPackage-hover --
Package a is a package for testing go to definition\.
+-- declBlockA-hover --
+```go
+type a struct {
+ x string
+}
+```
+
+1st type declaration block
+-- declBlockB-hover --
+```go
+type b struct{}
+```
+
+b has a comment
+-- declBlockC-hover --
+```go
+type c struct {
+ f string
+}
+```
+
+c is a struct
+-- declBlockD-hover --
+```go
+type d string
+```
+
+3rd type declaration block
+-- declBlockE-hover --
+```go
+type e struct {
+ f float64
+}
+```
+
+e has a comment
-- err-definition --
godef/a/a.go:33:6-9: defined here as ```go
var err error
@@ -148,39 +184,3 @@
```
z is a variable too\.
--- declBlockA-hover --
-```go
-type a struct {
- x string
-}
-```
-
-1st type declaration block
--- declBlockB-hover --
-```go
-type b struct{}
-```
-
-b has a comment
--- declBlockC-hover --
-```go
-type c struct {
- f string
-}
-```
-
-c is a struct
--- declBlockD-hover --
-```go
-type d string
-```
-
-3rd type declaration block
--- declBlockE-hover --
-```go
-type e struct {
- f float64
-}
-```
-
-e has a comment
diff --git a/internal/lsp/testdata/godef/a/h.go.golden b/internal/lsp/testdata/godef/a/h.go.golden
index 71f78e1..3525d4c 100644
--- a/internal/lsp/testdata/godef/a/h.go.golden
+++ b/internal/lsp/testdata/godef/a/h.go.golden
@@ -1,39 +1,3 @@
--- nestedNumber-hover --
-```go
-field number int64
-```
-
-nested number
--- nestedString-hover --
-```go
-field str string
-```
-
-nested string
--- nestedMap-hover --
-```go
-field m map[string]float64
-```
-
-nested map
--- structA-hover --
-```go
-field a int
-```
-
-a field
--- structB-hover --
-```go
-field b struct{c int}
-```
-
-b nested struct
--- structC-hover --
-```go
-field c int
-```
-
-c field of nested struct
-- arrD-hover --
```go
field d int
@@ -86,12 +50,60 @@
```
X value field
+-- nestedMap-hover --
+```go
+field m map[string]float64
+```
+
+nested map
+-- nestedNumber-hover --
+```go
+field number int64
+```
+
+nested number
+-- nestedString-hover --
+```go
+field str string
+```
+
+nested string
-- openMethod-hover --
```go
func (interface).open() error
```
open method comment
+-- returnX-hover --
+```go
+field x int
+```
+
+X coord
+-- returnY-hover --
+```go
+field y int
+```
+
+Y coord
+-- structA-hover --
+```go
+field a int
+```
+
+a field
+-- structB-hover --
+```go
+field b struct{c int}
+```
+
+b nested struct
+-- structC-hover --
+```go
+field c int
+```
+
+c field of nested struct
-- testDescription-hover --
```go
field desc string
@@ -122,15 +134,3 @@
```
expected test value
--- returnX-hover --
-```go
-field x int
-```
-
-X coord
--- returnY-hover --
-```go
-field y int
-```
-
-Y coord
\ No newline at end of file
diff --git a/internal/lsp/testdata/godef/b/h.go.golden b/internal/lsp/testdata/godef/b/h.go.golden
index 85f0404..b854dd4 100644
--- a/internal/lsp/testdata/godef/b/h.go.golden
+++ b/internal/lsp/testdata/godef/b/h.go.golden
@@ -1,12 +1,12 @@
+-- AStuff-hover --
+```go
+func AStuff()
+```
+
+[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff)
-- AVariable-hover --
```go
var _ A
```
variable of type a\.A
--- AStuff-hover --
-```go
-func AStuff()
-```
-
-[`a.AStuff` on pkg.go.dev](https://pkg.go.dev/golang.org/x/tools/internal/lsp/godef/a?utm_source=gopls#AStuff)
\ No newline at end of file
diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go b/internal/lsp/testdata/godef/infer_generics/inferred.go
new file mode 100644
index 0000000..78abf27
--- /dev/null
+++ b/internal/lsp/testdata/godef/infer_generics/inferred.go
@@ -0,0 +1,12 @@
+package inferred
+
+func app[S interface{ ~[]E }, E any](s S, e E) S {
+ return append(s, e)
+}
+
+func _() {
+ _ = app[[]int] //@mark(constrInfer, "app"),hover("app", constrInfer)
+ _ = app[[]int, int] //@mark(instance, "app"),hover("app", instance)
+ _ = app[[]int]([]int{}, 0) //@mark(partialInfer, "app"),hover("app", partialInfer)
+ _ = app([]int{}, 0) //@mark(argInfer, "app"),hover("app", argInfer)
+}
diff --git a/internal/lsp/testdata/godef/infer_generics/inferred.go.golden b/internal/lsp/testdata/godef/infer_generics/inferred.go.golden
new file mode 100644
index 0000000..2dd97d9
--- /dev/null
+++ b/internal/lsp/testdata/godef/infer_generics/inferred.go.golden
@@ -0,0 +1,20 @@
+-- argInfer-hover --
+```go
+func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁
+```
+-- constrInf-hover --
+```go
+func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁
+```
+-- constrInfer-hover --
+```go
+func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁
+```
+-- instance-hover --
+```go
+func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁
+```
+-- partialInfer-hover --
+```go
+func app(s []int, e int) []int // func[S₁ interface{~[]E₂}, E₂ interface{}](s S₁, e E₂) S₁
+```
diff --git a/internal/lsp/testdata/summary_generics.txt.golden b/internal/lsp/testdata/summary_generics.txt.golden
new file mode 100644
index 0000000..152f38d
--- /dev/null
+++ b/internal/lsp/testdata/summary_generics.txt.golden
@@ -0,0 +1,29 @@
+-- summary --
+CallHierarchyCount = 2
+CodeLensCount = 5
+CompletionsCount = 265
+CompletionSnippetCount = 103
+UnimportedCompletionsCount = 5
+DeepCompletionsCount = 5
+FuzzyCompletionsCount = 8
+RankedCompletionsCount = 163
+CaseSensitiveCompletionsCount = 4
+DiagnosticsCount = 37
+FoldingRangesCount = 2
+FormatCount = 6
+ImportCount = 8
+SemanticTokenCount = 3
+SuggestedFixCount = 40
+FunctionExtractionCount = 18
+DefinitionsCount = 99
+TypeDefinitionsCount = 18
+HighlightsCount = 69
+ReferencesCount = 25
+RenamesCount = 33
+PrepareRenamesCount = 7
+SymbolsCount = 5
+WorkspaceSymbolsCount = 20
+SignaturesCount = 32
+LinksCount = 7
+ImplementationsCount = 14
+
diff --git a/internal/lsp/tests/tests.go b/internal/lsp/tests/tests.go
index 53861e0..f942ced 100644
--- a/internal/lsp/tests/tests.go
+++ b/internal/lsp/tests/tests.go
@@ -32,6 +32,7 @@
"golang.org/x/tools/internal/lsp/source/completion"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/testenv"
+ "golang.org/x/tools/internal/typeparams"
"golang.org/x/tools/txtar"
)
@@ -39,10 +40,17 @@
overlayFileSuffix = ".overlay"
goldenFileSuffix = ".golden"
inFileSuffix = ".in"
- summaryFile = "summary.txt"
testModule = "golang.org/x/tools/internal/lsp"
)
+var summaryFile = "summary.txt"
+
+func init() {
+ if typeparams.Enabled {
+ summaryFile = "summary_generics.txt"
+ }
+}
+
var UpdateGolden = flag.Bool("golden", false, "Update golden files")
type CallHierarchy map[span.Span]*CallHierarchyResult
@@ -322,6 +330,14 @@
}
files := packagestest.MustCopyFileTree(dir)
+ // Prune test cases that exercise generics.
+ if !typeparams.Enabled {
+ for name := range files {
+ if strings.Contains(name, "_generics") {
+ delete(files, name)
+ }
+ }
+ }
overlays := map[string][]byte{}
for fragment, operation := range files {
if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment {