Source file src/cmd/vendor/golang.org/x/tools/internal/refactor/delete.go

     1  // Copyright 2025 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 refactor
     6  
     7  // This file defines operations for computing deletion edits.
     8  
     9  import (
    10  	"fmt"
    11  	"go/ast"
    12  	"go/token"
    13  	"go/types"
    14  	"slices"
    15  
    16  	"golang.org/x/tools/go/ast/edge"
    17  	"golang.org/x/tools/go/ast/inspector"
    18  	"golang.org/x/tools/internal/astutil"
    19  	"golang.org/x/tools/internal/typesinternal"
    20  	"golang.org/x/tools/internal/typesinternal/typeindex"
    21  )
    22  
    23  // DeleteVar returns edits to delete the declaration of a variable or
    24  // constant whose defining identifier is curId.
    25  //
    26  // It handles variants including:
    27  // - GenDecl > ValueSpec versus AssignStmt;
    28  // - RHS expression has effects, or not;
    29  // - entire statement/declaration may be eliminated;
    30  // and removes associated comments.
    31  //
    32  // If it cannot make the necessary edits, such as for a function
    33  // parameter or result, it returns nil.
    34  func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []Edit {
    35  	switch ek, _ := curId.ParentEdge(); ek {
    36  	case edge.ValueSpec_Names:
    37  		return deleteVarFromValueSpec(tokFile, info, curId)
    38  
    39  	case edge.AssignStmt_Lhs:
    40  		return deleteVarFromAssignStmt(tokFile, info, curId)
    41  	}
    42  
    43  	// e.g. function receiver, parameter, or result,
    44  	// or "switch v := expr.(T) {}" (which has no object).
    45  	return nil
    46  }
    47  
    48  // deleteVarFromValueSpec returns edits to delete the declaration of a
    49  // variable or constant within a ValueSpec.
    50  //
    51  // Precondition: curId is Ident beneath ValueSpec.Names beneath GenDecl.
    52  //
    53  // See also [deleteVarFromAssignStmt], which has parallel structure.
    54  func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []Edit {
    55  	var (
    56  		id      = curIdent.Node().(*ast.Ident)
    57  		curSpec = curIdent.Parent()
    58  		spec    = curSpec.Node().(*ast.ValueSpec)
    59  	)
    60  
    61  	declaresOtherNames := slices.ContainsFunc(spec.Names, func(name *ast.Ident) bool {
    62  		return name != id && name.Name != "_"
    63  	})
    64  	noRHSEffects := !slices.ContainsFunc(spec.Values, func(rhs ast.Expr) bool {
    65  		return !typesinternal.NoEffects(info, rhs)
    66  	})
    67  	if !declaresOtherNames && noRHSEffects {
    68  		// The spec is no longer needed, either to declare
    69  		// other variables, or for its side effects.
    70  		return DeleteSpec(tokFile, curSpec)
    71  	}
    72  
    73  	// The spec is still needed, either for
    74  	// at least one LHS, or for effects on RHS.
    75  	// Blank out or delete just one LHS.
    76  
    77  	_, index := curIdent.ParentEdge() // index of LHS within ValueSpec.Names
    78  
    79  	// If there is no RHS, we can delete the LHS.
    80  	if len(spec.Values) == 0 {
    81  		var pos, end token.Pos
    82  		if index == len(spec.Names)-1 {
    83  			// Delete final name.
    84  			//
    85  			// var _, lhs1 T
    86  			//      ------
    87  			pos = spec.Names[index-1].End()
    88  			end = spec.Names[index].End()
    89  		} else {
    90  			// Delete non-final name.
    91  			//
    92  			// var lhs0, _ T
    93  			//     ------
    94  			pos = spec.Names[index].Pos()
    95  			end = spec.Names[index+1].Pos()
    96  		}
    97  		return []Edit{{
    98  			Pos: pos,
    99  			End: end,
   100  		}}
   101  	}
   102  
   103  	// If the assignment is n:n and the RHS has no effects,
   104  	// we can delete the LHS and its corresponding RHS.
   105  	if len(spec.Names) == len(spec.Values) &&
   106  		typesinternal.NoEffects(info, spec.Values[index]) {
   107  
   108  		if index == len(spec.Names)-1 {
   109  			// Delete final items.
   110  			//
   111  			// var _, lhs1 = rhs0, rhs1
   112  			//      ------       ------
   113  			return []Edit{
   114  				{
   115  					Pos: spec.Names[index-1].End(),
   116  					End: spec.Names[index].End(),
   117  				},
   118  				{
   119  					Pos: spec.Values[index-1].End(),
   120  					End: spec.Values[index].End(),
   121  				},
   122  			}
   123  		} else {
   124  			// Delete non-final items.
   125  			//
   126  			// var lhs0, _ = rhs0, rhs1
   127  			//     ------    ------
   128  			return []Edit{
   129  				{
   130  					Pos: spec.Names[index].Pos(),
   131  					End: spec.Names[index+1].Pos(),
   132  				},
   133  				{
   134  					Pos: spec.Values[index].Pos(),
   135  					End: spec.Values[index+1].Pos(),
   136  				},
   137  			}
   138  		}
   139  	}
   140  
   141  	// We cannot delete the RHS.
   142  	// Blank out the LHS.
   143  	return []Edit{{
   144  		Pos:     id.Pos(),
   145  		End:     id.End(),
   146  		NewText: []byte("_"),
   147  	}}
   148  }
   149  
   150  // Precondition: curId is Ident beneath AssignStmt.Lhs.
   151  //
   152  // See also [deleteVarFromValueSpec], which has parallel structure.
   153  func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []Edit {
   154  	var (
   155  		id      = curIdent.Node().(*ast.Ident)
   156  		curStmt = curIdent.Parent()
   157  		assign  = curStmt.Node().(*ast.AssignStmt)
   158  	)
   159  
   160  	declaresOtherNames := slices.ContainsFunc(assign.Lhs, func(lhs ast.Expr) bool {
   161  		lhsId, ok := lhs.(*ast.Ident)
   162  		return ok && lhsId != id && lhsId.Name != "_"
   163  	})
   164  	noRHSEffects := !slices.ContainsFunc(assign.Rhs, func(rhs ast.Expr) bool {
   165  		return !typesinternal.NoEffects(info, rhs)
   166  	})
   167  	if !declaresOtherNames && noRHSEffects {
   168  		// The assignment is no longer needed, either to
   169  		// declare other variables, or for its side effects.
   170  		if edits := DeleteStmt(tokFile, curStmt); edits != nil {
   171  			return edits
   172  		}
   173  		// Statement could not not be deleted in this context.
   174  		// Fall back to conservative deletion.
   175  	}
   176  
   177  	// The assign is still needed, either for
   178  	// at least one LHS, or for effects on RHS,
   179  	// or because it cannot deleted because of its context.
   180  	// Blank out or delete just one LHS.
   181  
   182  	// If the assignment is 1:1 and the RHS has no effects,
   183  	// we can delete the LHS and its corresponding RHS.
   184  	_, index := curIdent.ParentEdge()
   185  	if len(assign.Lhs) > 1 &&
   186  		len(assign.Lhs) == len(assign.Rhs) &&
   187  		typesinternal.NoEffects(info, assign.Rhs[index]) {
   188  
   189  		if index == len(assign.Lhs)-1 {
   190  			// Delete final items.
   191  			//
   192  			// _, lhs1 := rhs0, rhs1
   193  			//  ------        ------
   194  			return []Edit{
   195  				{
   196  					Pos: assign.Lhs[index-1].End(),
   197  					End: assign.Lhs[index].End(),
   198  				},
   199  				{
   200  					Pos: assign.Rhs[index-1].End(),
   201  					End: assign.Rhs[index].End(),
   202  				},
   203  			}
   204  		} else {
   205  			// Delete non-final items.
   206  			//
   207  			// lhs0, _ := rhs0, rhs1
   208  			// ------     ------
   209  			return []Edit{
   210  				{
   211  					Pos: assign.Lhs[index].Pos(),
   212  					End: assign.Lhs[index+1].Pos(),
   213  				},
   214  				{
   215  					Pos: assign.Rhs[index].Pos(),
   216  					End: assign.Rhs[index+1].Pos(),
   217  				},
   218  			}
   219  		}
   220  	}
   221  
   222  	// We cannot delete the RHS.
   223  	// Blank out the LHS.
   224  	edits := []Edit{{
   225  		Pos:     id.Pos(),
   226  		End:     id.End(),
   227  		NewText: []byte("_"),
   228  	}}
   229  
   230  	// If this eliminates the final variable declared by
   231  	// an := statement, we need to turn it into an =
   232  	// assignment to avoid a "no new variables on left
   233  	// side of :=" error.
   234  	if !declaresOtherNames {
   235  		edits = append(edits, Edit{
   236  			Pos:     assign.TokPos,
   237  			End:     assign.TokPos + token.Pos(len(":=")),
   238  			NewText: []byte("="),
   239  		})
   240  	}
   241  
   242  	return edits
   243  }
   244  
   245  // DeleteSpec returns edits to delete the {Type,Value}Spec identified by curSpec.
   246  //
   247  // TODO(adonovan): add test suite. Test for consts as well.
   248  func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []Edit {
   249  	var (
   250  		spec    = curSpec.Node().(ast.Spec)
   251  		curDecl = curSpec.Parent()
   252  		decl    = curDecl.Node().(*ast.GenDecl)
   253  	)
   254  
   255  	// If it is the sole spec in the decl,
   256  	// delete the entire decl.
   257  	if len(decl.Specs) == 1 {
   258  		return DeleteDecl(tokFile, curDecl)
   259  	}
   260  
   261  	// Delete the spec and its comments.
   262  	_, index := curSpec.ParentEdge() // index of ValueSpec within GenDecl.Specs
   263  	pos, end := spec.Pos(), spec.End()
   264  	if doc := astutil.DocComment(spec); doc != nil {
   265  		pos = doc.Pos() // leading comment
   266  	}
   267  	if index == len(decl.Specs)-1 {
   268  		// Delete final spec.
   269  		if c := eolComment(spec); c != nil {
   270  			//  var (v int // comment \n)
   271  			end = c.End()
   272  		}
   273  	} else {
   274  		// Delete non-final spec.
   275  		//   var ( a T; b T )
   276  		//         -----
   277  		end = decl.Specs[index+1].Pos()
   278  	}
   279  	return []Edit{{
   280  		Pos: pos,
   281  		End: end,
   282  	}}
   283  }
   284  
   285  // DeleteDecl returns edits to delete the ast.Decl identified by curDecl.
   286  //
   287  // TODO(adonovan): add test suite.
   288  func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []Edit {
   289  	decl := curDecl.Node().(ast.Decl)
   290  
   291  	ek, _ := curDecl.ParentEdge()
   292  	switch ek {
   293  	case edge.DeclStmt_Decl:
   294  		return DeleteStmt(tokFile, curDecl.Parent())
   295  
   296  	case edge.File_Decls:
   297  		pos, end := decl.Pos(), decl.End()
   298  		if doc := astutil.DocComment(decl); doc != nil {
   299  			pos = doc.Pos()
   300  		}
   301  
   302  		// Delete free-floating comments on same line as rparen.
   303  		//    var (...) // comment
   304  		var (
   305  			file        = curDecl.Parent().Node().(*ast.File)
   306  			lineOf      = tokFile.Line
   307  			declEndLine = lineOf(decl.End())
   308  		)
   309  		for _, cg := range file.Comments {
   310  			for _, c := range cg.List {
   311  				if c.Pos() < end {
   312  					continue // too early
   313  				}
   314  				commentEndLine := lineOf(c.End())
   315  				if commentEndLine > declEndLine {
   316  					break // too late
   317  				} else if lineOf(c.Pos()) == declEndLine && commentEndLine == declEndLine {
   318  					end = c.End()
   319  				}
   320  			}
   321  		}
   322  
   323  		return []Edit{{
   324  			Pos: pos,
   325  			End: end,
   326  		}}
   327  
   328  	default:
   329  		panic(fmt.Sprintf("Decl parent is %v, want DeclStmt or File", ek))
   330  	}
   331  }
   332  
   333  // find leftmost Pos bigger than start and rightmost less than end
   334  func filterPos(nds []*ast.Comment, start, end token.Pos) (token.Pos, token.Pos, bool) {
   335  	l, r := end, token.NoPos
   336  	ok := false
   337  	for _, n := range nds {
   338  		if n.Pos() > start && n.Pos() < l {
   339  			l = n.Pos()
   340  			ok = true
   341  		}
   342  		if n.End() <= end && n.End() > r {
   343  			r = n.End()
   344  			ok = true
   345  		}
   346  	}
   347  	return l, r, ok
   348  }
   349  
   350  // DeleteStmt returns the edits to remove the [ast.Stmt] identified by
   351  // curStmt if it recognizes the context. It returns nil otherwise.
   352  // TODO(pjw, adonovan): it should not return nil, it should return an error
   353  //
   354  // DeleteStmt is called with just the AST so it has trouble deciding if
   355  // a comment is associated with the statement to be deleted. For instance,
   356  //
   357  //	for /*A*/ init()/*B*/;/*C/cond()/*D/;/*E*/post() /*F*/ { /*G*/}
   358  //
   359  // comment B and C are indistinguishable, as are D and E. That is, as the
   360  // AST does not say where the semicolons are, B and C could go either
   361  // with the init() or the cond(), so cannot be removed safely. The same
   362  // is true for D, E, and the post(). (And there are other similar cases.)
   363  // But the other comments can be removed as they are unambiguously
   364  // associated with the statement being deleted. In particular,
   365  // it removes whole lines like
   366  //
   367  //	stmt // comment
   368  func DeleteStmt(file *token.File, curStmt inspector.Cursor) []Edit {
   369  	// if the stmt is on a line by itself, or a range of lines, delete the whole thing
   370  	// including comments. Except for the heads of switches, type
   371  	// switches, and for-statements that's the usual case. Complexity occurs where
   372  	// there are multiple statements on the same line, and adjacent comments.
   373  
   374  	// In that case we remove some adjacent comments:
   375  	// In me()/*A*/;b(), comment A cannot be removed, because the ast
   376  	// is indistinguishable from me();/*A*/b()
   377  	// and the same for cases like switch me()/*A*/; x.(type) {
   378  
   379  	// this would be more precise with the file contents, or if the ast
   380  	// contained the location of semicolons
   381  	var (
   382  		stmt          = curStmt.Node().(ast.Stmt)
   383  		tokFile       = file
   384  		lineOf        = tokFile.Line
   385  		stmtStartLine = lineOf(stmt.Pos())
   386  		stmtEndLine   = lineOf(stmt.End())
   387  
   388  		leftSyntax, rightSyntax     token.Pos      // pieces of parent node on stmt{Start,End}Line
   389  		leftComments, rightComments []*ast.Comment // comments before/after stmt on the same line
   390  	)
   391  
   392  	// remember the Pos that are on the same line as stmt
   393  	use := func(left, right token.Pos) {
   394  		if lineOf(left) == stmtStartLine {
   395  			leftSyntax = left
   396  		}
   397  		if lineOf(right) == stmtEndLine {
   398  			rightSyntax = right
   399  		}
   400  	}
   401  
   402  	// find the comments, if any, on the same line
   403  Big:
   404  	for _, cg := range astutil.EnclosingFile(curStmt).Comments {
   405  		for _, co := range cg.List {
   406  			if lineOf(co.End()) < stmtStartLine {
   407  				continue
   408  			} else if lineOf(co.Pos()) > stmtEndLine {
   409  				break Big // no more are possible
   410  			}
   411  			if lineOf(co.End()) == stmtStartLine && co.End() <= stmt.Pos() {
   412  				// comment is before the statement
   413  				leftComments = append(leftComments, co)
   414  			} else if lineOf(co.Pos()) == stmtEndLine && co.Pos() >= stmt.End() {
   415  				// comment is after the statement
   416  				rightComments = append(rightComments, co)
   417  			}
   418  		}
   419  	}
   420  
   421  	// find any other syntax on the same line
   422  	var (
   423  		leftStmt, rightStmt token.Pos // end/start positions of sibling statements in a []Stmt list
   424  		inStmtList          = false
   425  		curParent           = curStmt.Parent()
   426  	)
   427  	switch parent := curParent.Node().(type) {
   428  	case *ast.BlockStmt:
   429  		use(parent.Lbrace, parent.Rbrace)
   430  		inStmtList = true
   431  	case *ast.CaseClause:
   432  		use(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
   433  		inStmtList = true
   434  	case *ast.CommClause:
   435  		if parent.Comm == stmt {
   436  			return nil // maybe the user meant to remove the entire CommClause?
   437  		}
   438  		use(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
   439  		inStmtList = true
   440  	case *ast.ForStmt:
   441  		use(parent.For, parent.Body.Lbrace)
   442  		// special handling, as init;cond;post BlockStmt is not a statment list
   443  		if parent.Init != nil && parent.Cond != nil && stmt == parent.Init && lineOf(parent.Cond.Pos()) == lineOf(stmt.End()) {
   444  			rightStmt = parent.Cond.Pos()
   445  		} else if parent.Post != nil && parent.Cond != nil && stmt == parent.Post && lineOf(parent.Cond.End()) == lineOf(stmt.Pos()) {
   446  			leftStmt = parent.Cond.End()
   447  		}
   448  	case *ast.IfStmt:
   449  		switch stmt {
   450  		case parent.Init:
   451  			use(parent.If, parent.Body.Lbrace)
   452  		case parent.Else:
   453  			// stmt is the {...} in "if cond {} else {...}" and removing
   454  			// it would require removing the 'else' keyword, but the ast
   455  			// does not contain its position.
   456  			return nil
   457  		}
   458  	case *ast.SwitchStmt:
   459  		use(parent.Switch, parent.Body.Lbrace)
   460  	case *ast.TypeSwitchStmt:
   461  		if stmt == parent.Assign {
   462  			return nil // don't remove .(type)
   463  		}
   464  		use(parent.Switch, parent.Body.Lbrace)
   465  	default:
   466  		return nil // not one of ours
   467  	}
   468  
   469  	if inStmtList {
   470  		// find the siblings, if any, on the same line
   471  		if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
   472  			if _, ok := prev.Node().(ast.Stmt); ok {
   473  				leftStmt = prev.Node().End() // preceding statement ends on same line
   474  			}
   475  		}
   476  		if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
   477  			rightStmt = next.Node().Pos() // following statement begins on same line
   478  		}
   479  	}
   480  
   481  	// compute the left and right limits of the edit
   482  	var leftEdit, rightEdit token.Pos
   483  	if leftStmt.IsValid() {
   484  		leftEdit = stmt.Pos() // can't remove preceding comments: a()/*A*/; me()
   485  	} else if leftSyntax.IsValid() {
   486  		// remove intervening leftComments
   487  		if a, _, ok := filterPos(leftComments, leftSyntax, stmt.Pos()); ok {
   488  			leftEdit = a
   489  		} else {
   490  			leftEdit = stmt.Pos()
   491  		}
   492  	} else { // remove whole line
   493  		for leftEdit = stmt.Pos(); lineOf(leftEdit) == stmtStartLine; leftEdit-- {
   494  		}
   495  		if leftEdit < stmt.Pos() {
   496  			leftEdit++ // beginning of line
   497  		}
   498  	}
   499  	if rightStmt.IsValid() {
   500  		rightEdit = stmt.End() // can't remove following comments
   501  	} else if rightSyntax.IsValid() {
   502  		// remove intervening rightComments
   503  		if _, b, ok := filterPos(rightComments, stmt.End(), rightSyntax); ok {
   504  			rightEdit = b
   505  		} else {
   506  			rightEdit = stmt.End()
   507  		}
   508  	} else { // remove whole line
   509  		fend := token.Pos(file.Base()) + token.Pos(file.Size())
   510  		for rightEdit = stmt.End(); fend >= rightEdit && lineOf(rightEdit) == stmtEndLine; rightEdit++ {
   511  		}
   512  		// don't remove \n if there was other stuff earlier
   513  		if leftSyntax.IsValid() || leftStmt.IsValid() {
   514  			rightEdit--
   515  		}
   516  	}
   517  
   518  	return []Edit{{Pos: leftEdit, End: rightEdit}}
   519  }
   520  
   521  // DeleteUnusedVars computes the edits required to delete the
   522  // declarations of any local variables whose last uses are in the
   523  // curDelend subtree, which is about to be deleted.
   524  func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []Edit {
   525  	// TODO(adonovan): we might want to generalize this by
   526  	// splitting the two phases below, so that we can gather
   527  	// across a whole sequence of deletions then finally compute the
   528  	// set of variables that are no longer wanted.
   529  
   530  	// Count number of deletions of each var.
   531  	delcount := make(map[*types.Var]int)
   532  	for curId := range curDelend.Preorder((*ast.Ident)(nil)) {
   533  		id := curId.Node().(*ast.Ident)
   534  		if v, ok := info.Uses[id].(*types.Var); ok &&
   535  			typesinternal.GetVarKind(v) == typesinternal.LocalVar { // always false before go1.25
   536  			delcount[v]++
   537  		}
   538  	}
   539  
   540  	// Delete declaration of each var that became unused.
   541  	var edits []Edit
   542  	for v, count := range delcount {
   543  		if len(slices.Collect(index.Uses(v))) == count {
   544  			if curDefId, ok := index.Def(v); ok {
   545  				edits = append(edits, DeleteVar(tokFile, info, curDefId)...)
   546  			}
   547  		}
   548  	}
   549  	return edits
   550  }
   551  
   552  func eolComment(n ast.Node) *ast.CommentGroup {
   553  	// TODO(adonovan): support:
   554  	//    func f() {...} // comment
   555  	switch n := n.(type) {
   556  	case *ast.GenDecl:
   557  		if !n.TokPos.IsValid() && len(n.Specs) == 1 {
   558  			return eolComment(n.Specs[0])
   559  		}
   560  	case *ast.ValueSpec:
   561  		return n.Comment
   562  	case *ast.TypeSpec:
   563  		return n.Comment
   564  	}
   565  	return nil
   566  }
   567  

View as plain text