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#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  		compLitType = info.TypeOf(compLit)
   242  		tObj        = info.ObjectOf(lhs)
   243  		// Marks the contiguous block of embedded field assign statements that will
   244  		// be moved into the struct initialization.
   245  		firstStmt, lastStmt  inspector.Cursor
   246  		hasEmbeddedSelection bool
   247  	)
   248  	if compLitType == nil {
   249  		return nil
   250  	}
   251  
   252  	// Record the index paths of the existing fields in the composite literal. Two
   253  	// fields in a composite literal conflict if one field's path is a prefix of
   254  	// the other's. If the field in an assignment conflicts with an existing
   255  	// field, we won't suggest a fix to move it into the struct literal.
   256  	var fieldPaths [][]int
   257  	for _, elt := range compLit.Elts {
   258  		k, ok := elt.(*ast.KeyValueExpr).Key.(*ast.Ident)
   259  		if !ok {
   260  			return nil
   261  		}
   262  		_, idx, _ := types.LookupFieldOrMethod(compLitType, true, pass.Pkg, k.Name)
   263  		if len(idx) == 0 {
   264  			return nil
   265  		}
   266  		fieldPaths = append(fieldPaths, idx)
   267  	}
   268  
   269  stmtloop:
   270  	for {
   271  		var ok bool
   272  		curStmt, ok = curStmt.NextSibling()
   273  		if !ok {
   274  			break // end of (e.g.) block
   275  		}
   276  		// All embedded field value assignments must immediately follow the struct
   277  		// initialization.
   278  		assign, ok := curStmt.Node().(*ast.AssignStmt)
   279  		if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) {
   280  			// TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2
   281  			break
   282  		}
   283  		expr := assign.Lhs[0]
   284  		sel, ok := expr.(*ast.SelectorExpr)
   285  		if !ok {
   286  			break
   287  		}
   288  		// Verify that sel.X refers to the same object as "t"
   289  		selXId, ok := sel.X.(*ast.Ident)
   290  		if !ok {
   291  			// TODO(mkalil): handle deeply nested expressions like t.B.x
   292  			break
   293  		}
   294  		obj := info.ObjectOf(selXId)
   295  		if obj != tObj {
   296  			break
   297  		}
   298  		fieldObj, assignIdx, indirect := types.LookupFieldOrMethod(compLitType, true, pass.Pkg, sel.Sel.Name)
   299  		fieldVar, ok := fieldObj.(*types.Var)
   300  		if !ok || len(assignIdx) == 0 || indirect { // don't allow accessing promoted fields through implicit pointer indirection
   301  			break
   302  		}
   303  		// A composite literal cannot specify both an enclosing embedded field and a promoted
   304  		// field from within it, nor duplicate fields.
   305  		if slices.ContainsFunc(fieldPaths, func(index []int) bool { return pathConflicts(index, assignIdx) }) {
   306  			break
   307  		}
   308  		// The selection is from an embedded field if it directly
   309  		// assigns an embedded struct field (t.B = B{...}) or if
   310  		// the length of the index path is greater than one.
   311  		if fieldVar.Embedded() || len(assignIdx) > 1 {
   312  			hasEmbeddedSelection = true
   313  		}
   314  
   315  		rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0)
   316  		if uses(index, rhsCur, tObj) {
   317  			break
   318  		}
   319  		for c := range rhsCur.Preorder((*ast.Ident)(nil)) {
   320  			id := c.Node().(*ast.Ident)
   321  			// If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because
   322  			// we can't evaluate t.y when constructing the new literal.
   323  			if info.ObjectOf(id) == tObj {
   324  				break stmtloop
   325  			}
   326  			// Note: we don't need to worry about expressions with side effects
   327  			// changing the behavior when moved inside the comp lit. The order of
   328  			// effects will be preserved because we preserve the order of the key
   329  			// value pairs inside the comp lit.
   330  		}
   331  		// We might move multiple sequential assignment statements into
   332  		// the struct literal, so we need to keep track of the index path
   333  		// of this assignment to check it against subsequent assignments.
   334  		fieldPaths = append(fieldPaths, assignIdx)
   335  		if !firstStmt.Valid() {
   336  			firstStmt = curStmt
   337  		}
   338  		lastStmt = curStmt
   339  	}
   340  
   341  	if !firstStmt.Valid() || !hasEmbeddedSelection {
   342  		// We should not suggest a fix if none of the selections are from embedded fields.
   343  		return nil
   344  	}
   345  
   346  	file := astutil.EnclosingFile(curLit)
   347  	// Enable modernizer only for Go1.27.
   348  	if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
   349  		return nil
   350  	}
   351  
   352  	// Read file content to determine if the struct lit has a trailing comma
   353  	// after its last element.
   354  	tokFile := pass.Fset.File(compLit.Rbrace)
   355  	filename := tokFile.Name()
   356  	src, err := pass.ReadFile(filename)
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	hasTrailingComma := false
   362  	if len(compLit.Elts) > 0 {
   363  		lastElt := compLit.Elts[len(compLit.Elts)-1]
   364  		lastEltOffset := tokFile.Offset(lastElt.End())
   365  		rbraceOffset := tokFile.Offset(compLit.Rbrace)
   366  		span := bytes.Clone(src[lastEltOffset:rbraceOffset])
   367  		// Zero out any comments in the span, so that a comma within
   368  		// one is not mistaken for the literal's trailing comma.
   369  		for co := range astutil.Comments(file, lastElt.End(), compLit.Rbrace) {
   370  			start := max(tokFile.Offset(co.Pos())-lastEltOffset, 0)
   371  			end := min(tokFile.Offset(co.End())-lastEltOffset, len(span))
   372  			if start < end {
   373  				clear(span[start:end])
   374  			}
   375  		}
   376  		hasTrailingComma = bytes.Contains(span, []byte(","))
   377  	}
   378  	var edits []analysis.TextEdit
   379  	// Emit edits to move the field assignment into the struct lit while
   380  	// removing it from its current place.
   381  	// t := T{...}; t.x = v
   382  	//           ----- --- -
   383  	// t := T{...,    x:  v}
   384  
   385  	// Add a trailing comma before the closing brace of compLit if one doesn't
   386  	// exist, and delete the closing brace itself.
   387  	// t := T{...}; t.x = v
   388  	//           -
   389  	// t := T{..., t.x = v
   390  	if len(compLit.Elts) > 0 && !hasTrailingComma {
   391  		edits = append(edits, analysis.TextEdit{
   392  			Pos:     compLit.Rbrace,
   393  			End:     compLit.Rbrace + 1,
   394  			NewText: []byte(","),
   395  		})
   396  	} else {
   397  		edits = append(edits, analysis.TextEdit{
   398  			Pos: compLit.Rbrace,
   399  			End: compLit.Rbrace + 1,
   400  		})
   401  	}
   402  
   403  	// For each assignment:
   404  	// t.x = v
   405  	// -- ---
   406  	//   x : v
   407  	curStmt = firstStmt
   408  	var prevStmt inspector.Cursor
   409  	for {
   410  		assign := curStmt.Node().(*ast.AssignStmt)
   411  		expr := assign.Lhs[0]
   412  		sel := expr.(*ast.SelectorExpr)
   413  		// Delete "t."
   414  		edits = append(edits, analysis.TextEdit{
   415  			Pos: assign.Pos(),
   416  			End: sel.Sel.Pos(),
   417  		})
   418  		// Replace "=" with ":"
   419  		edits = append(edits, analysis.TextEdit{
   420  			Pos:     expr.End(),
   421  			End:     assign.TokPos + 1,
   422  			NewText: []byte(":"),
   423  		})
   424  
   425  		// Add a comma after the previous assignment if this is not the first one.
   426  		if prevStmt.Valid() {
   427  			edits = append(edits, analysis.TextEdit{
   428  				Pos:     prevStmt.Node().End(),
   429  				NewText: []byte(","),
   430  			})
   431  		}
   432  
   433  		// For the last assignment, add the closing brace of the struct lit.
   434  		if curStmt == lastStmt {
   435  			edits = append(edits, analysis.TextEdit{
   436  				Pos:     assign.End(),
   437  				NewText: []byte("}"),
   438  			})
   439  			break
   440  		}
   441  		prevStmt = curStmt
   442  		curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt
   443  	}
   444  
   445  	pass.Report(analysis.Diagnostic{
   446  		Pos:     curLit.Node().Pos(),
   447  		End:     curLit.Node().End(),
   448  		Message: "embedded field assignment can be moved to struct literal",
   449  		SuggestedFixes: []analysis.SuggestedFix{
   450  			{
   451  				Message:   "Move embedded field assignment to struct literal",
   452  				TextEdits: edits,
   453  			},
   454  		},
   455  	})
   456  	return nil
   457  }
   458  
   459  // isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for
   460  // an embedded field for which we can safely remove its type.
   461  // If so, it returns the corresponding CompositeLit.
   462  // If elt contains an unkeyed field or ambiguous type, it returns nil.
   463  func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
   464  	obj := keyedField(info, kv)
   465  	if obj == nil || !obj.Embedded() {
   466  		return nil
   467  	}
   468  	lit, ok := kv.Value.(*ast.CompositeLit)
   469  	if !ok || len(lit.Elts) == 0 {
   470  		// Skip if the struct literal is empty.
   471  		return nil
   472  	}
   473  	// We cannot remove this type if any of its nested composite elements have
   474  	// unkeyed fields or are ambiguous, so we check for those conditions before
   475  	// returning.
   476  	for _, elt := range lit.Elts {
   477  		kv, ok := elt.(*ast.KeyValueExpr)
   478  		if !ok {
   479  			return nil
   480  		}
   481  		obj := keyedField(info, kv)
   482  		if obj == nil {
   483  			return nil
   484  		}
   485  		k := kv.Key.(*ast.Ident) // can't fail
   486  		// Cannot promote an ambiguous type, for example:
   487  		// type T struct { A; B }
   488  		// type A struct { x int }
   489  		// type B struct { x int }
   490  		// _ = T{A: A{x: 1}}
   491  		// cannot be simplified to T{x: 1} because T has two different embedded fields called "x".
   492  		// We also reject composite literals with slice elements, as parentObj will be nil.
   493  		parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
   494  		if parentObj != obj {
   495  			return nil
   496  		}
   497  	}
   498  	return lit
   499  }
   500  
   501  // keyedField reports whether the key of kv is an embedded field type. If so, it
   502  // returns the type of the embedded field, otherwise it returns nil.
   503  func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
   504  	k, ok := kv.Key.(*ast.Ident)
   505  	if !ok {
   506  		return nil
   507  	}
   508  	obj, ok := info.ObjectOf(k).(*types.Var)
   509  	if !ok || !obj.IsField() {
   510  		return nil
   511  	}
   512  	return obj
   513  }
   514  
   515  // pathConflicts reports whether the specified index paths conflict.
   516  // Two index paths conflict if one is a prefix of the other.
   517  func pathConflicts(p1, p2 []int) bool {
   518  	return (len(p1) >= len(p2) && slices.Equal(p1[:len(p2)], p2)) ||
   519  		(len(p2) >= len(p1) && slices.Equal(p2[:len(p1)], p1))
   520  }
   521  

View as plain text