| // 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 ( | |
| "bytes" | |
| "fmt" | |
| "go/ast" | |
| "go/token" | |
| "go/types" | |
| "slices" | |
| "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" | |
| typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex" | |
| "golang.org/x/tools/internal/astutil" | |
| "golang.org/x/tools/internal/moreiters" | |
| "golang.org/x/tools/internal/typesinternal/typeindex" | |
| "golang.org/x/tools/internal/versions" | |
| ) | |
| var EmbedLitAnalyzer = &analysis.Analyzer{ | |
| Name: "embedlit", | |
| Doc: analyzerutil.MustExtractDoc(doc, "embedlit"), | |
| Requires: []*analysis.Analyzer{ | |
| inspect.Analyzer, | |
| typeindexanalyzer.Analyzer, | |
| }, | |
| Run: runEmbedLit, | |
| URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit", | |
| } | |
| // Go1.27 introduced the ability to directly access embedded struct fields. | |
| // The embedlit modernizer suggests two types of fixes that use this feature: | |
| // 1. Removing redundant field type specifiers in embedded struct fields. | |
| // 2. Moving embedded struct field assignments inside of the struct literal | |
| // initialization. | |
| func runEmbedLit(pass *analysis.Pass) (any, error) { | |
| var ( | |
| inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) | |
| index = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index) | |
| info = pass.TypesInfo | |
| ) | |
| for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) { | |
| if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { // non-nested comp lit | |
| // TODO(mkalil): Figure out how to handle addition/removal of commas in | |
| // the comp lit when we observe code where both patterns apply. (This will | |
| // likely require a significant amount of work). For now, only apply edits | |
| // from one pattern at a time. | |
| if !embedlitUnnest(pass, info, curLit) { | |
| err := embedlitCombine(pass, index, info, curLit) // calls pass.ReadFile | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| } | |
| } | |
| return nil, nil | |
| } | |
| // Pattern A: removing unneeded embedded field type specifier from the struct | |
| // literal. | |
| // T{U: U{f: v, ...}} => T{f: v, ...} | |
| // It returns true if it reported a diagnostic with edits. | |
| func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) (reported bool) { | |
| var ( | |
| lit = curLit.Node().(*ast.CompositeLit) | |
| compLitType = info.TypeOf(lit) | |
| ) | |
| // checkLit determines whether any of the fields in the given struct literal can | |
| // be promoted, and calculates the corresponding edits. | |
| var checkLit func(lit *ast.CompositeLit) | |
| checkLit = func(lit *ast.CompositeLit) { | |
| for i, 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 { | |
| // Inv: len(innerLit.Elts) > 0. We skip empty struct literals. | |
| // Emit edits to delete the unnecessary embedded field type specifier | |
| // and its closing brace. | |
| // 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 | |
| } | |
| tokFile := pass.Fset.File(kv.Pos()) | |
| fileEnd := token.Pos(tokFile.Base() + tokFile.Size()) | |
| // Skip if brace positions are missing or invalid (golang/go#81590). | |
| if !innerLit.Lbrace.IsValid() || !innerLit.Rbrace.IsValid() || | |
| kv.Pos() > innerLit.Lbrace || closingPos > innerLit.Rbrace || | |
| innerLit.Rbrace >= fileEnd { | |
| continue | |
| } | |
| // If any comments overlap with the range to delete, don't suggest a fix. | |
| if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) || | |
| !moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) { | |
| continue | |
| } | |
| // Delete starting from the key to the character right after the | |
| // opening brace of the inner literal: | |
| // T{U: U{f: v, ...}} | |
| // ----- | |
| startPos := kv.Pos() | |
| endPos := innerLit.Lbrace + 1 | |
| // Delete the entire line if the key and its opening brace are | |
| // together on their own line. This prevents leaving behind unneeded | |
| // blank lines inside struct literals that `gofmt` will not remove. | |
| // T{ | |
| // U: U{ <- delete entire line | |
| // f: v, | |
| // } | |
| // } | |
| // | |
| lineOf := func(pos token.Pos) int { | |
| return tokFile.PositionFor(pos, false).Line | |
| } | |
| curLine := lineOf(kv.Pos()) | |
| var prevLine int | |
| if i == 0 { | |
| // First element, so the previous line is the parent lbrace. | |
| prevLine = lineOf(lit.Lbrace) | |
| } else { | |
| prevLine = lineOf(lit.Elts[i-1].End()) | |
| } | |
| // We can safely delete the entire line if the key value expression is | |
| // alone on its line: it starts on a new line relative to the previous | |
| // element (prevLine < curLine), and the first element of the inner | |
| // literal starts on a subsequent line. | |
| if prevLine < curLine && curLine < tokFile.LineCount() && // (1-based) | |
| lineOf(innerLit.Elts[0].Pos()) > curLine { | |
| lineStart := tokFile.LineStart(curLine) | |
| nextLineStart := tokFile.LineStart(curLine + 1) | |
| // Check that there are no comments on the line we are going to delete. | |
| if moreiters.Empty(astutil.Comments(file, lineStart, nextLineStart)) { | |
| startPos = tokFile.LineStart(curLine) | |
| endPos = nextLineStart | |
| } | |
| } | |
| if !(startPos < endPos && endPos <= closingPos) { | |
| continue | |
| } | |
| edits := []analysis.TextEdit{ | |
| // T{U: U{f: v, ...}} | |
| // ----- - | |
| { | |
| // Delete the key and the opening brace of the inner struct literal. | |
| Pos: startPos, | |
| End: endPos, | |
| }, | |
| { | |
| // 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, | |
| }, | |
| } | |
| pass.Report(analysis.Diagnostic{ | |
| Pos: kv.Pos(), | |
| End: innerLit.Lbrace + 1, | |
| Message: "embedded field type can be removed from struct literal", | |
| SuggestedFixes: []analysis.SuggestedFix{ | |
| { | |
| Message: fmt.Sprintf("Remove embedded field type %s", kv.Key.(*ast.Ident).Name), | |
| TextEdits: edits, | |
| }, | |
| }, | |
| }) | |
| reported = true | |
| checkLit(innerLit) | |
| } | |
| } | |
| } | |
| } | |
| checkLit(lit) | |
| return reported | |
| } | |
| // Pattern B: moving embedded field assignments inside the struct literal | |
| // initialization. | |
| // t := T{...}; t.x = x => t := T{..., x: x} | |
| // (or var t = ...) | |
| func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error { | |
| compLit := curLit.Node().(*ast.CompositeLit) | |
| if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool { | |
| return is[*ast.KeyValueExpr](e) | |
| }) { | |
| // Promoting additional embedded fields would result in mixing keyed and | |
| // unkeyed fields, which isn't allowed. | |
| return nil | |
| } | |
| var ( | |
| // Ident for "t" in the assignment. | |
| lhs *ast.Ident | |
| // The cursor representing the statement that initializes the comp lit "t". | |
| // We use its siblings to search for field assignments and verify that there | |
| // are no intervening statements, in case those statements observe "t". | |
| curStmt inspector.Cursor | |
| ) | |
| switch curLit.ParentEdgeKind() { | |
| case edge.AssignStmt_Rhs: | |
| assign := curLit.Parent().Node().(*ast.AssignStmt) | |
| // TODO(mkalil): Handle lhs forms that aren't idents, i.e. x.y[i] = T{...}. | |
| // TODO(mkalil): Handle multi-assignments like t1, t2 := A{}, B{} | |
| if len(assign.Lhs) != 1 { | |
| return nil | |
| } | |
| if id, ok := assign.Lhs[0].(*ast.Ident); ok { | |
| lhs = id | |
| curStmt = curLit.Parent() | |
| } | |
| case edge.ValueSpec_Values: | |
| spec := curLit.Parent().Node().(*ast.ValueSpec) | |
| // TODO(mkalil): Handle multi-declarations like var (x = A{}; y = B{}) or var x, y = ... | |
| if len(spec.Names) != 1 { | |
| return nil | |
| } | |
| lhs = spec.Names[0] | |
| if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok { | |
| if gdecl, ok := decl.Node().(*ast.DeclStmt).Decl.(*ast.GenDecl); ok && len(gdecl.Specs) == 1 { | |
| curStmt = decl | |
| } | |
| } | |
| default: | |
| return nil | |
| } | |
| if lhs == nil || !curStmt.Valid() { | |
| return nil | |
| } | |
| var ( | |
| compLitType = info.TypeOf(compLit) | |
| tObj = info.ObjectOf(lhs) | |
| // Marks the contiguous block of embedded field assign statements that will | |
| // be moved into the struct initialization. | |
| firstStmt, lastStmt inspector.Cursor | |
| hasEmbeddedSelection bool | |
| ) | |
| if compLitType == nil { | |
| return nil | |
| } | |
| // Record the index paths of the existing fields in the composite literal. Two | |
| // fields in a composite literal conflict if one field's path is a prefix of | |
| // the other's. If the field in an assignment conflicts with an existing | |
| // field, we won't suggest a fix to move it into the struct literal. | |
| var fieldPaths [][]int | |
| for _, elt := range compLit.Elts { | |
| k, ok := elt.(*ast.KeyValueExpr).Key.(*ast.Ident) | |
| if !ok { | |
| return nil | |
| } | |
| _, idx, _ := types.LookupFieldOrMethod(compLitType, true, pass.Pkg, k.Name) | |
| if len(idx) == 0 { | |
| return nil | |
| } | |
| fieldPaths = append(fieldPaths, idx) | |
| } | |
| stmtloop: | |
| for { | |
| var ok bool | |
| curStmt, ok = curStmt.NextSibling() | |
| if !ok { | |
| break // end of (e.g.) block | |
| } | |
| // All embedded field value assignments must immediately follow the struct | |
| // initialization. | |
| assign, ok := curStmt.Node().(*ast.AssignStmt) | |
| if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) { | |
| // TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2 | |
| break | |
| } | |
| expr := assign.Lhs[0] | |
| sel, ok := expr.(*ast.SelectorExpr) | |
| if !ok { | |
| break | |
| } | |
| // Verify that sel.X refers to the same object as "t" | |
| selXId, ok := sel.X.(*ast.Ident) | |
| if !ok { | |
| // TODO(mkalil): handle deeply nested expressions like t.B.x | |
| break | |
| } | |
| obj := info.ObjectOf(selXId) | |
| if obj != tObj { | |
| break | |
| } | |
| fieldObj, assignIdx, indirect := types.LookupFieldOrMethod(compLitType, true, pass.Pkg, sel.Sel.Name) | |
| fieldVar, ok := fieldObj.(*types.Var) | |
| if !ok || len(assignIdx) == 0 || indirect { // don't allow accessing promoted fields through implicit pointer indirection | |
| break | |
| } | |
| // A composite literal cannot specify both an enclosing embedded field and a promoted | |
| // field from within it, nor duplicate fields. | |
| if slices.ContainsFunc(fieldPaths, func(index []int) bool { return pathConflicts(index, assignIdx) }) { | |
| break | |
| } | |
| // The selection is from an embedded field if it directly | |
| // assigns an embedded struct field (t.B = B{...}) or if | |
| // the length of the index path is greater than one. | |
| if fieldVar.Embedded() || len(assignIdx) > 1 { | |
| hasEmbeddedSelection = true | |
| } | |
| rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0) | |
| if uses(index, rhsCur, tObj) { | |
| break | |
| } | |
| for c := range rhsCur.Preorder((*ast.Ident)(nil)) { | |
| id := c.Node().(*ast.Ident) | |
| // If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because | |
| // we can't evaluate t.y when constructing the new literal. | |
| if info.ObjectOf(id) == tObj { | |
| break stmtloop | |
| } | |
| // Note: we don't need to worry about expressions with side effects | |
| // changing the behavior when moved inside the comp lit. The order of | |
| // effects will be preserved because we preserve the order of the key | |
| // value pairs inside the comp lit. | |
| } | |
| // We might move multiple sequential assignment statements into | |
| // the struct literal, so we need to keep track of the index path | |
| // of this assignment to check it against subsequent assignments. | |
| fieldPaths = append(fieldPaths, assignIdx) | |
| if !firstStmt.Valid() { | |
| firstStmt = curStmt | |
| } | |
| lastStmt = curStmt | |
| } | |
| if !firstStmt.Valid() || !hasEmbeddedSelection { | |
| // We should not suggest a fix if none of the selections are from embedded fields. | |
| return nil | |
| } | |
| file := astutil.EnclosingFile(curLit) | |
| // Enable modernizer only for Go1.27. | |
| if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) { | |
| return nil | |
| } | |
| // Read file content to determine if the struct lit has a trailing comma | |
| // after its last element. | |
| tokFile := pass.Fset.File(compLit.Rbrace) | |
| filename := tokFile.Name() | |
| src, err := pass.ReadFile(filename) | |
| if err != nil { | |
| return err | |
| } | |
| hasTrailingComma := false | |
| if len(compLit.Elts) > 0 { | |
| lastElt := compLit.Elts[len(compLit.Elts)-1] | |
| lastEltOffset := tokFile.Offset(lastElt.End()) | |
| rbraceOffset := tokFile.Offset(compLit.Rbrace) | |
| span := bytes.Clone(src[lastEltOffset:rbraceOffset]) | |
| // Zero out any comments in the span, so that a comma within | |
| // one is not mistaken for the literal's trailing comma. | |
| for co := range astutil.Comments(file, lastElt.End(), compLit.Rbrace) { | |
| start := max(tokFile.Offset(co.Pos())-lastEltOffset, 0) | |
| end := min(tokFile.Offset(co.End())-lastEltOffset, len(span)) | |
| if start < end { | |
| clear(span[start:end]) | |
| } | |
| } | |
| hasTrailingComma = bytes.Contains(span, []byte(",")) | |
| } | |
| var edits []analysis.TextEdit | |
| // Emit edits to move the field assignment into the struct lit while | |
| // removing it from its current place. | |
| // t := T{...}; t.x = v | |
| // ----- --- - | |
| // t := T{..., x: v} | |
| // Add a trailing comma before the closing brace of compLit if one doesn't | |
| // exist, and delete the closing brace itself. | |
| // t := T{...}; t.x = v | |
| // - | |
| // t := T{..., t.x = v | |
| if len(compLit.Elts) > 0 && !hasTrailingComma { | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: compLit.Rbrace, | |
| End: compLit.Rbrace + 1, | |
| NewText: []byte(","), | |
| }) | |
| } else { | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: compLit.Rbrace, | |
| End: compLit.Rbrace + 1, | |
| }) | |
| } | |
| // For each assignment: | |
| // t.x = v | |
| // -- --- | |
| // x : v | |
| curStmt = firstStmt | |
| var prevStmt inspector.Cursor | |
| for { | |
| assign := curStmt.Node().(*ast.AssignStmt) | |
| expr := assign.Lhs[0] | |
| sel := expr.(*ast.SelectorExpr) | |
| // Delete "t." | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: assign.Pos(), | |
| End: sel.Sel.Pos(), | |
| }) | |
| // Replace "=" with ":" | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: expr.End(), | |
| End: assign.TokPos + 1, | |
| NewText: []byte(":"), | |
| }) | |
| // Add a comma after the previous assignment if this is not the first one. | |
| if prevStmt.Valid() { | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: prevStmt.Node().End(), | |
| NewText: []byte(","), | |
| }) | |
| } | |
| // For the last assignment, add the closing brace of the struct lit. | |
| if curStmt == lastStmt { | |
| edits = append(edits, analysis.TextEdit{ | |
| Pos: assign.End(), | |
| NewText: []byte("}"), | |
| }) | |
| break | |
| } | |
| prevStmt = curStmt | |
| curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt | |
| } | |
| pass.Report(analysis.Diagnostic{ | |
| Pos: curLit.Node().Pos(), | |
| End: curLit.Node().End(), | |
| Message: "embedded field assignment can be moved to struct literal", | |
| SuggestedFixes: []analysis.SuggestedFix{ | |
| { | |
| Message: "Move embedded field assignment to struct literal", | |
| TextEdits: edits, | |
| }, | |
| }, | |
| }) | |
| return 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 || len(lit.Elts) == 0 { | |
| // Skip if the struct literal is empty. | |
| 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". | |
| // We also reject composite literals with slice elements, as parentObj will be nil. | |
| 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 | |
| } | |
| // pathConflicts reports whether the specified index paths conflict. | |
| // Two index paths conflict if one is a prefix of the other. | |
| func pathConflicts(p1, p2 []int) bool { | |
| return (len(p1) >= len(p2) && slices.Equal(p1[:len(p2)], p2)) || | |
| (len(p2) >= len(p1) && slices.Equal(p2[:len(p1)], p1)) | |
| } |