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)) return; 63 const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); 64 if (includes.length === 0) return; 65 const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); 66 // If there isn't some include for this, add a new one. 67 if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) && 68 !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { 69 changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), factory.createStringLiteral(relativePath(newFileOrDirPath))); 70 } 71 return; 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 return; 89 } 90 }); 91 92 function updatePaths(property: PropertyAssignment): boolean { 93 const elements = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; 94 let foundExactMatch = false; 95 for (const element of elements) { 96 foundExactMatch = tryUpdateString(element) || foundExactMatch; 97 } 98 return foundExactMatch; 99 } 100 101 function tryUpdateString(element: Expression): boolean { 102 if (!isStringLiteral(element)) return false; 103 const elementFileName = combinePathsSafe(configDir, element.text); 104 105 const updated = oldToNew(elementFileName); 106 if (updated !== undefined) { 107 changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated)); 108 return true; 109 } 110 return false; 111 } 112 113 function relativePath(path: string): string { 114 return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames); 115 } 116 } 117 118 function updateImports( 119 program: Program, 120 changeTracker: textChanges.ChangeTracker, 121 oldToNew: PathUpdater, 122 newToOld: PathUpdater, 123 host: LanguageServiceHost, 124 getCanonicalFileName: GetCanonicalFileName, 125 ): void { 126 const allFiles = program.getSourceFiles(); 127 for (const sourceFile of allFiles) { 128 const newFromOld = oldToNew(sourceFile.fileName); 129 const newImportFromPath = newFromOld ?? sourceFile.fileName; 130 const newImportFromDirectory = getDirectoryPath(newImportFromPath); 131 132 const oldFromNew: string | undefined = newToOld(sourceFile.fileName); 133 const oldImportFromPath: string = oldFromNew || sourceFile.fileName; 134 const oldImportFromDirectory = getDirectoryPath(oldImportFromPath); 135 136 const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined; 137 138 updateImportsWorker(sourceFile, changeTracker, 139 referenceText => { 140 if (!pathIsRelative(referenceText)) return undefined; 141 const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText); 142 const newAbsolute = oldToNew(oldAbsolute); 143 return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName)); 144 }, 145 importLiteral => { 146 const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); 147 // No need to update if it's an ambient module^M 148 if (importedModuleSymbol?.declarations && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) return undefined; 149 150 const toImport = oldFromNew !== undefined 151 // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location. 152 // TODO:GH#18217 153 ? getSourceFileToImportFromResolved(importLiteral, resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), 154 oldToNew, allFiles, program.getCompilerOptions().packageManagerType) 155 : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew); 156 157 // Need an update if the imported file moved, or the importing file moved and was using a relative path. 158 return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text))) 159 ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text) 160 : undefined; 161 }); 162 } 163 } 164 165 function combineNormal(pathA: string, pathB: string): string { 166 return normalizePath(combinePaths(pathA, pathB)); 167 } 168 function combinePathsSafe(pathA: string, pathB: string): string { 169 return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); 170 } 171 172 interface ToImport { 173 readonly newFileName: string; 174 /** True if the imported file was renamed. */ 175 readonly updated: boolean; 176 } 177 function getSourceFileToImport( 178 importedModuleSymbol: Symbol | undefined, 179 importLiteral: StringLiteralLike, 180 importingSourceFile: SourceFile, 181 program: Program, 182 host: LanguageServiceHost, 183 oldToNew: PathUpdater, 184 ): ToImport | undefined { 185 if (importedModuleSymbol) { 186 // `find` should succeed because we checked for ambient modules before calling this function. 187 const oldFileName = find(importedModuleSymbol.declarations, isSourceFile)!.fileName; 188 const newFileName = oldToNew(oldFileName); 189 return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true }; 190 } 191 else { 192 const mode = getModeForUsageLocation(importingSourceFile, importLiteral); 193 const resolved = host.resolveModuleNames 194 ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode) 195 : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode); 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