• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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