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