• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* @internal */
2namespace ts {
3    export function getEditsForFileRename(
4        program: Program,
5        oldFileOrDirPath: string,
6        newFileOrDirPath: string,
7        host: LanguageServiceHost,
8        formatContext: formatting.FormatContext,
9        preferences: UserPreferences,
10        sourceMapper: SourceMapper,
11    ): readonly FileTextChanges[] {
12        const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
13        const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
14        const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper);
15        const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper);
16        return textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => {
17            updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames);
18            updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName);
19        });
20    }
21
22    /** If 'path' refers to an old directory, returns path in the new directory. */
23    type PathUpdater = (path: string) => string | undefined;
24    // exported for tests
25    export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater {
26        const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath);
27        return path => {
28            const originalPath = sourceMapper && sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 });
29            const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path);
30            return originalPath
31                ? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName)
32                : updatedPath;
33        };
34
35        function getUpdatedPath(pathToUpdate: string): string | undefined {
36            if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath;
37            const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName);
38            return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
39        }
40    }
41
42    // Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1.
43    function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string {
44        const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName);
45        return combinePathsSafe(getDirectoryPath(a1), rel);
46    }
47
48    function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void {
49        const { configFile } = program.getCompilerOptions();
50        if (!configFile) return;
51        const configDir = getDirectoryPath(configFile.fileName);
52
53        const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile);
54        if (!jsonObjectLiteral) return;
55
56        forEachProperty(jsonObjectLiteral, (property, propertyName) => {
57            switch (propertyName) {
58                case "files":
59                case "include":
60                case "exclude": {
61                    const foundExactMatch = updatePaths(property);
62                    if (!foundExactMatch && propertyName === "include" && isArrayLiteralExpression(property.initializer)) {
63                        const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined);
64                        const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory);
65                        // If there isn't some include for this, add a new one.
66                        if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) &&
67                            !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) {
68                            changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), factory.createStringLiteral(relativePath(newFileOrDirPath)));
69                        }
70                    }
71                    break;
72                }
73                case "compilerOptions":
74                    forEachProperty(property.initializer, (property, propertyName) => {
75                        const option = getOptionFromName(propertyName);
76                        if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) {
77                            updatePaths(property);
78                        }
79                        else if (propertyName === "paths") {
80                            forEachProperty(property.initializer, (pathsProperty) => {
81                                if (!isArrayLiteralExpression(pathsProperty.initializer)) return;
82                                for (const e of pathsProperty.initializer.elements) {
83                                    tryUpdateString(e);
84                                }
85                            });
86                        }
87                    });
88                    break;
89            }
90        });
91
92        function updatePaths(property: PropertyAssignment): boolean {
93            // Type annotation needed due to #7294
94            const elements: readonly Expression[] = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer];
95            let foundExactMatch = false;
96            for (const element of elements) {
97                foundExactMatch = tryUpdateString(element) || foundExactMatch;
98            }
99            return foundExactMatch;
100        }
101
102        function tryUpdateString(element: Expression): boolean {
103            if (!isStringLiteral(element)) return false;
104            const elementFileName = combinePathsSafe(configDir, element.text);
105
106            const updated = oldToNew(elementFileName);
107            if (updated !== undefined) {
108                changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated));
109                return true;
110            }
111            return false;
112        }
113
114        function relativePath(path: string): string {
115            return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames);
116        }
117    }
118
119    function updateImports(
120        program: Program,
121        changeTracker: textChanges.ChangeTracker,
122        oldToNew: PathUpdater,
123        newToOld: PathUpdater,
124        host: LanguageServiceHost,
125        getCanonicalFileName: GetCanonicalFileName,
126    ): void {
127        const allFiles = program.getSourceFiles();
128        for (const sourceFile of allFiles) {
129            const newFromOld = oldToNew(sourceFile.fileName);
130            const newImportFromPath = newFromOld ?? sourceFile.fileName;
131            const newImportFromDirectory = getDirectoryPath(newImportFromPath);
132
133            const oldFromNew: string | undefined = newToOld(sourceFile.fileName);
134            const oldImportFromPath: string = oldFromNew || sourceFile.fileName;
135            const oldImportFromDirectory = getDirectoryPath(oldImportFromPath);
136
137            const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined;
138
139            updateImportsWorker(sourceFile, changeTracker,
140                referenceText => {
141                    if (!pathIsRelative(referenceText)) return undefined;
142                    const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText);
143                    const newAbsolute = oldToNew(oldAbsolute);
144                    return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName));
145                },
146                importLiteral => {
147                    const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral);
148                    // No need to update if it's an ambient module^M
149                    if (importedModuleSymbol && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) return undefined;
150
151                    const toImport = oldFromNew !== undefined
152                        // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location.
153                        // TODO:GH#18217
154                        ? getSourceFileToImportFromResolved(importLiteral, resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost),
155                                                            oldToNew, allFiles, program.getCompilerOptions().packageManagerType)
156                        : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew);
157
158                    // Need an update if the imported file moved, or the importing file moved and was using a relative path.
159                    return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text)))
160                        ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text)
161                        : undefined;
162                });
163        }
164    }
165
166    function combineNormal(pathA: string, pathB: string): string {
167        return normalizePath(combinePaths(pathA, pathB));
168    }
169    function combinePathsSafe(pathA: string, pathB: string): string {
170        return ensurePathIsNonModuleName(combineNormal(pathA, pathB));
171    }
172
173    interface ToImport {
174        readonly newFileName: string;
175        /** True if the imported file was renamed. */
176        readonly updated: boolean;
177    }
178    function getSourceFileToImport(
179        importedModuleSymbol: Symbol | undefined,
180        importLiteral: StringLiteralLike,
181        importingSourceFile: SourceFile,
182        program: Program,
183        host: LanguageServiceHost,
184        oldToNew: PathUpdater,
185    ): ToImport | undefined {
186        if (importedModuleSymbol) {
187            // `find` should succeed because we checked for ambient modules before calling this function.
188            const oldFileName = find(importedModuleSymbol.declarations, isSourceFile)!.fileName;
189            const newFileName = oldToNew(oldFileName);
190            return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true };
191        }
192        else {
193            const resolved = host.resolveModuleNames
194                ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName)
195                : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName);
196            return getSourceFileToImportFromResolved(importLiteral, resolved, oldToNew, program.getSourceFiles(), program.getCompilerOptions().packageManagerType);
197        }
198    }
199
200    function getSourceFileToImportFromResolved(importLiteral: StringLiteralLike, resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly SourceFile[], packageManagerType?: string): ToImport | undefined {
201        // Search through all locations looking for a moved file, and only then test already existing files.
202        // This is because if `a.ts` is compiled to `a.js` and `a.ts` is moved, we don't want to resolve anything to `a.js`, but to `a.ts`'s new location.
203        if (!resolved) return undefined;
204
205        // First try resolved module
206        if (resolved.resolvedModule) {
207            const result = tryChange(resolved.resolvedModule.resolvedFileName);
208            if (result) return result;
209        }
210
211        // Then failed lookups that are in the list of sources
212        const result = forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting)
213            // Then failed lookups except package.json since we dont want to touch them (only included ts/js files).
214            // At this point, the confidence level of this fix being correct is too low to change bare specifiers or absolute paths.
215            || pathIsRelative(importLiteral.text) && forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson);
216        if (result) return result;
217
218        // If nothing changed, then result is resolved module file thats not updated
219        return resolved.resolvedModule && { newFileName: resolved.resolvedModule.resolvedFileName, updated: false };
220
221        function tryChangeWithIgnoringPackageJsonExisting(oldFileName: string) {
222            const newFileName = oldToNew(oldFileName);
223            return newFileName && find(sourceFiles, src => src.fileName === newFileName)
224                ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined;
225        }
226
227        function tryChangeWithIgnoringPackageJson(oldFileName: string) {
228            return !endsWith(oldFileName, isOhpm(packageManagerType) ? "/oh-package.json5" : "/package.json") ? tryChange(oldFileName) : undefined;
229        }
230
231        function tryChange(oldFileName: string) {
232            const newFileName = oldToNew(oldFileName);
233            return newFileName && { newFileName, updated: true };
234        }
235    }
236
237    function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) {
238        for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162
239            const updated = updateRef(ref.fileName);
240            if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated);
241        }
242
243        for (const importStringLiteral of sourceFile.imports) {
244            const updated = updateImport(importStringLiteral);
245            if (updated !== undefined && updated !== importStringLiteral.text) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated);
246        }
247    }
248
249    function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange {
250        return createRange(node.getStart(sourceFile) + 1, node.end - 1);
251    }
252
253    function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) {
254        if (!isObjectLiteralExpression(objectLiteral)) return;
255        for (const property of objectLiteral.properties) {
256            if (isPropertyAssignment(property) && isStringLiteral(property.name)) {
257                cb(property, property.name.text);
258            }
259        }
260    }
261}
262