internal/ls/change/delete.go (211 lines of code) (raw):

package change import ( "slices" "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/astnav" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/debug" "github.com/microsoft/typescript-go/internal/format" "github.com/microsoft/typescript-go/internal/lsp/lsproto" "github.com/microsoft/typescript-go/internal/scanner" "github.com/microsoft/typescript-go/internal/stringutil" ) // deleteDeclaration deletes a node with smart handling for different node types. // This handles special cases like import specifiers in lists, parameters, etc. func deleteDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) { switch node.Kind { case ast.KindParameter: oldFunction := node.Parent if oldFunction.Kind == ast.KindArrowFunction && len(oldFunction.AsArrowFunction().Parameters.Nodes) == 1 && astnav.FindChildOfKind(oldFunction, ast.KindOpenParenToken, sourceFile) == nil { // Lambdas with exactly one parameter are special because, after removal, there // must be an empty parameter list (i.e. `()`) and this won't necessarily be the // case if the parameter is simply removed (e.g. in `x => 1`). t.ReplaceRangeWithText(sourceFile, t.getAdjustedRange(sourceFile, node, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude), "()") } else { deleteNodeInList(t, deletedNodesInLists, sourceFile, node) } case ast.KindImportDeclaration, ast.KindImportEqualsDeclaration: imports := sourceFile.Imports() isFirstImport := len(imports) > 0 && node == imports[0].Parent || node == core.Find(sourceFile.Statements.Nodes, func(s *ast.Node) bool { return ast.IsAnyImportSyntax(s) }) // For first import, leave header comment in place, otherwise only delete JSDoc comments leadingTrivia := LeadingTriviaOptionStartLine if isFirstImport { leadingTrivia = LeadingTriviaOptionExclude } else if hasJSDocNodes(node) { leadingTrivia = LeadingTriviaOptionJSDoc } deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude) case ast.KindBindingElement: pattern := node.Parent preserveComma := pattern.Kind == ast.KindArrayBindingPattern && node != pattern.AsBindingPattern().Elements.Nodes[len(pattern.AsBindingPattern().Elements.Nodes)-1] if preserveComma { deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude) } else { deleteNodeInList(t, deletedNodesInLists, sourceFile, node) } case ast.KindVariableDeclaration: deleteVariableDeclaration(t, deletedNodesInLists, sourceFile, node) case ast.KindTypeParameter: deleteNodeInList(t, deletedNodesInLists, sourceFile, node) case ast.KindImportSpecifier: namedImports := node.Parent if len(namedImports.AsNamedImports().Elements.Nodes) == 1 { deleteImportBinding(t, sourceFile, namedImports) } else { deleteNodeInList(t, deletedNodesInLists, sourceFile, node) } case ast.KindNamespaceImport: deleteImportBinding(t, sourceFile, node) case ast.KindSemicolonToken: deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionExclude) case ast.KindTypeKeyword: // For type keyword in import clauses, we need to delete the keyword and any trailing space // The trailing space is part of the next token's leading trivia, so we include it deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude) case ast.KindFunctionKeyword: deleteNode(t, sourceFile, node, LeadingTriviaOptionExclude, TrailingTriviaOptionInclude) case ast.KindClassDeclaration, ast.KindFunctionDeclaration: leadingTrivia := LeadingTriviaOptionStartLine if hasJSDocNodes(node) { leadingTrivia = LeadingTriviaOptionJSDoc } deleteNode(t, sourceFile, node, leadingTrivia, TrailingTriviaOptionInclude) default: if node.Parent == nil { // a misbehaving client can reach here with the SourceFile node deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) } else if node.Parent.Kind == ast.KindImportClause && node.Parent.AsImportClause().Name() == node { deleteDefaultImport(t, sourceFile, node.Parent) } else if node.Parent.Kind == ast.KindCallExpression && slices.Contains(node.Parent.AsCallExpression().Arguments.Nodes, node) { deleteNodeInList(t, deletedNodesInLists, sourceFile, node) } else { deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) } } } func deleteDefaultImport(t *Tracker, sourceFile *ast.SourceFile, importClause *ast.Node) { clause := importClause.AsImportClause() if clause.NamedBindings == nil { // Delete the whole import deleteNode(t, sourceFile, importClause.Parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) } else { // import |d,| * as ns from './file' name := clause.Name() start := astnav.GetStartOfNode(name, sourceFile, false) nextToken := astnav.GetTokenAtPosition(sourceFile, name.End()) if nextToken != nil && nextToken.Kind == ast.KindCommaToken { // shift first non-whitespace position after comma to the start position of the node end := scanner.SkipTriviaEx(sourceFile.Text(), nextToken.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true}) startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(start)) endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(end)) t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "") } else { deleteNode(t, sourceFile, name, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) } } } func deleteImportBinding(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node) { importClause := node.Parent.AsImportClause() if importClause.Name() != nil { // Delete named imports while preserving the default import // import d|, * as ns| from './file' // import d|, { a }| from './file' previousToken := astnav.GetTokenAtPosition(sourceFile, node.Pos()-1) debug.Assert(previousToken != nil, "previousToken should not be nil") startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(astnav.GetStartOfNode(previousToken, sourceFile, false))) endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(node.End())) t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "") } else { // Delete the entire import declaration // |import * as ns from './file'| // |import { a } from './file'| importDecl := ast.FindAncestorKind(node, ast.KindImportDeclaration) debug.Assert(importDecl != nil, "importDecl should not be nil") deleteNode(t, sourceFile, importDecl, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) } } func deleteVariableDeclaration(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) { parent := node.Parent if parent.Kind == ast.KindCatchClause { // TODO: There's currently no unused diagnostic for this, could be a suggestion openParen := astnav.FindChildOfKind(parent, ast.KindOpenParenToken, sourceFile) closeParen := astnav.FindChildOfKind(parent, ast.KindCloseParenToken, sourceFile) debug.Assert(openParen != nil && closeParen != nil, "catch clause should have parens") t.DeleteNodeRange(sourceFile, openParen, closeParen, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) return } if len(parent.AsVariableDeclarationList().Declarations.Nodes) != 1 { deleteNodeInList(t, deletedNodesInLists, sourceFile, node) return } gp := parent.Parent switch gp.Kind { case ast.KindForOfStatement, ast.KindForInStatement: t.ReplaceNode(sourceFile, node, t.NodeFactory.NewObjectLiteralExpression(t.NodeFactory.NewNodeList([]*ast.Node{}), false), nil) case ast.KindForStatement: deleteNode(t, sourceFile, parent, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) case ast.KindVariableStatement: leadingTrivia := LeadingTriviaOptionStartLine if hasJSDocNodes(gp) { leadingTrivia = LeadingTriviaOptionJSDoc } deleteNode(t, sourceFile, gp, leadingTrivia, TrailingTriviaOptionInclude) default: debug.Fail("Unexpected grandparent kind: " + gp.Kind.String()) } } // deleteNode deletes a node with the specified trivia options. // Warning: This deletes comments too. func deleteNode(t *Tracker, sourceFile *ast.SourceFile, node *ast.Node, leadingTrivia LeadingTriviaOption, trailingTrivia TrailingTriviaOption) { startPosition := t.getAdjustedStartPosition(sourceFile, node, leadingTrivia, false) endPosition := t.getAdjustedEndPosition(sourceFile, node, trailingTrivia) startPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPosition)) endPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPosition)) t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startPos, End: endPos}, "") } func deleteNodeInList(t *Tracker, deletedNodesInLists map[*ast.Node]bool, sourceFile *ast.SourceFile, node *ast.Node) { containingList := format.GetContainingList(node, sourceFile) debug.Assert(containingList != nil, "containingList should not be nil") index := slices.Index(containingList.Nodes, node) debug.Assert(index != -1, "node should be in containing list") if len(containingList.Nodes) == 1 { deleteNode(t, sourceFile, node, LeadingTriviaOptionIncludeAll, TrailingTriviaOptionInclude) return } // Note: We will only delete a comma *after* a node. This will leave a trailing comma if we delete the last node. // That's handled in the end by finishTrailingCommaAfterDeletingNodesInList. debug.Assert(!deletedNodesInLists[node], "Deleting a node twice") deletedNodesInLists[node] = true startPos := t.startPositionToDeleteNodeInList(sourceFile, node) var endPos int if index == len(containingList.Nodes)-1 { endPos = t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionInclude) } else { prevNode := (*ast.Node)(nil) if index > 0 { prevNode = containingList.Nodes[index-1] } endPos = t.endPositionToDeleteNodeInList(sourceFile, node, prevNode, containingList.Nodes[index+1]) } startLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(startPos)) endLSPos := t.converters.PositionToLineAndCharacter(sourceFile, core.TextPos(endPos)) t.ReplaceRangeWithText(sourceFile, lsproto.Range{Start: startLSPos, End: endLSPos}, "") } // startPositionToDeleteNodeInList finds the first non-whitespace position in the leading trivia of the node func (t *Tracker) startPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node) int { start := t.getAdjustedStartPosition(sourceFile, node, LeadingTriviaOptionIncludeAll, false) return scanner.SkipTriviaEx(sourceFile.Text(), start, &scanner.SkipTriviaOptions{StopAfterLineBreak: false, StopAtComments: true}) } func (t *Tracker) endPositionToDeleteNodeInList(sourceFile *ast.SourceFile, node *ast.Node, prevNode *ast.Node, nextNode *ast.Node) int { end := t.startPositionToDeleteNodeInList(sourceFile, nextNode) if prevNode == nil || positionsAreOnSameLine(t.getAdjustedEndPosition(sourceFile, node, TrailingTriviaOptionInclude), end, sourceFile) { return end } token := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(nextNode, sourceFile, false)) if isSeparator(node, token) { prevToken := astnav.FindPrecedingToken(sourceFile, astnav.GetStartOfNode(node, sourceFile, false)) if isSeparator(prevNode, prevToken) { pos := scanner.SkipTriviaEx(sourceFile.Text(), token.End(), &scanner.SkipTriviaOptions{StopAfterLineBreak: true, StopAtComments: true}) if positionsAreOnSameLine(astnav.GetStartOfNode(prevToken, sourceFile, false), astnav.GetStartOfNode(token, sourceFile, false), sourceFile) { if pos > 0 && stringutil.IsLineBreak(rune(sourceFile.Text()[pos-1])) { return pos - 1 } return pos } if stringutil.IsLineBreak(rune(sourceFile.Text()[pos])) { return pos } } } return end } func positionsAreOnSameLine(pos1, pos2 int, sourceFile *ast.SourceFile) bool { return format.GetLineStartPositionForPosition(pos1, sourceFile) == format.GetLineStartPositionForPosition(pos2, sourceFile) } // hasJSDocNodes checks if a node has JSDoc comments func hasJSDocNodes(node *ast.Node) bool { if node == nil { return false } // nil is ok for JSDoc - it will return empty slice if not available jsdocs := node.JSDoc(nil) return len(jsdocs) > 0 }