gopls/internal/golang/completion: support go1.27 embedded T{f: ...}
This CL causes completion to offer as candidates the names of
fields promoted from embedded structs, if the source file
uses go1.27 or later.
+ tests
Fixes golang/go#78553
Change-Id: Id346e0a5862c7a937e3e5efdeeaeb3e6b035057f
Reviewed-on: https://go-review.googlesource.com/c/tools/+/786780
Reviewed-by: Hongxiang Jiang <hxjiang@golang.org>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go
index fc8a2f8..411e708 100644
--- a/gopls/internal/golang/completion/completion.go
+++ b/gopls/internal/golang/completion/completion.go
@@ -14,6 +14,7 @@
"go/scanner"
"go/token"
"go/types"
+ "iter"
"math"
"slices"
"sort"
@@ -1809,38 +1810,81 @@
func (c *completer) structLiteralFieldName(ctx context.Context) error {
clInfo := c.enclosingCompositeLiteral
- // Mark fields of the composite literal that have already been set,
- // except for the current field.
- addedFields := make(map[*types.Var]bool)
- for _, el := range clInfo.cl.Elts {
- if kvExpr, ok := el.(*ast.KeyValueExpr); ok {
- if clInfo.kv == kvExpr {
- continue
- }
+ if t, ok := clInfo.clType.Underlying().(*types.Struct); ok {
- if key, ok := kvExpr.Key.(*ast.Ident); ok {
- if used, ok := c.pkg.TypesInfo().Uses[key]; ok {
- if usedVar, ok := used.(*types.Var); ok {
- addedFields[usedVar] = true
+ // Collect selection indices of all existing
+ // fields specified by the struct literal.
+ existing := make(map[types.Object][]int)
+ for _, elt := range clInfo.cl.Elts {
+ if kv, ok := elt.(*ast.KeyValueExpr); ok {
+ if key, ok := kv.Key.(*ast.Ident); ok && clInfo.kv != kv {
+ seln, ok := types.LookupSelection(clInfo.clType, true, c.pkg.Types(), key.Name)
+ if ok {
+ existing[seln.Obj()] = seln.Index()
}
}
}
}
- }
- // Add struct fields.
- if t, ok := types.Unalias(clInfo.clType).(*types.Struct); ok {
- const deltaScore = 0.0001
- for i := range t.NumFields() {
- field := t.Field(i)
- if !addedFields[field] {
- c.deepState.enqueue(candidate{
- obj: field,
- score: highScore - float64(i)*deltaScore,
- })
+ // fields returns the sequence of candidate fields of struct type t.
+ fields := func(t *types.Struct) iter.Seq[*types.Var] {
+ // go1.27 permits promoted fields in struct literals.
+ deep := versions.AtLeast(c.goversion, versions.Go1_27)
+
+ return func(yield func(*types.Var) bool) {
+ var collect func(t *types.Struct) bool
+ collect = func(t *types.Struct) bool {
+ for f := range t.Fields() {
+ if !yield(f) {
+ return false
+ }
+ if deep && f.Anonymous() {
+ if inner, ok := f.Type().Underlying().(*types.Struct); ok && !collect(inner) {
+ return false
+ }
+ }
+ }
+ return true
+ }
+ collect(t)
}
}
+ // conflict reports whether one field index
+ // sequence is a prefix (ancestor) of the other.
+ conflict := func(a, b []int) bool {
+ for i := range min(len(a), len(b)) {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+ }
+
+ fieldloop:
+ for f := range fields(t) {
+ seln, ok := types.LookupSelection(clInfo.clType, true, c.pkg.Types(), f.Name())
+ if !ok || seln.Obj() != f {
+ continue // candidate's name is shadowed or ambiguous here
+ }
+
+ // Reject candidates that conflict with existing fields.
+ for _, indices := range existing {
+ if conflict(seln.Index(), indices) {
+ continue fieldloop
+ }
+ }
+
+ const deltaScore = 0.0001
+ const depthPenalty = 0.01
+ depth := len(seln.Index())
+ fieldIdx := seln.Index()[depth-1]
+ c.deepState.enqueue(candidate{
+ obj: seln.Obj(),
+ score: highScore - float64(depth-1)*depthPenalty - float64(fieldIdx)*deltaScore,
+ })
+ }
+
// Fall through and add lexical completions if we aren't
// certain we are in the key part of a key-value pair.
if !clInfo.maybeInFieldName {
diff --git a/gopls/internal/test/marker/testdata/completion/issue78553.txt b/gopls/internal/test/marker/testdata/completion/issue78553.txt
index 23e813a..34fb489 100644
--- a/gopls/internal/test/marker/testdata/completion/issue78553.txt
+++ b/gopls/internal/test/marker/testdata/completion/issue78553.txt
@@ -4,9 +4,6 @@
-- flags --
-min_go_command=go1.27
--- skip --
-Skipping as this feature is not yet implemented. Ref: go.dev/issues/78553
-
-- go.mod --
module mod.com
@@ -16,21 +13,32 @@
package main
type E1 struct {
- A int //@item(fieldA, "A", "int", "field")
+ A int //@item(fieldA, "A", "int", "field"),item(structE1, "E1", "struct{...}", "struct")
}
type E2 struct {
- E1
+ E1 //@item(fieldE1, "E1", "E1", "field"),item(structE2, "E2", "struct{...}", "struct")
B int //@item(fieldB, "B", "int", "field")
}
type T struct {
- E2 //@item(fieldE2, "E2", "E2", "field")
+ E2 //@item(fieldE2, "E2", "E2", "field"),item(structT, "T", "struct{...}", "struct")
C int //@item(fieldC, "C", "int", "field")
}
+//@item(literalE2, "E2{}", "", "var")
+//@item(funcMain, "main", "func()", "func")
+
func main() {
_ = T{
- //@complete("", fieldA, fieldB, fieldC, fieldE2)
+ // Fields are suggested in breadth-first order:
+ // T fields (E2, C), then E2 fields (E1, B), then E1 fields (A).
+
+ //@complete("", fieldE2, fieldC, fieldE1, fieldB, fieldA, literalE2, funcMain, structE1, structE2, structT)
+ }
+
+ _ = T{
+ A: 1,
+ //@complete("", fieldC, fieldB)
}
}
diff --git a/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt b/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt
new file mode 100644
index 0000000..0e4ec31
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/completion/issue78553_before127.txt
@@ -0,0 +1,28 @@
+Test for completion on promoted fields in struct literals before Go 1.27.
+Ref: go.dev/issues/78553
+
+-- go.mod --
+module mod.com
+
+go 1.26
+
+-- main.go --
+package main
+
+type E1 struct {
+ A int //@item(fieldA, "A", "int", "field")
+}
+
+type T struct {
+ E1 //@item(fieldE1, "E1", "E1", "field"),item(structE1, "E1", "struct{...}", "struct")
+ C int //@item(fieldC, "C", "int", "field"),item(structT, "T", "struct{...}", "struct")
+}
+
+//@item(literalE1, "E1{}", "", "var")
+//@item(funcMain, "main", "func()", "func")
+
+func main() {
+ _ = T{
+ //@complete("", fieldE1, fieldC, literalE1, funcMain, structE1, structT)
+ }
+}