1import { 2 compareStringsCaseSensitive, Comparison, createTextSpan, DiagnosticMessage, Diagnostics, endsWith, every, Extension, 3 fileExtensionIs, find, getAdjustedRenameLocation, getContextualTypeFromParentOrAncestorTypeNode, 4 getLocaleSpecificMessage, getPathComponents, getTextOfIdentifierOrLiteral, getTextOfNode, getTouchingPropertyName, 5 ImportSpecifier, isExternalModuleNameRelative, isIdentifier, isImportOrExportSpecifierName, isImportSpecifier, 6 isInsideNodeModules, isLabelName, isLiteralNameOfPropertyDeclarationOrIndexAccess, isSourceFile, 7 isStringLiteralLike, isStringOrNumericLiteralLike, Node, NumericLiteral, Path, Program, removeFileExtension, 8 RenameInfo, RenameInfoFailure, RenameInfoSuccess, ScriptElementKind, ScriptElementKindModifier, some, SourceFile, 9 StringLiteralLike, stripQuotes, Symbol, SymbolDisplay, SymbolFlags, SyntaxKind, tryGetImportFromModuleSpecifier, 10 tryRemoveSuffix, TypeChecker, TypeFlags, UnionType, UserPreferences, 11} from "./_namespaces/ts"; 12 13/** @internal */ 14export function getRenameInfo(program: Program, sourceFile: SourceFile, position: number, preferences: UserPreferences): RenameInfo { 15 const node = getAdjustedRenameLocation(getTouchingPropertyName(sourceFile, position)); 16 if (nodeIsEligibleForRename(node)) { 17 const renameInfo = getRenameInfoForNode(node, program.getTypeChecker(), sourceFile, program, preferences); 18 if (renameInfo) { 19 return renameInfo; 20 } 21 } 22 return getRenameInfoError(Diagnostics.You_cannot_rename_this_element); 23} 24 25function getRenameInfoForNode( 26 node: Node, 27 typeChecker: TypeChecker, 28 sourceFile: SourceFile, 29 program: Program, 30 preferences: UserPreferences): RenameInfo | undefined { 31 const symbol = typeChecker.getSymbolAtLocation(node); 32 if (!symbol) { 33 if (isStringLiteralLike(node)) { 34 const type = getContextualTypeFromParentOrAncestorTypeNode(node, typeChecker); 35 if (type && ((type.flags & TypeFlags.StringLiteral) || ( 36 (type.flags & TypeFlags.Union) && every((type as UnionType).types, type => !!(type.flags & TypeFlags.StringLiteral)) 37 ))) { 38 return getRenameInfoSuccess(node.text, node.text, ScriptElementKind.string, "", node, sourceFile); 39 } 40 } 41 else if (isLabelName(node)) { 42 const name = getTextOfNode(node); 43 return getRenameInfoSuccess(name, name, ScriptElementKind.label, ScriptElementKindModifier.none, node, sourceFile); 44 } 45 return undefined; 46 } 47 // Only allow a symbol to be renamed if it actually has at least one declaration. 48 const { declarations } = symbol; 49 if (!declarations || declarations.length === 0) return; 50 51 // Disallow rename for elements that are defined in the standard TypeScript library. 52 if (declarations.some(declaration => isDefinedInLibraryFile(program, declaration))) { 53 return getRenameInfoError(Diagnostics.You_cannot_rename_elements_that_are_defined_in_the_standard_TypeScript_library); 54 } 55 56 // Cannot rename `default` as in `import { default as foo } from "./someModule"; 57 if (isIdentifier(node) && node.originalKeywordKind === SyntaxKind.DefaultKeyword && symbol.parent && symbol.parent.flags & SymbolFlags.Module) { 58 return undefined; 59 } 60 61 if (isStringLiteralLike(node) && tryGetImportFromModuleSpecifier(node)) { 62 return preferences.allowRenameOfImportPath ? getRenameInfoForModule(node, sourceFile, symbol) : undefined; 63 } 64 65 // Disallow rename for elements that would rename across `*/node_modules/*` packages. 66 const wouldRenameNodeModules = wouldRenameInOtherNodeModules(sourceFile, symbol, typeChecker, preferences); 67 if (wouldRenameNodeModules) { 68 return getRenameInfoError(wouldRenameNodeModules); 69 } 70 71 const kind = SymbolDisplay.getSymbolKind(typeChecker, symbol, node); 72 const specifierName = (isImportOrExportSpecifierName(node) || isStringOrNumericLiteralLike(node) && node.parent.kind === SyntaxKind.ComputedPropertyName) 73 ? stripQuotes(getTextOfIdentifierOrLiteral(node)) 74 : undefined; 75 const displayName = specifierName || typeChecker.symbolToString(symbol); 76 const fullDisplayName = specifierName || typeChecker.getFullyQualifiedName(symbol); 77 return getRenameInfoSuccess(displayName, fullDisplayName, kind, SymbolDisplay.getSymbolModifiers(typeChecker,symbol), node, sourceFile); 78} 79 80function isDefinedInLibraryFile(program: Program, declaration: Node) { 81 const sourceFile = declaration.getSourceFile(); 82 return program.isSourceFileDefaultLibrary(sourceFile) && fileExtensionIs(sourceFile.fileName, Extension.Dts); 83} 84 85function wouldRenameInOtherNodeModules( 86 originalFile: SourceFile, 87 symbol: Symbol, 88 checker: TypeChecker, 89 preferences: UserPreferences 90): DiagnosticMessage | undefined { 91 if (!preferences.providePrefixAndSuffixTextForRename && symbol.flags & SymbolFlags.Alias) { 92 const importSpecifier = symbol.declarations && find(symbol.declarations, decl => isImportSpecifier(decl)); 93 if (importSpecifier && !(importSpecifier as ImportSpecifier).propertyName) { 94 symbol = checker.getAliasedSymbol(symbol); 95 } 96 } 97 const { declarations } = symbol; 98 if (!declarations) { 99 return undefined; 100 } 101 const originalPackage = getPackagePathComponents(originalFile.path); 102 if (originalPackage === undefined) { // original source file is not in node_modules 103 if (some(declarations, declaration => isInsideNodeModules(declaration.getSourceFile().path))) { 104 return Diagnostics.You_cannot_rename_elements_that_are_defined_in_a_node_modules_folder; 105 } 106 else { 107 return undefined; 108 } 109 } 110 // original source file is in node_modules 111 for (const declaration of declarations) { 112 const declPackage = getPackagePathComponents(declaration.getSourceFile().path); 113 if (declPackage) { 114 const length = Math.min(originalPackage.length, declPackage.length); 115 for (let i = 0; i <= length; i++) { 116 if (compareStringsCaseSensitive(originalPackage[i], declPackage[i]) !== Comparison.EqualTo) { 117 return Diagnostics.You_cannot_rename_elements_that_are_defined_in_another_node_modules_folder; 118 } 119 } 120 } 121 } 122 return undefined; 123} 124 125function getPackagePathComponents(filePath: Path): string[] | undefined { 126 const components = getPathComponents(filePath); 127 const nodeModulesIdx = components.lastIndexOf("node_modules"); 128 if (nodeModulesIdx === -1) { 129 return undefined; 130 } 131 return components.slice(0, nodeModulesIdx + 2); 132} 133 134function getRenameInfoForModule(node: StringLiteralLike, sourceFile: SourceFile, moduleSymbol: Symbol): RenameInfo | undefined { 135 if (!isExternalModuleNameRelative(node.text)) { 136 return getRenameInfoError(Diagnostics.You_cannot_rename_a_module_via_a_global_import); 137 } 138 139 const moduleSourceFile = moduleSymbol.declarations && find(moduleSymbol.declarations, isSourceFile); 140 if (!moduleSourceFile) return undefined; 141 const withoutIndex = endsWith(node.text, "/index") || endsWith(node.text, "/index.js") ? undefined : tryRemoveSuffix(removeFileExtension(moduleSourceFile.fileName), "/index"); 142 const name = withoutIndex === undefined ? moduleSourceFile.fileName : withoutIndex; 143 const kind = withoutIndex === undefined ? ScriptElementKind.moduleElement : ScriptElementKind.directory; 144 const indexAfterLastSlash = node.text.lastIndexOf("/") + 1; 145 // Span should only be the last component of the path. + 1 to account for the quote character. 146 const triggerSpan = createTextSpan(node.getStart(sourceFile) + 1 + indexAfterLastSlash, node.text.length - indexAfterLastSlash); 147 return { 148 canRename: true, 149 fileToRename: name, 150 kind, 151 displayName: name, 152 fullDisplayName: name, 153 kindModifiers: ScriptElementKindModifier.none, 154 triggerSpan, 155 }; 156} 157 158function getRenameInfoSuccess(displayName: string, fullDisplayName: string, kind: ScriptElementKind, kindModifiers: string, node: Node, sourceFile: SourceFile): RenameInfoSuccess { 159 return { 160 canRename: true, 161 fileToRename: undefined, 162 kind, 163 displayName, 164 fullDisplayName, 165 kindModifiers, 166 triggerSpan: createTriggerSpanForNode(node, sourceFile) 167 }; 168} 169 170function getRenameInfoError(diagnostic: DiagnosticMessage): RenameInfoFailure { 171 return { canRename: false, localizedErrorMessage: getLocaleSpecificMessage(diagnostic) }; 172} 173 174function createTriggerSpanForNode(node: Node, sourceFile: SourceFile) { 175 let start = node.getStart(sourceFile); 176 let width = node.getWidth(sourceFile); 177 if (isStringLiteralLike(node)) { 178 // Exclude the quotes 179 start += 1; 180 width -= 2; 181 } 182 return createTextSpan(start, width); 183} 184 185/** @internal */ 186export function nodeIsEligibleForRename(node: Node): boolean { 187 switch (node.kind) { 188 case SyntaxKind.Identifier: 189 case SyntaxKind.PrivateIdentifier: 190 case SyntaxKind.StringLiteral: 191 case SyntaxKind.NoSubstitutionTemplateLiteral: 192 case SyntaxKind.ThisKeyword: 193 return true; 194 case SyntaxKind.NumericLiteral: 195 return isLiteralNameOfPropertyDeclarationOrIndexAccess(node as NumericLiteral); 196 default: 197 return false; 198 } 199} 200