TypeScript/src/services/getEditsForFileRename.ts
Eli Barzilay 9e2845227c getEditsForFileRename: fix updateTsconfigFiles w/ empty include
Avoid the assumption that there are always include patterns: when there
are none (and therefore the renamed file didn't match anyway), just skip
the test for added include.

Also change the code to use `return` to make it flatter.

(Also get rid of a redundant type.)

Fixes #40386.
2021-06-21 13:38:54 -04:00

261 lines
15 KiB
TypeScript

/* @internal */
namespace ts {
export function getEditsForFileRename(
program: Program,
oldFileOrDirPath: string,
newFileOrDirPath: string,
host: LanguageServiceHost,
formatContext: formatting.FormatContext,
preferences: UserPreferences,
sourceMapper: SourceMapper,
): readonly FileTextChanges[] {
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper);
const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper);
return textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => {
updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames);
updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName);
});
}
/** If 'path' refers to an old directory, returns path in the new directory. */
type PathUpdater = (path: string) => string | undefined;
// exported for tests
export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater {
const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath);
return path => {
const originalPath = sourceMapper && sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 });
const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path);
return originalPath
? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName)
: updatedPath;
};
function getUpdatedPath(pathToUpdate: string): string | undefined {
if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath;
const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName);
return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
}
}
// Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1.
function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string {
const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName);
return combinePathsSafe(getDirectoryPath(a1), rel);
}
function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void {
const { configFile } = program.getCompilerOptions();
if (!configFile) return;
const configDir = getDirectoryPath(configFile.fileName);
const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile);
if (!jsonObjectLiteral) return;
forEachProperty(jsonObjectLiteral, (property, propertyName) => {
switch (propertyName) {
case "files":
case "include":
case "exclude": {
const foundExactMatch = updatePaths(property);
if (foundExactMatch || propertyName !== "include" || !isArrayLiteralExpression(property.initializer)) return;
const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined);
if (includes.length === 0) return;
const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory);
// If there isn't some include for this, add a new one.
if (getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) &&
!getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) {
changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), factory.createStringLiteral(relativePath(newFileOrDirPath)));
}
return;
}
case "compilerOptions":
forEachProperty(property.initializer, (property, propertyName) => {
const option = getOptionFromName(propertyName);
if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) {
updatePaths(property);
}
else if (propertyName === "paths") {
forEachProperty(property.initializer, (pathsProperty) => {
if (!isArrayLiteralExpression(pathsProperty.initializer)) return;
for (const e of pathsProperty.initializer.elements) {
tryUpdateString(e);
}
});
}
});
return;
}
});
function updatePaths(property: PropertyAssignment): boolean {
const elements = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer];
let foundExactMatch = false;
for (const element of elements) {
foundExactMatch = tryUpdateString(element) || foundExactMatch;
}
return foundExactMatch;
}
function tryUpdateString(element: Expression): boolean {
if (!isStringLiteral(element)) return false;
const elementFileName = combinePathsSafe(configDir, element.text);
const updated = oldToNew(elementFileName);
if (updated !== undefined) {
changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated));
return true;
}
return false;
}
function relativePath(path: string): string {
return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames);
}
}
function updateImports(
program: Program,
changeTracker: textChanges.ChangeTracker,
oldToNew: PathUpdater,
newToOld: PathUpdater,
host: LanguageServiceHost,
getCanonicalFileName: GetCanonicalFileName,
): void {
const allFiles = program.getSourceFiles();
for (const sourceFile of allFiles) {
const newFromOld = oldToNew(sourceFile.fileName);
const newImportFromPath = newFromOld ?? sourceFile.fileName;
const newImportFromDirectory = getDirectoryPath(newImportFromPath);
const oldFromNew: string | undefined = newToOld(sourceFile.fileName);
const oldImportFromPath: string = oldFromNew || sourceFile.fileName;
const oldImportFromDirectory = getDirectoryPath(oldImportFromPath);
const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined;
updateImportsWorker(sourceFile, changeTracker,
referenceText => {
if (!pathIsRelative(referenceText)) return undefined;
const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText);
const newAbsolute = oldToNew(oldAbsolute);
return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName));
},
importLiteral => {
const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral);
// No need to update if it's an ambient module^M
if (importedModuleSymbol?.declarations && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) return undefined;
const toImport = oldFromNew !== undefined
// If we're at the new location (file was already renamed), need to redo module resolution starting from the old location.
// TODO:GH#18217
? getSourceFileToImportFromResolved(importLiteral, resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost),
oldToNew, allFiles)
: getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew);
// Need an update if the imported file moved, or the importing file moved and was using a relative path.
return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text)))
? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), getCanonicalFileName(newImportFromPath) as Path, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text)
: undefined;
});
}
}
function combineNormal(pathA: string, pathB: string): string {
return normalizePath(combinePaths(pathA, pathB));
}
function combinePathsSafe(pathA: string, pathB: string): string {
return ensurePathIsNonModuleName(combineNormal(pathA, pathB));
}
interface ToImport {
readonly newFileName: string;
/** True if the imported file was renamed. */
readonly updated: boolean;
}
function getSourceFileToImport(
importedModuleSymbol: Symbol | undefined,
importLiteral: StringLiteralLike,
importingSourceFile: SourceFile,
program: Program,
host: LanguageServiceHost,
oldToNew: PathUpdater,
): ToImport | undefined {
if (importedModuleSymbol) {
// `find` should succeed because we checked for ambient modules before calling this function.
const oldFileName = find(importedModuleSymbol.declarations!, isSourceFile)!.fileName;
const newFileName = oldToNew(oldFileName);
return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true };
}
else {
const resolved = host.resolveModuleNames
? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName)
: program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName);
return getSourceFileToImportFromResolved(importLiteral, resolved, oldToNew, program.getSourceFiles());
}
}
function getSourceFileToImportFromResolved(importLiteral: StringLiteralLike, resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly SourceFile[]): ToImport | undefined {
// Search through all locations looking for a moved file, and only then test already existing files.
// 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.
if (!resolved) return undefined;
// First try resolved module
if (resolved.resolvedModule) {
const result = tryChange(resolved.resolvedModule.resolvedFileName);
if (result) return result;
}
// Then failed lookups that are in the list of sources
const result = forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting)
// Then failed lookups except package.json since we dont want to touch them (only included ts/js files).
// At this point, the confidence level of this fix being correct is too low to change bare specifiers or absolute paths.
|| pathIsRelative(importLiteral.text) && forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson);
if (result) return result;
// If nothing changed, then result is resolved module file thats not updated
return resolved.resolvedModule && { newFileName: resolved.resolvedModule.resolvedFileName, updated: false };
function tryChangeWithIgnoringPackageJsonExisting(oldFileName: string) {
const newFileName = oldToNew(oldFileName);
return newFileName && find(sourceFiles, src => src.fileName === newFileName)
? tryChangeWithIgnoringPackageJson(oldFileName) : undefined;
}
function tryChangeWithIgnoringPackageJson(oldFileName: string) {
return !endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined;
}
function tryChange(oldFileName: string) {
const newFileName = oldToNew(oldFileName);
return newFileName && { newFileName, updated: true };
}
}
function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) {
for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162
const updated = updateRef(ref.fileName);
if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated);
}
for (const importStringLiteral of sourceFile.imports) {
const updated = updateImport(importStringLiteral);
if (updated !== undefined && updated !== importStringLiteral.text) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated);
}
}
function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange {
return createRange(node.getStart(sourceFile) + 1, node.end - 1);
}
function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) {
if (!isObjectLiteralExpression(objectLiteral)) return;
for (const property of objectLiteral.properties) {
if (isPropertyAssignment(property) && isStringLiteral(property.name)) {
cb(property, property.name.text);
}
}
}
}