Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/modernize/embedlit.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package modernize
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"go/ast"
    11  	"go/token"
    12  	"go/types"
    13  	"slices"
    14  	"strings"
    15  
    16  	"golang.org/x/tools/go/analysis"
    17  	"golang.org/x/tools/go/analysis/passes/inspect"
    18  	"golang.org/x/tools/go/ast/edge"
    19  	"golang.org/x/tools/go/ast/inspector"
    20  	"golang.org/x/tools/internal/analysis/analyzerutil"
    21  	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
    22  	"golang.org/x/tools/internal/astutil"
    23  	"golang.org/x/tools/internal/moreiters"
    24  	"golang.org/x/tools/internal/typesinternal/typeindex"
    25  	"golang.org/x/tools/internal/versions"
    26  )
    27  
    28  var EmbedLitAnalyzer = &analysis.Analyzer{
    29  	Name: "embedlit",
    30  	Doc:  analyzerutil.MustExtractDoc(doc, "embedlit"),
    31  	Requires: []*analysis.Analyzer{
    32  		inspect.Analyzer,
    33  		typeindexanalyzer.Analyzer,
    34  	},
    35  	Run: runEmbedLit,
    36  	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_embedlit",
    37  }
    38  
    39  // Go1.27 introduced the ability to directly access embedded struct fields.
    40  // The embedlit modernizer suggests two types of fixes that use this feature:
    41  // 1. Removing redundant field type specifiers in embedded struct fields.
    42  // 2. Moving embedded struct field assignments inside of the struct literal
    43  // initialization.
    44  func runEmbedLit(pass *analysis.Pass) (any, error) {
    45  	var (
    46  		inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    47  		index   = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
    48  		info    = pass.TypesInfo
    49  	)
    50  	for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
    51  		if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { // non-nested comp lit
    52  			// TODO(mkalil): Figure out how to handle addition/removal of commas in
    53  			// the comp lit when we observe code where both patterns apply. (This will
    54  			// likely require a significant amount of work). For now, only apply edits
    55  			// from one pattern at a time.
    56  			if !embedlitUnnest(pass, info, curLit) {
    57  				err := embedlitCombine(pass, index, info, curLit) // calls pass.ReadFile
    58  				if err != nil {
    59  					return nil, err
    60  				}
    61  			}
    62  		}
    63  	}
    64  	return nil, nil
    65  }
    66  
    67  // Pattern A: removing unneeded embedded field type specifier from the struct
    68  // literal.
    69  // T{U: U{f: v, ...}} => T{f: v, ...}
    70  // It returns true if it reported a diagnostic with edits.
    71  func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) bool {
    72  	var (
    73  		edits       []analysis.TextEdit
    74  		names       []string // names of the embedded field types that can be removed
    75  		lit         = curLit.Node().(*ast.CompositeLit)
    76  		compLitType = info.TypeOf(lit)
    77  	)
    78  
    79  	// checkLit determines whether any of the fields in the given struct literal can
    80  	// be promoted, and calculates the corresponding edits.
    81  	var checkLit func(lit *ast.CompositeLit)
    82  	checkLit = func(lit *ast.CompositeLit) {
    83  		for i, elt := range lit.Elts {
    84  			// Can't promote an unkeyed field; would result in a syntax error.
    85  			if kv, ok := elt.(*ast.KeyValueExpr); ok {
    86  				if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil {
    87  					// Inv: len(innerLit.Elts) > 0. We skip empty struct literals.
    88  					// Emit edits to delete the unnecessary embedded field type specifier
    89  					// and its closing brace.
    90  					// Delete any inner trailing commas or white space. Extra trailing commas
    91  					// would result in invalid code.
    92  					closingPos := innerLit.Elts[len(innerLit.Elts)-1].End()
    93  					file := astutil.EnclosingFile(curLit)
    94  					// Enable modernizer only for Go1.27.
    95  					if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
    96  						return
    97  					}
    98  					// If any comments overlap with the range to delete, don't suggest a fix.
    99  					if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) ||
   100  						!moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) {
   101  						continue
   102  					}
   103  					// Delete starting from the key to the character right after the
   104  					// opening brace of the inner literal:
   105  					// T{U: U{f: v, ...}}
   106  					//   -----
   107  					startPos := kv.Pos()
   108  					endPos := innerLit.Lbrace + 1
   109  
   110  					// Delete the entire line if the key and its opening brace are
   111  					// together on their own line. This prevents leaving behind unneeded
   112  					// blank lines inside struct literals that `gofmt` will not remove.
   113  					// T{
   114  					// 		U: U{    <- delete entire line
   115  					// 			f: v,
   116  					//		}
   117  					//	}
   118  					//
   119  					tokFile := pass.Fset.File(kv.Pos())
   120  					lineOf := func(pos token.Pos) int {
   121  						return tokFile.PositionFor(pos, false).Line
   122  					}
   123  					curLine := lineOf(kv.Pos())
   124  					var prevLine int
   125  					if i == 0 {
   126  						// First element, so the previous line is the parent lbrace.
   127  						prevLine = lineOf(lit.Lbrace)
   128  					} else {
   129  						prevLine = lineOf(lit.Elts[i-1].End())
   130  					}
   131  
   132  					// We can safely delete the entire line if the key value expression is
   133  					// alone on its line: it starts on a new line relative to the previous
   134  					// element (prevLine < curLine), and the first element of the inner
   135  					// literal starts on a subsequent line.
   136  					if prevLine < curLine && curLine < tokFile.LineCount() && // (1-based)
   137  						lineOf(innerLit.Elts[0].Pos()) > curLine {
   138  						lineStart := tokFile.LineStart(curLine)
   139  						nextLineStart := tokFile.LineStart(curLine + 1)
   140  						// Check that there are no comments on the line we are going to delete.
   141  						if moreiters.Empty(astutil.Comments(file, lineStart, nextLineStart)) {
   142  							startPos = tokFile.LineStart(curLine)
   143  							endPos = nextLineStart
   144  						}
   145  					}
   146  
   147  					edits = append(edits, []analysis.TextEdit{
   148  						// T{U: U{f: v, ...}}
   149  						//   -----         -
   150  						{
   151  							// Delete the key and the opening brace of the inner struct literal.
   152  							Pos: startPos,
   153  							End: endPos,
   154  						},
   155  						{
   156  							// Delete the corresponding closing brace, including preceding
   157  							// white space or commas. Failing to delete trailing commas may
   158  							// result in invalid code.
   159  							Pos: closingPos,
   160  							End: innerLit.Rbrace + 1,
   161  						},
   162  					}...)
   163  					names = append(names, kv.Key.(*ast.Ident).Name)
   164  					checkLit(innerLit)
   165  				}
   166  			}
   167  		}
   168  	}
   169  	checkLit(lit)
   170  	if len(edits) > 0 {
   171  		pass.Report(analysis.Diagnostic{
   172  			Pos:     curLit.Node().Pos(),
   173  			End:     curLit.Node().End(),
   174  			Message: "embedded field type can be removed from struct literal",
   175  			SuggestedFixes: []analysis.SuggestedFix{
   176  				{
   177  					Message:   fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")),
   178  					TextEdits: edits,
   179  				},
   180  			},
   181  		})
   182  		return true
   183  	}
   184  	return false
   185  }
   186  
   187  // Pattern B: moving embedded field assignments inside the struct literal
   188  // initialization.
   189  // t := T{...}; t.x = x => t := T{..., x: x}
   190  // (or var t = ...)
   191  func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error {
   192  	compLit := curLit.Node().(*ast.CompositeLit)
   193  	if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool {
   194  		return is[*ast.KeyValueExpr](e)
   195  	}) {
   196  		// Promoting additional embedded fields would result in mixing keyed and
   197  		// unkeyed fields, which isn't allowed.
   198  		return nil
   199  	}
   200  	var (
   201  		// Ident for "t" in the assignment.
   202  		lhs *ast.Ident
   203  		// The cursor representing the statement that initializes the comp lit "t".
   204  		// We use its siblings to search for field assignments and verify that there
   205  		// are no intervening statements, in case those statements observe "t".
   206  		curStmt inspector.Cursor
   207  	)
   208  	switch curLit.ParentEdgeKind() {
   209  	case edge.AssignStmt_Rhs:
   210  		assign := curLit.Parent().Node().(*ast.AssignStmt)
   211  		// TODO(mkalil): Handle lhs forms that aren't idents, i.e. x.y[i] = T{...}.
   212  		// TODO(mkalil): Handle multi-assignments like t1, t2 := A{}, B{}
   213  		if len(assign.Lhs) != 1 {
   214  			return nil
   215  		}
   216  		if id, ok := assign.Lhs[0].(*ast.Ident); ok {
   217  			lhs = id
   218  			curStmt = curLit.Parent()
   219  		}
   220  	case edge.ValueSpec_Values:
   221  		spec := curLit.Parent().Node().(*ast.ValueSpec)
   222  		// TODO(mkalil): Handle multi-declarations like var (x = A{}; y = B{}) or var x, y = ...
   223  		if len(spec.Names) != 1 {
   224  			return nil
   225  		}
   226  		lhs = spec.Names[0]
   227  		if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok {
   228  			if gdecl, ok := decl.Node().(*ast.DeclStmt).Decl.(*ast.GenDecl); ok && len(gdecl.Specs) == 1 {
   229  				curStmt = decl
   230  			}
   231  		}
   232  	default:
   233  		return nil
   234  	}
   235  
   236  	if lhs == nil || !curStmt.Valid() {
   237  		return nil
   238  	}
   239  
   240  	var (
   241  		tObj = info.ObjectOf(lhs)
   242  		// Marks the contiguous block of embedded field assign statements that will
   243  		// be moved into the struct initialization.
   244  		firstStmt, lastStmt  inspector.Cursor
   245  		hasEmbeddedSelection bool
   246  	)
   247  stmtloop:
   248  	for {
   249  		var ok bool
   250  		curStmt, ok = curStmt.NextSibling()
   251  		if !ok {
   252  			break // end of (e.g.) block
   253  		}
   254  		// All embedded field value assignments must immediately follow the struct
   255  		// initialization.
   256  		assign, ok := curStmt.Node().(*ast.AssignStmt)
   257  		if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) {
   258  			// TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2
   259  			break
   260  		}
   261  		expr := assign.Lhs[0]
   262  		sel, ok := expr.(*ast.SelectorExpr)
   263  		if !ok {
   264  			break
   265  		}
   266  		// Verify that sel.X refers to the same object as "t"
   267  		selXId, ok := sel.X.(*ast.Ident)
   268  		if !ok {
   269  			// TODO(mkalil): handle deeply nested expressions like t.B.x
   270  			break
   271  		}
   272  		obj := info.ObjectOf(selXId)
   273  		if obj != tObj {
   274  			break
   275  		}
   276  		// The selection is from an embedded field if it directly
   277  		// assigns an embedded struct field (t.B = B{...}) or if
   278  		// the length of the index path is greater than one.
   279  		seln := info.Selections[sel]
   280  		if v, ok := seln.Obj().(*types.Var); ok && v.Embedded() ||
   281  			len(seln.Index()) > 1 {
   282  			hasEmbeddedSelection = true
   283  		}
   284  
   285  		rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0)
   286  		if uses(index, rhsCur, tObj) {
   287  			break
   288  		}
   289  		for c := range rhsCur.Preorder((*ast.Ident)(nil)) {
   290  			id := c.Node().(*ast.Ident)
   291  			// If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because
   292  			// we can't evaluate t.y when constructing the new literal.
   293  			if info.ObjectOf(id) == tObj {
   294  				break stmtloop
   295  			}
   296  			// Note: we don't need to worry about expressions with side effects
   297  			// changing the behavior when moved inside the comp lit. The order of
   298  			// effects will be preserved because we preserve the order of the key
   299  			// value pairs inside the comp lit.
   300  		}
   301  		if !firstStmt.Valid() {
   302  			firstStmt = curStmt
   303  		}
   304  		lastStmt = curStmt
   305  	}
   306  
   307  	if !firstStmt.Valid() || !hasEmbeddedSelection {
   308  		// We should not suggest a fix if none of the selections are from embedded fields.
   309  		return nil
   310  	}
   311  
   312  	file := astutil.EnclosingFile(curLit)
   313  	// Enable modernizer only for Go1.27.
   314  	if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
   315  		return nil
   316  	}
   317  
   318  	// Read file content to determine if the struct lit has a trailing comma
   319  	// after its last element.
   320  	tokFile := pass.Fset.File(compLit.Rbrace)
   321  	filename := tokFile.Name()
   322  	src, err := pass.ReadFile(filename)
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	hasTrailingComma := false
   328  	if len(compLit.Elts) > 0 {
   329  		lastElt := compLit.Elts[len(compLit.Elts)-1]
   330  		lastEltOffset := tokFile.Offset(lastElt.End())
   331  		rbraceOffset := tokFile.Offset(compLit.Rbrace)
   332  		hasTrailingComma = bytes.Contains(src[lastEltOffset:rbraceOffset], []byte(","))
   333  	}
   334  	var edits []analysis.TextEdit
   335  	// Emit edits to move the field assignment into the struct lit while
   336  	// removing it from its current place.
   337  	// t := T{...}; t.x = v
   338  	//           ----- --- -
   339  	// t := T{...,    x:  v}
   340  
   341  	// Add a trailing comma before the closing brace of compLit if one doesn't
   342  	// exist, and delete the closing brace itself.
   343  	// t := T{...}; t.x = v
   344  	//           -
   345  	// t := T{..., t.x = v
   346  	if len(compLit.Elts) > 0 && !hasTrailingComma {
   347  		edits = append(edits, analysis.TextEdit{
   348  			Pos:     compLit.Rbrace,
   349  			End:     compLit.Rbrace + 1,
   350  			NewText: []byte(","),
   351  		})
   352  	} else {
   353  		edits = append(edits, analysis.TextEdit{
   354  			Pos: compLit.Rbrace,
   355  			End: compLit.Rbrace + 1,
   356  		})
   357  	}
   358  
   359  	// For each assignment:
   360  	// t.x = v
   361  	// -- ---
   362  	//   x : v
   363  	curStmt = firstStmt
   364  	var prevStmt inspector.Cursor
   365  	for {
   366  		assign := curStmt.Node().(*ast.AssignStmt)
   367  		expr := assign.Lhs[0]
   368  		sel := expr.(*ast.SelectorExpr)
   369  		// Delete "t."
   370  		edits = append(edits, analysis.TextEdit{
   371  			Pos: assign.Pos(),
   372  			End: sel.Sel.Pos(),
   373  		})
   374  		// Replace "=" with ":"
   375  		edits = append(edits, analysis.TextEdit{
   376  			Pos:     expr.End(),
   377  			End:     assign.TokPos + 1,
   378  			NewText: []byte(":"),
   379  		})
   380  
   381  		// Add a comma after the previous assignment if this is not the first one.
   382  		if prevStmt.Valid() {
   383  			edits = append(edits, analysis.TextEdit{
   384  				Pos:     prevStmt.Node().End(),
   385  				NewText: []byte(","),
   386  			})
   387  		}
   388  
   389  		// For the last assignment, add the closing brace of the struct lit.
   390  		if curStmt == lastStmt {
   391  			edits = append(edits, analysis.TextEdit{
   392  				Pos:     assign.End(),
   393  				NewText: []byte("}"),
   394  			})
   395  			break
   396  		}
   397  		prevStmt = curStmt
   398  		curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt
   399  	}
   400  
   401  	pass.Report(analysis.Diagnostic{
   402  		Pos:     curLit.Node().Pos(),
   403  		End:     curLit.Node().End(),
   404  		Message: "embedded field assignment can be moved to struct literal",
   405  		SuggestedFixes: []analysis.SuggestedFix{
   406  			{
   407  				Message:   "Move embedded field assignment to struct literal",
   408  				TextEdits: edits,
   409  			},
   410  		},
   411  	})
   412  	return nil
   413  }
   414  
   415  // isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for
   416  // an embedded field for which we can safely remove its type.
   417  // If so, it returns the corresponding CompositeLit.
   418  // If elt contains an unkeyed field or ambiguous type, it returns nil.
   419  func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
   420  	obj := keyedField(info, kv)
   421  	if obj == nil || !obj.Embedded() {
   422  		return nil
   423  	}
   424  	lit, ok := kv.Value.(*ast.CompositeLit)
   425  	if !ok || len(lit.Elts) == 0 {
   426  		// Skip if the struct literal is empty.
   427  		return nil
   428  	}
   429  	// We cannot remove this type if any of its nested composite elements have
   430  	// unkeyed fields or are ambiguous, so we check for those conditions before
   431  	// returning.
   432  	for _, elt := range lit.Elts {
   433  		kv, ok := elt.(*ast.KeyValueExpr)
   434  		if !ok {
   435  			return nil
   436  		}
   437  		obj := keyedField(info, kv)
   438  		if obj == nil {
   439  			return nil
   440  		}
   441  		k := kv.Key.(*ast.Ident) // can't fail
   442  		// Cannot promote an ambiguous type, for example:
   443  		// type T struct { A; B }
   444  		// type A struct { x int }
   445  		// type B struct { x int }
   446  		// _ = T{A: A{x: 1}}
   447  		// cannot be simplified to T{x: 1} because T has two different embedded fields called "x".
   448  		// We also reject composite literals with slice elements, as parentObj will be nil.
   449  		parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
   450  		if parentObj != obj {
   451  			return nil
   452  		}
   453  	}
   454  	return lit
   455  }
   456  
   457  // keyedField reports whether the key of kv is an embedded field type. If so, it
   458  // returns the type of the embedded field, otherwise it returns nil.
   459  func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
   460  	k, ok := kv.Key.(*ast.Ident)
   461  	if !ok {
   462  		return nil
   463  	}
   464  	obj, ok := info.ObjectOf(k).(*types.Var)
   465  	if !ok || !obj.IsField() {
   466  		return nil
   467  	}
   468  	return obj
   469  }
   470  

View as plain text