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