go/analysis/passes/modernize: add embedlit modernizer
This modernizer offers to simplify references to embedded fields
in struct literals by removing unneccessary field type specifiers.
This rewrite is possible due to a spec change, introduced in Go1.27,
allowing direct reference to embedded fields in struct literals.
For example:
type T struct { U }
type U struct { x int }
T{U: U{x:1}}
can be rewritten as
T{x: 1}
Certain struct literals cannot be rewritten, or only partially simplified,
if they contain unkeyed fields or ambiguous types.
Note: this CL addresses patterns where values are initialized
inside the struct, and leaves as a TODO patterns where values
are set after struct initialization using "dot field" references
(i.e. t := T{} ... t.x = x => t := T{x: x}).
Updates golang/go#77965
Change-Id: I246f2251ee55d9cb7c529b83e50ff2259eeac5d0
Reviewed-on: https://go-review.googlesource.com/c/tools/+/768240
Auto-Submit: Alan Donovan <adonovan@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/passes/modernize/doc.go b/go/analysis/passes/modernize/doc.go
index 8636b46..6184a19 100644
--- a/go/analysis/passes/modernize/doc.go
+++ b/go/analysis/passes/modernize/doc.go
@@ -111,6 +111,31 @@
`interface{}`, with the `any` alias, which was introduced in Go 1.18.
This is a purely stylistic change that makes code more readable.
+# Analyzer embedlit
+
+embedlit: simplify references to embedded fields in composite literals
+
+The embedlit analyzer suggests removing redundant embedded field type specifiers
+from composite literals. Go1.27 introduced the ability to directly initialize
+fields promoted from embedded struct types without a nested literal. For
+example, given the following structs:
+
+ type T struct {
+ U
+ }
+
+ type U struct {
+ x int
+ }
+
+A composite literal such as
+
+ t := T{U: U{x: 1}}
+
+would become
+
+ t := T{x: 1}
+
# Analyzer errorsastype
errorsastype: replace errors.As with errors.AsType[T]
diff --git a/go/analysis/passes/modernize/embedlit.go b/go/analysis/passes/modernize/embedlit.go
new file mode 100644
index 0000000..090ee4c
--- /dev/null
+++ b/go/analysis/passes/modernize/embedlit.go
@@ -0,0 +1,175 @@
+// Copyright 2026 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 modernize
+
+import (
+ "fmt"
+ "go/ast"
+ "go/types"
+ "strings"
+
+ "golang.org/x/tools/go/analysis"
+ "golang.org/x/tools/go/analysis/passes/inspect"
+ "golang.org/x/tools/go/ast/edge"
+ "golang.org/x/tools/go/ast/inspector"
+ "golang.org/x/tools/internal/analysis/analyzerutil"
+ "golang.org/x/tools/internal/astutil"
+ "golang.org/x/tools/internal/goplsexport"
+ "golang.org/x/tools/internal/moreiters"
+ "golang.org/x/tools/internal/versions"
+)
+
+var embedLitAnalyzer = &analysis.Analyzer{
+ Name: "embedlit",
+ Doc: analyzerutil.MustExtractDoc(doc, "embedlit"),
+ Requires: []*analysis.Analyzer{
+ inspect.Analyzer,
+ },
+ Run: runEmbedLit,
+ URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit",
+}
+
+func init() {
+ goplsexport.EmbedLitModernizer = embedLitAnalyzer
+}
+
+// TODO(mkalil): Handle other patterns such as:
+// t := T{...}
+// t.x = x
+// ...
+// =>
+// t := T{..., x: x, ...}
+func runEmbedLit(pass *analysis.Pass) (any, error) {
+ var (
+ inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
+ info = pass.TypesInfo
+ )
+ for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
+ var (
+ edits []analysis.TextEdit
+ names []string // names of the embedded field types that can be removed
+ compLit = curLit.Node().(*ast.CompositeLit)
+ compLitType = info.TypeOf(compLit)
+ check func(*ast.CompositeLit)
+ )
+ check = func(lit *ast.CompositeLit) {
+ for _, elt := range lit.Elts {
+ // Can't promote an unkeyed field; would result in a syntax error.
+ if kv, ok := elt.(*ast.KeyValueExpr); ok {
+ if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil {
+ // Emit edits to delete the unnecessary embedded field type specifier
+ // and its closing brace.
+ closingPos := innerLit.Rbrace
+ if len(innerLit.Elts) > 0 {
+ // Delete any inner trailing commas or white space. Extra trailing commas
+ // would result in invalid code.
+ closingPos = innerLit.Elts[len(innerLit.Elts)-1].End()
+ }
+ file := astutil.EnclosingFile(curLit)
+ // Enable modernizer only for Go1.27.
+ if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
+ return
+ }
+ // If any comments overlap with the range to delete, don't suggest a fix.
+ if !moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) {
+ continue
+ }
+ edits = append(edits, []analysis.TextEdit{
+ // T{U: U{f: v, ...}}
+ // ----- -
+ {
+ // Delete the key and the opening brace of the inner struct literal.
+ Pos: kv.Pos(),
+ End: innerLit.Lbrace + 1,
+ },
+ {
+ // Delete the corresponding closing brace, including preceding
+ // white space or commas. Failing to delete trailing commas may
+ // result in invalid code.
+ Pos: closingPos,
+ End: innerLit.Rbrace + 1,
+ },
+ }...)
+ names = append(names, kv.Key.(*ast.Ident).Name)
+ check(innerLit)
+ }
+ }
+ }
+ }
+
+ if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value {
+ compLit := curLit.Node().(*ast.CompositeLit)
+ check(compLit) // non-nested comp lit
+ }
+ if len(edits) > 0 {
+ pass.Report(analysis.Diagnostic{
+ Pos: curLit.Node().Pos(),
+ End: curLit.Node().End(),
+ Message: "embedded field type can be removed from struct literal",
+ SuggestedFixes: []analysis.SuggestedFix{
+ {
+ Message: fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")),
+ TextEdits: edits,
+ },
+ },
+ })
+ }
+ }
+ return nil, nil
+}
+
+// isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for
+// an embedded field for which we can safely remove its type.
+// If so, it returns the corresponding CompositeLit.
+// If elt contains an unkeyed field or ambiguous type, it returns nil.
+func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
+ obj := keyedField(info, kv)
+ if obj == nil || !obj.Embedded() {
+ return nil
+ }
+ lit, ok := kv.Value.(*ast.CompositeLit)
+ if !ok {
+ return nil
+ }
+ // We cannot remove this type if any of its nested composite elements have
+ // unkeyed fields or are ambiguous, so we check for those conditions before
+ // returning.
+ for _, elt := range lit.Elts {
+ kv, ok := elt.(*ast.KeyValueExpr)
+ if !ok {
+ return nil
+ }
+ obj := keyedField(info, kv)
+ if obj == nil {
+ return nil
+ }
+ k := kv.Key.(*ast.Ident) // can't fail
+ // Cannot promote an ambiguous type, for example:
+ // type T struct { A; B }
+ // type A struct { x int }
+ // type B struct { x int }
+ // _ = T{A: A{x: 1}}
+ // cannot be simplified to T{x: 1} because T has two different embedded fields called "x".
+ parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
+ if parentObj != obj {
+ return nil
+ }
+ }
+ return lit
+}
+
+// keyedField reports whether the key of kv is an embedded field type. If so, it
+// returns the type of the embedded field, otherwise it returns nil.
+func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
+ k, ok := kv.Key.(*ast.Ident)
+ if !ok {
+ return nil
+ }
+ obj, ok := info.ObjectOf(k).(*types.Var)
+ if !ok || !obj.IsField() {
+ return nil
+ }
+ return obj
+}
diff --git a/go/analysis/passes/modernize/modernize.go b/go/analysis/passes/modernize/modernize.go
index db3b86f..b84f8fe 100644
--- a/go/analysis/passes/modernize/modernize.go
+++ b/go/analysis/passes/modernize/modernize.go
@@ -38,6 +38,7 @@
atomicTypesAnalyzer,
// AppendClippedAnalyzer, // not nil-preserving!
// BLoopAnalyzer, // may skew benchmark results, see golang/go#74967
+ embedLitAnalyzer,
FmtAppendfAnalyzer,
ForVarAnalyzer,
MapsLoopAnalyzer,
diff --git a/go/analysis/passes/modernize/modernize_test.go b/go/analysis/passes/modernize/modernize_test.go
index f757e99..239920b 100644
--- a/go/analysis/passes/modernize/modernize_test.go
+++ b/go/analysis/passes/modernize/modernize_test.go
@@ -29,6 +29,11 @@
RunWithSuggestedFixes(t, TestData(), modernize.AnyAnalyzer, "any")
}
+func TestEmbedLit(t *testing.T) {
+ testenv.NeedsGo1Point(t, 27)
+ RunWithSuggestedFixes(t, TestData(), goplsexport.EmbedLitModernizer, "embedlit")
+}
+
func TestErrorsAsType(t *testing.T) {
RunWithSuggestedFixes(t, TestData(), goplsexport.ErrorsAsTypeModernizer, "errorsastype/...")
}
diff --git a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit.go b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit.go
new file mode 100644
index 0000000..5f7cc61
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit.go
@@ -0,0 +1 @@
+package embedlit
diff --git a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go
new file mode 100644
index 0000000..f79de53
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go
@@ -0,0 +1,95 @@
+//go:build go1.27
+
+package embedlit
+
+type A struct {
+ a int
+ B
+}
+
+type B struct {
+ b int
+ C
+}
+
+type C struct {
+ c int
+ D
+}
+
+type D struct {
+ d int
+ E
+}
+
+type E struct {
+ e int
+ F
+}
+
+type F struct {
+ f int
+}
+
+type G struct {
+ f int
+}
+
+type H struct {
+ F
+ G
+}
+
+type I struct {
+ i int
+}
+
+type J struct {
+ F
+ G
+ I
+}
+
+const zero = 0
+
+type K struct{ L }
+type L []int
+
+var (
+ _ = A{B: B{b: 1}} // want "embedded field type can be removed from struct literal"
+ _ = A{a: 1, B: B{b: 1, C: C{c: 1}}} // want "embedded field type can be removed from struct literal"
+ _ = E{F: F{1}} // nope: cannot promote unkeyed fields
+ _ = D{E: E{F: F{1}}, d: 1} // want "embedded field type can be removed from struct literal"
+ _ = D{E: E{e: 2, F: F{1}}} // want "embedded field type can be removed from struct literal"
+ _ = A{a: 10, B: B{C: C{1, D{d: 1}}}} // want "embedded field type can be removed from struct literal"
+ _ = H{F: F{f: 1}} // nope: cannot promote ambiguous fields
+ _ = J{I: I{i: 1}, F: F{f: 1}, G: G{f: 1}} // want "embedded field type can be removed from struct literal"
+ // multi-line with commas
+ _ = A{ // want "embedded field type can be removed from struct literal"
+ B: B{
+ b: 1,
+ },
+ }
+ // empty composite lit
+ _ = A{a: 1, B: B{}} // want "embedded field type can be removed from struct literal"
+ // don't suggest a fix if it's too tricky to preserve comments
+ _ = A{ // nope: comments within range to delete
+ B: B{ // one
+ C: C{ // two
+ c: 1, // three
+ }, // four
+ }, // five
+ }
+ _ = A{ // nope: comments within range to delete
+ B: B{b: 1 /* comment, with comma */},
+ a: 2,
+ }
+ _ = A{ // nope: comments within range to delete
+ B: B{
+ b: 1, // comment, with comma
+ },
+ a: 2,
+ }
+ _ = A{B: B{b: 1} /* comment */, a: 2} // want "embedded field type can be removed from struct literal"
+ _ = K{L: L{zero: 0}} // nope: cannot promote slice elements
+)
diff --git a/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden
new file mode 100644
index 0000000..d20da95
--- /dev/null
+++ b/go/analysis/passes/modernize/testdata/src/embedlit/embedlit_go127.go.golden
@@ -0,0 +1,94 @@
+//go:build go1.27
+
+package embedlit
+
+type A struct {
+ a int
+ B
+}
+
+type B struct {
+ b int
+ C
+}
+
+type C struct {
+ c int
+ D
+}
+
+type D struct {
+ d int
+ E
+}
+
+type E struct {
+ e int
+ F
+}
+
+type F struct {
+ f int
+}
+
+type G struct {
+ f int
+}
+
+type H struct {
+ F
+ G
+}
+
+type I struct {
+ i int
+}
+
+type J struct {
+ F
+ G
+ I
+}
+
+const zero = 0
+
+type K struct{ L }
+type L []int
+
+var (
+ _ = A{b: 1} // want "embedded field type can be removed from struct literal"
+ _ = A{a: 1, b: 1, c: 1} // want "embedded field type can be removed from struct literal"
+ _ = E{F: F{1}} // nope: cannot promote unkeyed fields
+ _ = D{F: F{1}, d: 1} // want "embedded field type can be removed from struct literal"
+ _ = D{e: 2, F: F{1}} // want "embedded field type can be removed from struct literal"
+ _ = A{a: 10, C: C{1, D{d: 1}}} // want "embedded field type can be removed from struct literal"
+ _ = H{F: F{f: 1}} // nope: cannot promote ambiguous fields
+ _ = J{i: 1, F: F{f: 1}, G: G{f: 1}} // want "embedded field type can be removed from struct literal"
+ // multi-line with commas
+ _ = A{ // want "embedded field type can be removed from struct literal"
+
+ b: 1,
+ }
+ // empty composite lit
+ _ = A{a: 1} // want "embedded field type can be removed from struct literal"
+ // don't suggest a fix if it's too tricky to preserve comments
+ _ = A{ // nope: comments within range to delete
+ B: B{ // one
+ C: C{ // two
+ c: 1, // three
+ }, // four
+ }, // five
+ }
+ _ = A{ // nope: comments within range to delete
+ B: B{b: 1 /* comment, with comma */},
+ a: 2,
+ }
+ _ = A{ // nope: comments within range to delete
+ B: B{
+ b: 1, // comment, with comma
+ },
+ a: 2,
+ }
+ _ = A{b: 1 /* comment */, a: 2} // want "embedded field type can be removed from struct literal"
+ _ = K{L: L{zero: 0}} // nope: cannot promote slice elements
+)
diff --git a/gopls/doc/release/v0.22.0.md b/gopls/doc/release/v0.22.0.md
index a3d6d84..444936e 100644
--- a/gopls/doc/release/v0.22.0.md
+++ b/gopls/doc/release/v0.22.0.md
@@ -21,6 +21,14 @@
`var x atomic.Int32` and updates all corresponding call sites from
`atomic.AddInt32(&x, 1)` to method calls like `x.Add(1)`.
+### `embedlit` modernizer
+<!-- golang/go#77965 -->
+The new `embedlit` modernizer suggests removing embedded field type specifiers
+from composite literals when they are redundant under the rules of Go 1.27,
+which introduces the ability to directly initialize fields promoted from
+embedded struct types without a nested literal.
+
+
### `fieldassignment` analyzer
<!-- golang/go#76237 -->
The fieldassignment analyzer, which reports diagnostics about memory layout and
diff --git a/internal/astutil/comment.go b/internal/astutil/comment.go
index 5ed4765..40a3472 100644
--- a/internal/astutil/comment.go
+++ b/internal/astutil/comment.go
@@ -8,6 +8,7 @@
"go/ast"
"go/token"
"iter"
+ "sort"
"strings"
)
@@ -114,18 +115,25 @@
}
// Comments returns an iterator over the comments overlapping the specified interval.
+// Comments are sorted by position in the file, so we can use binary search.
func Comments(file *ast.File, start, end token.Pos) iter.Seq[*ast.Comment] {
- // TODO(adonovan): optimize use binary O(log n) instead of linear O(n) search.
return func(yield func(*ast.Comment) bool) {
- for _, cg := range file.Comments {
- for _, co := range cg.List {
+ // Find the first comment group that overlaps the range.
+ i := sort.Search(len(file.Comments), func(i int) bool {
+ return file.Comments[i].End() >= start
+ })
+ for _, cg := range file.Comments[i:] {
+ if cg.Pos() > end {
+ return
+ }
+ // Find the first comment in the group that overlaps the range.
+ j := sort.Search(len(cg.List), func(j int) bool {
+ return cg.List[j].End() >= start
+ })
+ for _, co := range cg.List[j:] {
if co.Pos() > end {
return
}
- if co.End() < start {
- continue
- }
-
if !yield(co) {
return
}
diff --git a/internal/goplsexport/export.go b/internal/goplsexport/export.go
index 57c15c4..414c9cb 100644
--- a/internal/goplsexport/export.go
+++ b/internal/goplsexport/export.go
@@ -9,11 +9,12 @@
import "golang.org/x/tools/go/analysis"
var (
- ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer
+ ErrorsAsTypeModernizer *analysis.Analyzer // = modernize.errorsastypeAnalyzer
SlicesBackwardModernizer *analysis.Analyzer // = modernize.slicesbackwardAnalyzer
- StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer
- PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer
- StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer
- UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer
- AtomicTypesModernizer *analysis.Analyzer // = modernize.atomicTypesAnalyzer
+ StdIteratorsModernizer *analysis.Analyzer // = modernize.stditeratorsAnalyzer
+ PlusBuildModernizer *analysis.Analyzer // = modernize.plusbuildAnalyzer
+ StringsCutModernizer *analysis.Analyzer // = modernize.stringscutAnalyzer
+ UnsafeFuncsModernizer *analysis.Analyzer // = modernize.unsafeFuncsAnalyzer
+ AtomicTypesModernizer *analysis.Analyzer // = modernize.atomicTypesAnalyzer
+ EmbedLitModernizer *analysis.Analyzer // = modernize.embedLitAnalyzer
)
diff --git a/internal/moreiters/iters.go b/internal/moreiters/iters.go
index 9e4aaf9..3dc81c4 100644
--- a/internal/moreiters/iters.go
+++ b/internal/moreiters/iters.go
@@ -53,3 +53,11 @@
}
return
}
+
+// Empty reports whether the sequence contains no elements.
+func Empty[T any](seq iter.Seq[T]) bool {
+ for range seq {
+ return false
+ }
+ return true
+}