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/analysis"
    17  	"golang.org/x/tools/go/ast/edge"
    18  	"golang.org/x/tools/go/ast/inspector"
    19  	"golang.org/x/tools/internal/astutil"
    20  	"golang.org/x/tools/internal/typesinternal"
    21  	"golang.org/x/tools/internal/typesinternal/typeindex"
    22  )
    23  
    24  // DeleteVar returns edits to delete the declaration of a variable or
    25  // constant whose defining identifier is curId.
    26  //
    27  // It handles variants including:
    28  // - GenDecl > ValueSpec versus AssignStmt;
    29  // - RHS expression has effects, or not;
    30  // - entire statement/declaration may be eliminated;
    31  // and removes associated comments.
    32  //
    33  // If it cannot make the necessary edits, such as for a function
    34  // parameter or result, it returns nil.
    35  func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []analysis.TextEdit {
    36  	switch ek, _ := curId.ParentEdge(); ek {
    37  	case edge.ValueSpec_Names:
    38  		return deleteVarFromValueSpec(tokFile, info, curId)
    39  
    40  	case edge.AssignStmt_Lhs:
    41  		return deleteVarFromAssignStmt(tokFile, info, curId)
    42  	}
    43  
    44  	// e.g. function receiver, parameter, or result,
    45  	// or "switch v := expr.(T) {}" (which has no object).
    46  	return nil
    47  }
    48  
    49  // deleteVarFromValueSpec returns edits to delete the declaration of a
    50  // variable or constant within a ValueSpec.
    51  //
    52  // Precondition: curId is Ident beneath ValueSpec.Names beneath GenDecl.
    53  //
    54  // See also [deleteVarFromAssignStmt], which has parallel structure.
    55  func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
    56  	var (
    57  		id      = curIdent.Node().(*ast.Ident)
    58  		curSpec = curIdent.Parent()
    59  		spec    = curSpec.Node().(*ast.ValueSpec)
    60  	)
    61  
    62  	declaresOtherNames := slices.ContainsFunc(spec.Names, func(name *ast.Ident) bool {
    63  		return name != id && name.Name != "_"
    64  	})
    65  	noRHSEffects := !slices.ContainsFunc(spec.Values, func(rhs ast.Expr) bool {
    66  		return !typesinternal.NoEffects(info, rhs)
    67  	})
    68  	if !declaresOtherNames && noRHSEffects {
    69  		// The spec is no longer needed, either to declare
    70  		// other variables, or for its side effects.
    71  		return DeleteSpec(tokFile, curSpec)
    72  	}
    73  
    74  	// The spec is still needed, either for
    75  	// at least one LHS, or for effects on RHS.
    76  	// Blank out or delete just one LHS.
    77  
    78  	_, index := curIdent.ParentEdge() // index of LHS within ValueSpec.Names
    79  
    80  	// If there is no RHS, we can delete the LHS.
    81  	if len(spec.Values) == 0 {
    82  		var pos, end token.Pos
    83  		if index == len(spec.Names)-1 {
    84  			// Delete final name.
    85  			//
    86  			// var _, lhs1 T
    87  			//      ------
    88  			pos = spec.Names[index-1].End()
    89  			end = spec.Names[index].End()
    90  		} else {
    91  			// Delete non-final name.
    92  			//
    93  			// var lhs0, _ T
    94  			//     ------
    95  			pos = spec.Names[index].Pos()
    96  			end = spec.Names[index+1].Pos()
    97  		}
    98  		return []analysis.TextEdit{{
    99  			Pos: pos,
   100  			End: end,
   101  		}}
   102  	}
   103  
   104  	// If the assignment is n:n and the RHS has no effects,
   105  	// we can delete the LHS and its corresponding RHS.
   106  	if len(spec.Names) == len(spec.Values) &&
   107  		typesinternal.NoEffects(info, spec.Values[index]) {
   108  
   109  		if index == len(spec.Names)-1 {
   110  			// Delete final items.
   111  			//
   112  			// var _, lhs1 = rhs0, rhs1
   113  			//      ------       ------
   114  			return []analysis.TextEdit{
   115  				{
   116  					Pos: spec.Names[index-1].End(),
   117  					End: spec.Names[index].End(),
   118  				},
   119  				{
   120  					Pos: spec.Values[index-1].End(),
   121  					End: spec.Values[index].End(),
   122  				},
   123  			}
   124  		} else {
   125  			// Delete non-final items.
   126  			//
   127  			// var lhs0, _ = rhs0, rhs1
   128  			//     ------    ------
   129  			return []analysis.TextEdit{
   130  				{
   131  					Pos: spec.Names[index].Pos(),
   132  					End: spec.Names[index+1].Pos(),
   133  				},
   134  				{
   135  					Pos: spec.Values[index].Pos(),
   136  					End: spec.Values[index+1].Pos(),
   137  				},
   138  			}
   139  		}
   140  	}
   141  
   142  	// We cannot delete the RHS.
   143  	// Blank out the LHS.
   144  	return []analysis.TextEdit{{
   145  		Pos:     id.Pos(),
   146  		End:     id.End(),
   147  		NewText: []byte("_"),
   148  	}}
   149  }
   150  
   151  // Precondition: curId is Ident beneath AssignStmt.Lhs.
   152  //
   153  // See also [deleteVarFromValueSpec], which has parallel structure.
   154  func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
   155  	var (
   156  		id      = curIdent.Node().(*ast.Ident)
   157  		curStmt = curIdent.Parent()
   158  		assign  = curStmt.Node().(*ast.AssignStmt)
   159  	)
   160  
   161  	declaresOtherNames := slices.ContainsFunc(assign.Lhs, func(lhs ast.Expr) bool {
   162  		lhsId, ok := lhs.(*ast.Ident)
   163  		return ok && lhsId != id && lhsId.Name != "_"
   164  	})
   165  	noRHSEffects := !slices.ContainsFunc(assign.Rhs, func(rhs ast.Expr) bool {
   166  		return !typesinternal.NoEffects(info, rhs)
   167  	})
   168  	if !declaresOtherNames && noRHSEffects {
   169  		// The assignment is no longer needed, either to
   170  		// declare other variables, or for its side effects.
   171  		if edits := DeleteStmt(tokFile, curStmt); edits != nil {
   172  			return edits
   173  		}
   174  		// Statement could not not be deleted in this context.
   175  		// Fall back to conservative deletion.
   176  	}
   177  
   178  	// The assign is still needed, either for
   179  	// at least one LHS, or for effects on RHS,
   180  	// or because it cannot deleted because of its context.
   181  	// Blank out or delete just one LHS.
   182  
   183  	// If the assignment is 1:1 and the RHS has no effects,
   184  	// we can delete the LHS and its corresponding RHS.
   185  	_, index := curIdent.ParentEdge()
   186  	if len(assign.Lhs) > 1 &&
   187  		len(assign.Lhs) == len(assign.Rhs) &&
   188  		typesinternal.NoEffects(info, assign.Rhs[index]) {
   189  
   190  		if index == len(assign.Lhs)-1 {
   191  			// Delete final items.
   192  			//
   193  			// _, lhs1 := rhs0, rhs1
   194  			//  ------        ------
   195  			return []analysis.TextEdit{
   196  				{
   197  					Pos: assign.Lhs[index-1].End(),
   198  					End: assign.Lhs[index].End(),
   199  				},
   200  				{
   201  					Pos: assign.Rhs[index-1].End(),
   202  					End: assign.Rhs[index].End(),
   203  				},
   204  			}
   205  		} else {
   206  			// Delete non-final items.
   207  			//
   208  			// lhs0, _ := rhs0, rhs1
   209  			// ------     ------
   210  			return []analysis.TextEdit{
   211  				{
   212  					Pos: assign.Lhs[index].Pos(),
   213  					End: assign.Lhs[index+1].Pos(),
   214  				},
   215  				{
   216  					Pos: assign.Rhs[index].Pos(),
   217  					End: assign.Rhs[index+1].Pos(),
   218  				},
   219  			}
   220  		}
   221  	}
   222  
   223  	// We cannot delete the RHS.
   224  	// Blank out the LHS.
   225  	edits := []analysis.TextEdit{{
   226  		Pos:     id.Pos(),
   227  		End:     id.End(),
   228  		NewText: []byte("_"),
   229  	}}
   230  
   231  	// If this eliminates the final variable declared by
   232  	// an := statement, we need to turn it into an =
   233  	// assignment to avoid a "no new variables on left
   234  	// side of :=" error.
   235  	if !declaresOtherNames {
   236  		edits = append(edits, analysis.TextEdit{
   237  			Pos:     assign.TokPos,
   238  			End:     assign.TokPos + token.Pos(len(":=")),
   239  			NewText: []byte("="),
   240  		})
   241  	}
   242  
   243  	return edits
   244  }
   245  
   246  // DeleteSpec returns edits to delete the {Type,Value}Spec identified by curSpec.
   247  //
   248  // TODO(adonovan): add test suite. Test for consts as well.
   249  func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEdit {
   250  	var (
   251  		spec    = curSpec.Node().(ast.Spec)
   252  		curDecl = curSpec.Parent()
   253  		decl    = curDecl.Node().(*ast.GenDecl)
   254  	)
   255  
   256  	// If it is the sole spec in the decl,
   257  	// delete the entire decl.
   258  	if len(decl.Specs) == 1 {
   259  		return DeleteDecl(tokFile, curDecl)
   260  	}
   261  
   262  	// Delete the spec and its comments.
   263  	_, index := curSpec.ParentEdge() // index of ValueSpec within GenDecl.Specs
   264  	pos, end := spec.Pos(), spec.End()
   265  	if doc := astutil.DocComment(spec); doc != nil {
   266  		pos = doc.Pos() // leading comment
   267  	}
   268  	if index == len(decl.Specs)-1 {
   269  		// Delete final spec.
   270  		if c := eolComment(spec); c != nil {
   271  			//  var (v int // comment \n)
   272  			end = c.End()
   273  		}
   274  	} else {
   275  		// Delete non-final spec.
   276  		//   var ( a T; b T )
   277  		//         -----
   278  		end = decl.Specs[index+1].Pos()
   279  	}
   280  	return []analysis.TextEdit{{
   281  		Pos: pos,
   282  		End: end,
   283  	}}
   284  }
   285  
   286  // DeleteDecl returns edits to delete the ast.Decl identified by curDecl.
   287  //
   288  // TODO(adonovan): add test suite.
   289  func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEdit {
   290  	decl := curDecl.Node().(ast.Decl)
   291  
   292  	ek, _ := curDecl.ParentEdge()
   293  	switch ek {
   294  	case edge.DeclStmt_Decl:
   295  		return DeleteStmt(tokFile, curDecl.Parent())
   296  
   297  	case edge.File_Decls:
   298  		pos, end := decl.Pos(), decl.End()
   299  		if doc := astutil.DocComment(decl); doc != nil {
   300  			pos = doc.Pos()
   301  		}
   302  
   303  		// Delete free-floating comments on same line as rparen.
   304  		//    var (...) // comment
   305  		var (
   306  			file        = curDecl.Parent().Node().(*ast.File)
   307  			lineOf      = tokFile.Line
   308  			declEndLine = lineOf(decl.End())
   309  		)
   310  		for _, cg := range file.Comments {
   311  			for _, c := range cg.List {
   312  				if c.Pos() < end {
   313  					continue // too early
   314  				}
   315  				commentEndLine := lineOf(c.End())
   316  				if commentEndLine > declEndLine {
   317  					break // too late
   318  				} else if lineOf(c.Pos()) == declEndLine && commentEndLine == declEndLine {
   319  					end = c.End()
   320  				}
   321  			}
   322  		}
   323  
   324  		return []analysis.TextEdit{{
   325  			Pos: pos,
   326  			End: end,
   327  		}}
   328  
   329  	default:
   330  		panic(fmt.Sprintf("Decl parent is %v, want DeclStmt or File", ek))
   331  	}
   332  }
   333  
   334  // DeleteStmt returns the edits to remove the [ast.Stmt] identified by
   335  // curStmt, if it is contained within a BlockStmt, CaseClause,
   336  // CommClause, or is the STMT in switch STMT; ... {...}. It returns nil otherwise.
   337  func DeleteStmt(tokFile *token.File, curStmt inspector.Cursor) []analysis.TextEdit {
   338  	stmt := curStmt.Node().(ast.Stmt)
   339  	// if the stmt is on a line by itself delete the whole line
   340  	// otherwise just delete the statement.
   341  
   342  	// this logic would be a lot simpler with the file contents, and somewhat simpler
   343  	// if the cursors included the comments.
   344  
   345  	lineOf := tokFile.Line
   346  	stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
   347  
   348  	var from, to token.Pos
   349  	// bounds of adjacent syntax/comments on same line, if any
   350  	limits := func(left, right token.Pos) {
   351  		if lineOf(left) == stmtStartLine {
   352  			from = left
   353  		}
   354  		if lineOf(right) == stmtEndLine {
   355  			to = right
   356  		}
   357  	}
   358  	// TODO(pjw): there are other places a statement might be removed:
   359  	// IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
   360  	// (removing the blocks requires more rewriting than this routine would do)
   361  	// CommCase   = "case" ( SendStmt | RecvStmt ) | "default" .
   362  	// (removing the stmt requires more rewriting, and it's unclear what the user means)
   363  	switch parent := curStmt.Parent().Node().(type) {
   364  	case *ast.SwitchStmt:
   365  		limits(parent.Switch, parent.Body.Lbrace)
   366  	case *ast.TypeSwitchStmt:
   367  		limits(parent.Switch, parent.Body.Lbrace)
   368  		if parent.Assign == stmt {
   369  			return nil // don't let the user break the type switch
   370  		}
   371  	case *ast.BlockStmt:
   372  		limits(parent.Lbrace, parent.Rbrace)
   373  	case *ast.CommClause:
   374  		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
   375  		if parent.Comm == stmt {
   376  			return nil // maybe the user meant to remove the entire CommClause?
   377  		}
   378  	case *ast.CaseClause:
   379  		limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
   380  	case *ast.ForStmt:
   381  		limits(parent.For, parent.Body.Lbrace)
   382  
   383  	default:
   384  		return nil // not one of ours
   385  	}
   386  
   387  	if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
   388  		from = prev.Node().End() // preceding statement ends on same line
   389  	}
   390  	if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
   391  		to = next.Node().Pos() // following statement begins on same line
   392  	}
   393  	// and now for the comments
   394  Outer:
   395  	for _, cg := range astutil.EnclosingFile(curStmt).Comments {
   396  		for _, co := range cg.List {
   397  			if lineOf(co.End()) < stmtStartLine {
   398  				continue
   399  			} else if lineOf(co.Pos()) > stmtEndLine {
   400  				break Outer // no more are possible
   401  			}
   402  			if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
   403  				if !from.IsValid() || co.End() > from {
   404  					from = co.End()
   405  					continue // maybe there are more
   406  				}
   407  			}
   408  			if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
   409  				if !to.IsValid() || co.Pos() < to {
   410  					to = co.Pos()
   411  					continue // maybe there are more
   412  				}
   413  			}
   414  		}
   415  	}
   416  	// if either from or to is valid, just remove the statement
   417  	// otherwise remove the line
   418  	edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
   419  	if from.IsValid() || to.IsValid() {
   420  		// remove just the statement.
   421  		// we can't tell if there is a ; or whitespace right after the statement
   422  		// ideally we'd like to remove the former and leave the latter
   423  		// (if gofmt has run, there likely won't be a ;)
   424  		// In type switches we know there's a semicolon somewhere after the statement,
   425  		// but the extra work for this special case is not worth it, as gofmt will fix it.
   426  		return []analysis.TextEdit{edit}
   427  	}
   428  	// remove the whole line
   429  	for lineOf(edit.Pos) == stmtStartLine {
   430  		edit.Pos--
   431  	}
   432  	edit.Pos++ // get back tostmtStartLine
   433  	for lineOf(edit.End) == stmtEndLine {
   434  		edit.End++
   435  	}
   436  	return []analysis.TextEdit{edit}
   437  }
   438  
   439  // DeleteUnusedVars computes the edits required to delete the
   440  // declarations of any local variables whose last uses are in the
   441  // curDelend subtree, which is about to be deleted.
   442  func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []analysis.TextEdit {
   443  	// TODO(adonovan): we might want to generalize this by
   444  	// splitting the two phases below, so that we can gather
   445  	// across a whole sequence of deletions then finally compute the
   446  	// set of variables that are no longer wanted.
   447  
   448  	// Count number of deletions of each var.
   449  	delcount := make(map[*types.Var]int)
   450  	for curId := range curDelend.Preorder((*ast.Ident)(nil)) {
   451  		id := curId.Node().(*ast.Ident)
   452  		if v, ok := info.Uses[id].(*types.Var); ok &&
   453  			typesinternal.GetVarKind(v) == typesinternal.LocalVar { // always false before go1.25
   454  			delcount[v]++
   455  		}
   456  	}
   457  
   458  	// Delete declaration of each var that became unused.
   459  	var edits []analysis.TextEdit
   460  	for v, count := range delcount {
   461  		if len(slices.Collect(index.Uses(v))) == count {
   462  			if curDefId, ok := index.Def(v); ok {
   463  				edits = append(edits, DeleteVar(tokFile, info, curDefId)...)
   464  			}
   465  		}
   466  	}
   467  	return edits
   468  }
   469  
   470  func eolComment(n ast.Node) *ast.CommentGroup {
   471  	// TODO(adonovan): support:
   472  	//    func f() {...} // comment
   473  	switch n := n.(type) {
   474  	case *ast.GenDecl:
   475  		if !n.TokPos.IsValid() && len(n.Specs) == 1 {
   476  			return eolComment(n.Specs[0])
   477  		}
   478  	case *ast.ValueSpec:
   479  		return n.Comment
   480  	case *ast.TypeSpec:
   481  		return n.Comment
   482  	}
   483  	return nil
   484  }
   485  

View as plain text