vendor tsgo
This commit is contained in:
379
tools/tsgo/internal/ls/rename.go
Normal file
379
tools/tsgo/internal/ls/rename.go
Normal file
@@ -0,0 +1,379 @@
|
||||
package ls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/microsoft/typescript-go/internal/ast"
|
||||
"github.com/microsoft/typescript-go/internal/astnav"
|
||||
"github.com/microsoft/typescript-go/internal/checker"
|
||||
"github.com/microsoft/typescript-go/internal/compiler"
|
||||
"github.com/microsoft/typescript-go/internal/core"
|
||||
"github.com/microsoft/typescript-go/internal/diagnostics"
|
||||
"github.com/microsoft/typescript-go/internal/locale"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsconv"
|
||||
"github.com/microsoft/typescript-go/internal/ls/lsutil"
|
||||
"github.com/microsoft/typescript-go/internal/lsp/lsproto"
|
||||
"github.com/microsoft/typescript-go/internal/module"
|
||||
"github.com/microsoft/typescript-go/internal/tspath"
|
||||
)
|
||||
|
||||
// RenameInfo represents the result of a rename validation check.
|
||||
// It is used by the `textDocument/prepareRename` LSP handler.
|
||||
type RenameInfo struct {
|
||||
CanRename bool
|
||||
LocalizedErrorMessage string
|
||||
DisplayName string
|
||||
TriggerSpan lsproto.Range
|
||||
FileToRename string
|
||||
NewFileName string
|
||||
}
|
||||
|
||||
func (l *LanguageService) ProvideRename(ctx context.Context, params *lsproto.RenameParams, orchestrator CrossProjectOrchestrator) (lsproto.WorkspaceEditOrNull, error) {
|
||||
return handleCrossProject(
|
||||
l,
|
||||
ctx,
|
||||
params,
|
||||
orchestrator,
|
||||
(*LanguageService).symbolAndEntriesToRename,
|
||||
combineRenameResponse,
|
||||
true, /*isRename*/
|
||||
false, /*implementations*/
|
||||
symbolEntryTransformOptions{},
|
||||
)
|
||||
}
|
||||
|
||||
func (l *LanguageService) GetRenameInfo(ctx context.Context, newName string, documentURI lsproto.DocumentUri, position lsproto.Position) RenameInfo {
|
||||
program, sourceFile := l.getProgramAndFile(documentURI)
|
||||
pos := int(l.converters.LineAndCharacterToPosition(sourceFile, position))
|
||||
|
||||
node := astnav.GetTouchingPropertyName(sourceFile, pos)
|
||||
node = getAdjustedLocation(node, true /*forRename*/, sourceFile)
|
||||
|
||||
if nodeIsEligibleForRename(node) {
|
||||
if renameInfo, ok := l.getRenameInfoForNode(ctx, newName, node, sourceFile, program); ok {
|
||||
return renameInfo
|
||||
}
|
||||
}
|
||||
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_this_element)
|
||||
}
|
||||
|
||||
func (l *LanguageService) symbolAndEntriesToRename(ctx context.Context, params *lsproto.RenameParams, data SymbolAndEntriesData, options symbolEntryTransformOptions) (lsproto.WorkspaceEditOrNull, error) {
|
||||
if !nodeIsEligibleForRename(data.OriginalNode) {
|
||||
return lsproto.WorkspaceEditOrNull{}, nil
|
||||
}
|
||||
|
||||
program := l.GetProgram()
|
||||
|
||||
// Defense-in-depth: validate rename eligibility even if the client skipped prepareRename.
|
||||
// Use getRenameInfoForNode directly with the already-resolved node to avoid
|
||||
// re-resolving the position and polluting state baselines.
|
||||
sourceFile := ast.GetSourceFileOfNode(data.OriginalNode)
|
||||
if info, ok := l.getRenameInfoForNode(ctx, params.NewName, data.OriginalNode, sourceFile, program); !ok || !info.CanRename {
|
||||
return lsproto.WorkspaceEditOrNull{}, nil
|
||||
}
|
||||
|
||||
entries := core.FlatMap(data.SymbolsAndEntries, func(s *SymbolAndEntries) []*ReferenceEntry { return s.references })
|
||||
changes := make(map[lsproto.DocumentUri][]*lsproto.TextEdit)
|
||||
ch, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
quotePreference := lsutil.GetQuotePreference(sourceFile, l.UserPreferences())
|
||||
useAliasesForRename := l.UserPreferences().UseAliasesForRename.IsTrueOrUnknown()
|
||||
|
||||
for _, entry := range entries {
|
||||
uri := l.getFileNameOfEntry(entry)
|
||||
if l.UserPreferences().AllowRenameOfImportPath != core.TSTrue && entry.node != nil && ast.IsStringLiteralLike(entry.node) && ast.TryGetImportFromModuleSpecifier(entry.node) != nil {
|
||||
continue
|
||||
}
|
||||
textEdit := &lsproto.TextEdit{
|
||||
Range: l.getRangeOfEntry(entry),
|
||||
NewText: l.getTextForRename(data.OriginalNode, entry, params.NewName, ch, quotePreference, useAliasesForRename),
|
||||
}
|
||||
changes[uri] = append(changes[uri], textEdit)
|
||||
}
|
||||
return lsproto.WorkspaceEditOrNull{
|
||||
WorkspaceEdit: &lsproto.WorkspaceEdit{
|
||||
Changes: &changes,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getRenameInfoForNode performs detailed validation for a rename operation on a specific node.
|
||||
func (l *LanguageService) getRenameInfoForNode(ctx context.Context, newName string, node *ast.Node, sourceFile *ast.SourceFile, program *compiler.Program) (RenameInfo, bool) {
|
||||
ch, done := program.GetTypeChecker(ctx)
|
||||
defer done()
|
||||
|
||||
symbol := ch.GetSymbolAtLocation(node)
|
||||
if symbol == nil {
|
||||
if ast.IsStringLiteralLike(node) {
|
||||
// Allow renaming of string literal types with contextual string literal types
|
||||
typ := getContextualTypeFromParentOrAncestorTypeNode(node, ch)
|
||||
if typ != nil && (typ.IsStringLiteral() ||
|
||||
(typ.IsUnion() && core.Every(typ.Types(), func(t *checker.Type) bool {
|
||||
return t.IsStringLiteral()
|
||||
}))) {
|
||||
return getRenameInfoSuccess(node, sourceFile, node.Text(), l.converters), true
|
||||
}
|
||||
} else if ast.IsLabelName(node) {
|
||||
name := node.Text()
|
||||
return getRenameInfoSuccess(node, sourceFile, name, l.converters), true
|
||||
}
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
// Only allow a symbol to be renamed if it actually has at least one declaration.
|
||||
if len(symbol.Declarations) == 0 {
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
if msg := l.renameBlockedReason(sourceFile, node, symbol, ch, program); msg != nil {
|
||||
return getRenameInfoError(ctx, msg), true
|
||||
}
|
||||
|
||||
if ast.IsStringLiteralLike(node) && ast.TryGetImportFromModuleSpecifier(node) != nil {
|
||||
if l.UserPreferences().AllowRenameOfImportPath.IsTrue() {
|
||||
return l.getRenameInfoForModule(ctx, newName, node, sourceFile, symbol)
|
||||
}
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
return getRenameInfoSuccess(node, sourceFile, ch.SymbolToString(symbol), l.converters), true
|
||||
}
|
||||
|
||||
func nodeIsEligibleForRename(node *ast.Node) bool {
|
||||
switch node.Kind {
|
||||
case ast.KindIdentifier,
|
||||
ast.KindPrivateIdentifier,
|
||||
ast.KindStringLiteral,
|
||||
ast.KindNoSubstitutionTemplateLiteral,
|
||||
ast.KindThisKeyword:
|
||||
return true
|
||||
case ast.KindNumericLiteral:
|
||||
return isLiteralNameOfPropertyDeclarationOrIndexAccess(node)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// renameBlockedReason returns a non-nil diagnostic message if the rename should be blocked
|
||||
// because the symbol is a library definition, a default keyword, or would cross node_modules boundaries.
|
||||
func (l *LanguageService) renameBlockedReason(sourceFile *ast.SourceFile, node *ast.Node, symbol *ast.Symbol, ch *checker.Checker, program *compiler.Program) *diagnostics.Message {
|
||||
for _, declaration := range symbol.Declarations {
|
||||
if isDefinedInLibraryFile(program, declaration) {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot rename `default` as in `import { default as foo } from "./someModule"`
|
||||
if ast.IsIdentifier(node) && node.Text() == "default" && symbol.Parent != nil && symbol.Parent.Flags&ast.SymbolFlagsModule != 0 {
|
||||
return diagnostics.You_cannot_rename_this_element
|
||||
}
|
||||
|
||||
if msg := wouldRenameInOtherNodeModules(sourceFile, symbol, ch, l.UserPreferences()); msg != nil {
|
||||
return msg
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isDefinedInLibraryFile checks if a declaration is from a default library file (e.g., lib.d.ts).
|
||||
func isDefinedInLibraryFile(program *compiler.Program, declaration *ast.Node) bool {
|
||||
declSourceFile := ast.GetSourceFileOfNode(declaration)
|
||||
return program.IsSourceFileDefaultLibrary(declSourceFile.Path()) && tspath.IsDeclarationFileName(declSourceFile.FileName())
|
||||
}
|
||||
|
||||
// wouldRenameInOtherNodeModules checks if renaming the symbol would affect node_modules.
|
||||
func wouldRenameInOtherNodeModules(originalFile *ast.SourceFile, symbol *ast.Symbol, ch *checker.Checker, preferences lsutil.UserPreferences) *diagnostics.Message {
|
||||
sym := symbol
|
||||
if !preferences.UseAliasesForRename.IsTrueOrUnknown() && sym.Flags&ast.SymbolFlagsAlias != 0 {
|
||||
importSpecifier := core.Find(sym.Declarations, ast.IsImportSpecifier)
|
||||
if importSpecifier != nil && importSpecifier.AsImportSpecifier().PropertyName == nil {
|
||||
sym = ch.GetAliasedSymbol(sym)
|
||||
}
|
||||
}
|
||||
|
||||
declarations := sym.Declarations
|
||||
if len(declarations) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
originalPackage := module.ParseNodeModuleFromPath(originalFile.FileName(), false /*isFolder*/)
|
||||
if originalPackage == "" {
|
||||
// Original source file is not in node_modules.
|
||||
for _, declaration := range declarations {
|
||||
if isInsideNodeModules(ast.GetSourceFileOfNode(declaration).FileName()) {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_a_node_modules_folder
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Original source file is in node_modules.
|
||||
for _, declaration := range declarations {
|
||||
declPackage := module.ParseNodeModuleFromPath(ast.GetSourceFileOfNode(declaration).FileName(), false /*isFolder*/)
|
||||
if declPackage != "" && declPackage != originalPackage {
|
||||
return diagnostics.You_cannot_rename_elements_that_are_defined_in_another_node_modules_folder
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ClientSupportsWillRenameFiles(ctx context.Context) bool {
|
||||
return lsproto.GetClientCapabilities(ctx).Workspace.FileOperations.WillRename
|
||||
}
|
||||
|
||||
func ClientSupportsDocumentChanges(ctx context.Context) bool {
|
||||
return lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.DocumentChanges
|
||||
}
|
||||
|
||||
func ClientSupportsRenameResourceOperations(ctx context.Context) bool {
|
||||
return slices.Contains(lsproto.GetClientCapabilities(ctx).Workspace.WorkspaceEdit.ResourceOperations, lsproto.ResourceOperationKindRename)
|
||||
}
|
||||
|
||||
// getRenameInfoForModule handles rename validation for module specifiers.
|
||||
func (l *LanguageService) getRenameInfoForModule(ctx context.Context, newName string, specifier *ast.StringLiteralLike, sourceFile *ast.SourceFile, moduleSymbol *ast.Symbol) (RenameInfo, bool) {
|
||||
if !tspath.IsExternalModuleNameRelative(specifier.Text()) {
|
||||
return getRenameInfoError(ctx, diagnostics.You_cannot_rename_a_module_via_a_global_import), true
|
||||
}
|
||||
if !ClientSupportsDocumentChanges(ctx) || !ClientSupportsRenameResourceOperations(ctx) {
|
||||
return getRenameInfoError(ctx, diagnostics.File_rename_is_not_supported_by_the_editor), true
|
||||
}
|
||||
|
||||
moduleSourceFile := core.Find(moduleSymbol.Declarations, ast.IsSourceFile)
|
||||
if moduleSourceFile == nil {
|
||||
return RenameInfo{}, false
|
||||
}
|
||||
|
||||
fileName := moduleSourceFile.AsSourceFile().FileName()
|
||||
withoutIndex := ""
|
||||
if !strings.HasSuffix(specifier.Text(), "/index") && !strings.HasSuffix(specifier.Text(), "/index.js") {
|
||||
candidate := tspath.RemoveFileExtension(fileName)
|
||||
if trimmed, ok := strings.CutSuffix(candidate, "/index"); ok {
|
||||
withoutIndex = trimmed
|
||||
}
|
||||
}
|
||||
|
||||
displayName := fileName
|
||||
if withoutIndex != "" {
|
||||
displayName = withoutIndex
|
||||
}
|
||||
newFileName := l.getNewFileNameForModuleRename(displayName, specifier.Text(), newName)
|
||||
|
||||
// Span should only be the last component of the path. + 1 to account for the quote character.
|
||||
indexAfterLastSlash := strings.LastIndex(specifier.Text(), "/") + 1
|
||||
start := specifier.Pos() + 1 + indexAfterLastSlash
|
||||
length := len(specifier.Text()) - indexAfterLastSlash
|
||||
|
||||
return RenameInfo{
|
||||
CanRename: true,
|
||||
DisplayName: specifier.Text()[indexAfterLastSlash:],
|
||||
TriggerSpan: l.converters.ToLSPRange(sourceFile, core.NewTextRange(start, start+length)),
|
||||
FileToRename: displayName,
|
||||
NewFileName: newFileName,
|
||||
}, true
|
||||
}
|
||||
|
||||
// Adjust the new name based on the old path that an import specifier resolves to.
|
||||
// For example, if specifier "a.js" resolves to file a.ts, renaming "a.js" -> "b.js" should mean file rename a.ts -> b.ts.
|
||||
func (l *LanguageService) getNewFileNameForModuleRename(oldPath, specifierText, newName string) string {
|
||||
newPath := tspath.CombinePaths(tspath.GetDirectoryPath(oldPath), newName)
|
||||
ignoreCase := !l.host.UseCaseSensitiveFileNames()
|
||||
var oldExt string
|
||||
if tspath.IsDeclarationFileName(oldPath) {
|
||||
oldExt = tspath.GetDeclarationFileExtension(oldPath)
|
||||
} else {
|
||||
oldExt = tspath.GetAnyExtensionFromPath(oldPath, nil /*extensions*/, ignoreCase)
|
||||
}
|
||||
if !tspath.HasExtension(newPath) {
|
||||
newPath = newPath + oldExt
|
||||
} else if tspath.GetAnyExtensionFromPath(newPath, nil /*extensions*/, ignoreCase) == tspath.GetAnyExtensionFromPath(specifierText, nil /*extensions*/, ignoreCase) {
|
||||
newPath = tspath.ChangeAnyExtension(newPath, oldExt, nil /*extensions*/, ignoreCase)
|
||||
}
|
||||
return newPath
|
||||
}
|
||||
|
||||
func (l *LanguageService) getTextForRename(originalNode *ast.Node, entry *ReferenceEntry, newText string, ch *checker.Checker, quotePreference lsutil.QuotePreference, useAliasesForRename bool) string {
|
||||
if useAliasesForRename && entry.kind != entryKindRange && (ast.IsIdentifier(originalNode) || ast.IsStringLiteralLike(originalNode)) {
|
||||
node := ast.GetReparsedNodeForNode(entry.node)
|
||||
kind := entry.kind
|
||||
parent := node.Parent
|
||||
name := originalNode.Text()
|
||||
isShorthandAssignment := ast.IsShorthandPropertyAssignment(parent)
|
||||
switch {
|
||||
case isShorthandAssignment || (isObjectBindingElementWithoutPropertyName(parent) && parent.Name() == node && parent.AsBindingElement().DotDotDotToken == nil):
|
||||
if kind == entryKindSearchedLocalFoundProperty {
|
||||
return name + ": " + newText
|
||||
}
|
||||
if kind == entryKindSearchedPropertyFoundLocal {
|
||||
return newText + ": " + name
|
||||
}
|
||||
// In `const o = { x }; o.x`, symbolAtLocation at `x` in `{ x }` is the property symbol.
|
||||
// For a binding element `const { x } = o;`, symbolAtLocation at `x` is the property symbol.
|
||||
if isShorthandAssignment {
|
||||
grandParent := parent.Parent
|
||||
if ast.IsObjectLiteralExpression(grandParent) && ast.IsBinaryExpression(grandParent.Parent) && ast.IsModuleExportsAccessExpression(grandParent.Parent.AsBinaryExpression().Left) {
|
||||
return name + ": " + newText
|
||||
}
|
||||
return newText + ": " + name
|
||||
}
|
||||
return name + ": " + newText
|
||||
case ast.IsImportSpecifier(parent) && parent.PropertyName() == nil:
|
||||
// If the original symbol was using this alias, just rename the alias.
|
||||
var originalSymbol *ast.Symbol
|
||||
if ast.IsExportSpecifier(originalNode.Parent) {
|
||||
originalSymbol = ch.GetExportSpecifierLocalTargetSymbol(originalNode.Parent)
|
||||
} else {
|
||||
originalSymbol = ch.GetSymbolAtLocation(originalNode)
|
||||
}
|
||||
if originalSymbol != nil && slices.Contains(originalSymbol.Declarations, parent) {
|
||||
return name + " as " + newText
|
||||
}
|
||||
return newText
|
||||
case ast.IsExportSpecifier(parent) && parent.PropertyName() == nil:
|
||||
// If the symbol for the node is same as declared node symbol use prefix text
|
||||
if originalNode == entry.node || ch.GetSymbolAtLocation(originalNode) == ch.GetSymbolAtLocation(entry.node) {
|
||||
return name + " as " + newText
|
||||
}
|
||||
return newText + " as " + name
|
||||
}
|
||||
}
|
||||
|
||||
// If the node is a numerical indexing literal, then add quotes around the property access.
|
||||
if entry.kind != entryKindRange && ast.IsNumericLiteral(entry.node) && ast.IsAccessExpression(entry.node.Parent) {
|
||||
quote := getQuoteFromPreference(quotePreference)
|
||||
return quote + newText + quote
|
||||
}
|
||||
|
||||
return newText
|
||||
}
|
||||
|
||||
func getQuoteFromPreference(quotePreference lsutil.QuotePreference) string {
|
||||
if quotePreference == lsutil.QuotePreferenceSingle {
|
||||
return "'"
|
||||
}
|
||||
return `"`
|
||||
}
|
||||
|
||||
func getRenameInfoError(ctx context.Context, message *diagnostics.Message) RenameInfo {
|
||||
return RenameInfo{
|
||||
CanRename: false,
|
||||
LocalizedErrorMessage: message.Localize(locale.FromContext(ctx)),
|
||||
}
|
||||
}
|
||||
|
||||
func getRenameInfoSuccess(node *ast.Node, sourceFile *ast.SourceFile, displayName string, converters *lsconv.Converters) RenameInfo {
|
||||
start := astnav.GetStartOfNode(node, sourceFile, false /*includeJSDoc*/)
|
||||
end := node.End()
|
||||
if ast.IsStringLiteralLike(node) {
|
||||
// Exclude the quotes
|
||||
start++
|
||||
end--
|
||||
}
|
||||
return RenameInfo{
|
||||
CanRename: true,
|
||||
DisplayName: displayName,
|
||||
TriggerSpan: converters.ToLSPRange(sourceFile, core.NewTextRange(start, end)),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user