From d14e9810fa7ca0a9fad022e74f593896748bba35 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 21 May 2018 13:21:40 -0700 Subject: [PATCH 01/13] getEditsForFileRename: Support directory rename --- src/compiler/core.ts | 8 ++ src/compiler/utilities.ts | 15 +-- src/services/getEditsForFileRename.ts | 98 ++++++++++++------- .../getEditsForFileRename_directory.ts | 43 ++++++++ .../getEditsForFileRename_renameFromIndex.ts | 40 ++++++++ .../getEditsForFileRename_renameToIndex.ts | 34 +++++++ 6 files changed, 199 insertions(+), 39 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_directory.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 41e754a689e79..dca4bee9e23ed 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2564,6 +2564,10 @@ namespace ts { return startsWith(str, prefix) ? str.substr(prefix.length) : str; } + export function tryRemovePrefix(str: string, prefix: string): string | undefined { + return startsWith(str, prefix) ? str.substring(prefix.length) : undefined; + } + export function endsWith(str: string, suffix: string): boolean { const expectedPos = str.length - suffix.length; return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; @@ -2573,6 +2577,10 @@ namespace ts { return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : str; } + export function tryRemoveSuffix(str: string, suffix: string): string | undefined { + return endsWith(str, suffix) ? str.slice(0, str.length - suffix.length) : undefined; + } + export function stringContains(str: string, substring: string): boolean { return str.indexOf(substring) !== -1; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index df19a8948c722..5a0d6eac937ef 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1063,7 +1063,7 @@ namespace ts { } export function getPropertyAssignment(objectLiteral: ObjectLiteralExpression, key: string, key2?: string): ReadonlyArray { - return filter(objectLiteral.properties, (property): property is PropertyAssignment => { + return objectLiteral.properties.filter((property): property is PropertyAssignment => { if (property.kind === SyntaxKind.PropertyAssignment) { const propName = getTextOfPropertyName(property.name); return key === propName || (key2 && key2 === propName); @@ -1079,12 +1079,15 @@ namespace ts { } export function getTsConfigPropArrayElementValue(tsConfigSourceFile: TsConfigSourceFile | undefined, propKey: string, elementValue: string): StringLiteral | undefined { + return firstDefined(getTsConfigPropArray(tsConfigSourceFile, propKey), property => + isArrayLiteralExpression(property.initializer) ? + find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : + undefined); + } + + export function getTsConfigPropArray(tsConfigSourceFile: TsConfigSourceFile | undefined, propKey: string): ReadonlyArray { const jsonObjectLiteral = getTsConfigObjectLiteralExpression(tsConfigSourceFile); - return jsonObjectLiteral && - firstDefined(getPropertyAssignment(jsonObjectLiteral, propKey), property => - isArrayLiteralExpression(property.initializer) ? - find(property.initializer.elements, (element): element is StringLiteral => isStringLiteral(element) && element.text === elementValue) : - undefined); + return jsonObjectLiteral ? getPropertyAssignment(jsonObjectLiteral, propKey) : emptyArray; } export function getContainingFunction(node: Node): SignatureDeclaration { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index d05d22a98d952..df34aa54b941c 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -3,41 +3,35 @@ namespace ts { export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - updateTsconfigFiles(program, changeTracker, oldFilePath, newFilePath); - for (const { sourceFile, toUpdate } of getImportsToUpdate(program, oldFilePath, host)) { - const newPath = pathUpdater(isRef(toUpdate) ? toUpdate.fileName : toUpdate.text); - if (newPath !== undefined) { - const range = isRef(toUpdate) ? toUpdate : createStringRange(toUpdate, sourceFile); - changeTracker.replaceRangeWithText(sourceFile, range, isRef(toUpdate) ? newPath : removeFileExtension(newPath)); - } - } + updateTsconfigFiles(program, changeTracker, pathUpdater); + updateImports(program, changeTracker, pathUpdater, host); }); } - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldFilePath: string, newFilePath: string): void { + function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater): void { const configFile = program.getCompilerOptions().configFile; - const oldFile = getTsConfigPropArrayElementValue(configFile, "files", oldFilePath); - if (oldFile) { - changeTracker.replaceRangeWithText(configFile, createStringRange(oldFile, configFile), newFilePath); - } - } + for (const property of getTsConfigPropArray(configFile, "files")) { + if (!isArrayLiteralExpression(property.initializer)) continue; - interface ToUpdate { - readonly sourceFile: SourceFile; - readonly toUpdate: StringLiteralLike | FileReference; - } - function isRef(toUpdate: StringLiteralLike | FileReference): toUpdate is FileReference { - return "fileName" in toUpdate; + for (const element of property.initializer.elements) { + if (!isStringLiteral(element)) continue; + + const updated = pathUpdater(element.text, element.text, /*isImport*/ false); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(configFile, createStringRange(element, configFile), updated); + } + } + } } - function getImportsToUpdate(program: Program, oldFilePath: string, host: LanguageServiceHost): ReadonlyArray { + function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater, host: LanguageServiceHost): void { const checker = program.getTypeChecker(); - const result: ToUpdate[] = []; for (const sourceFile of program.getSourceFiles()) { for (const ref of sourceFile.referencedFiles) { - if (!program.getSourceFileFromReference(sourceFile, ref) && resolveTripleslashReference(ref.fileName, sourceFile.fileName) === oldFilePath) { - result.push({ sourceFile, toUpdate: ref }); - } + if (program.getSourceFileFromReference(sourceFile, ref)) continue; + + const updated = pathUpdater(resolveTripleslashReference(ref.fileName, sourceFile.fileName), ref.fileName, /*isImport*/ false); + if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); } for (const importStringLiteral of sourceFile.imports) { @@ -47,23 +41,61 @@ namespace ts { const resolved = host.resolveModuleNames ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName) : program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName); - if (resolved && contains(resolved.failedLookupLocations, oldFilePath)) { - result.push({ sourceFile, toUpdate: importStringLiteral }); + const updated = resolved && firstDefined(resolved.failedLookupLocations, path => pathUpdater(path, importStringLiteral.text, /*isImport*/ true)); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); } } } - return result; } - function getPathUpdater(oldFilePath: string, newFilePath: string, host: LanguageServiceHost): (oldPath: string) => string | undefined { + /** + * @param oldFullPath Absolute path to a failed lookup location + * @param oldRelPath Actual import text + */ + type PathUpdater = (oldFullPath: string, oldRelPath: string, isImport: boolean) => string | undefined; + function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): PathUpdater { // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = getRelativePathFromFile(oldFilePath, newFilePath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); - return oldPath => { - if (!pathIsRelative(oldPath)) return; - return ensurePathIsNonModuleName(normalizePath(combinePaths(getDirectoryPath(oldPath), rel))); + const rel = getRelativePathFromFile(oldFileOrDirPath, newFileOrDirPath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); + return (fullPath, relPath, isImport) => { + if (fullPath === oldFileOrDirPath) { + const newRelPath = pathIsRelative(relPath) + ? combinePathsSafe(tryRemoveIndexOrPackage(fullPath) && !endsWithSomeIndex(relPath) ? relPath : getDirectoryPath(relPath), rel) + : newFileOrDirPath; + return isImport ? pathToImportPath(newRelPath) : newRelPath; + } + else { + // e.g., Importing from "/old/a/b", suffix is "/a/b", and we'll leave that part alone. + const relToOld = tryRemovePrefix(fullPath, oldFileOrDirPath); + if (relToOld === undefined || !startsWith(relToOld, "/")) return undefined; + const suffix = isImport ? pathToImportPath(relToOld) : relToOld; + const newPrefix = pathIsRelative(relPath) + ? combinePathsSafe(getDirectoryPath(Debug.assertDefined(tryRemoveSuffix(relPath, suffix))), rel) + : newFileOrDirPath; + return newPrefix + suffix; + } }; } + function combinePathsSafe(pathA: string, pathB: string): string { + return ensurePathIsNonModuleName(normalizePath(combinePaths(pathA, pathB))); + } + + function endsWithSomeIndex(path: string): boolean { + return endsWith(removeFileExtension(path), "index"); + } + + /** Strips file extension and "/index" */ + function pathToImportPath(name: string): string { + const withoutIndex = tryRemoveIndexOrPackage(name); + return withoutIndex !== undefined ? withoutIndex : removeFileExtension(name); + } + + const indexEndings: ReadonlyArray = ["/package.json", "/index.js", "/index.jsx", "/index.ts", "/index.d.ts", "/index.tsx"]; + function tryRemoveIndexOrPackage(name: string): string | undefined { + return firstDefined(indexEndings, e => tryRemoveSuffix(name, e)); + } + function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange { return createTextRange(node.getStart(sourceFile) + 1, node.end - 1); } diff --git a/tests/cases/fourslash/getEditsForFileRename_directory.ts b/tests/cases/fourslash/getEditsForFileRename_directory.ts new file mode 100644 index 0000000000000..ff00ce4e7c935 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory.ts @@ -0,0 +1,43 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; + +// @Filename: /src/a.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; + +// @Filename: /src/foo/a.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; + +// @Filename: /tsconfig.json +////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/new", + newFileContents: { + "/a.ts": +`/// +import old from "./src/new"; +import old2 from "./src/new/file";`, + + "/src/a.ts": +`/// +import old from "./new"; +import old2 from "./new/file";`, + + "/src/foo/a.ts": +`/// +import old from "../new"; +import old2 from "../new/file";`, + + "/tsconfig.json": +`{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts new file mode 100644 index 0000000000000..228612cadda31 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts @@ -0,0 +1,40 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src"; +////import old2 from "./src/index"; + +// @Filename: /src/a.ts +/////// +////import old from "."; +////import old2 from "./index"; + +// @Filename: /src/foo/a.ts +/////// +////import old from ".."; +////import old2 from "../index"; + +// @Filename: /tsconfig.json +////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/index.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/index.ts", + newPath: "/src/new.ts", + newFileContents: { + "/a.ts": +`/// +import old from "./src/new"; +import old2 from "./src/new";`, + "/src/a.ts": +`/// +import old from "./new"; +import old2 from "./new";`, + "/src/foo/a.ts": +`/// +import old from "../new"; +import old2 from "../new";`, + "/tsconfig.json": +'{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts new file mode 100644 index 0000000000000..69d6331e9f486 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts @@ -0,0 +1,34 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; + +// @Filename: /src/a.ts +/////// +////import old from "./old"; + +// @Filename: /src/foo/a.ts +/////// +////import old from "../old"; + +// @Filename: /tsconfig.json +////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/index.ts", + newFileContents: { + "/a.ts": +`/// +import old from "./src";`, + "/src/a.ts": +`/// +import old from ".";`, + "/src/foo/a.ts": +`/// +import old from "..";`, + "/tsconfig.json": +'{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/index.ts"] }', + }, +}); From af54bda17e4abcedf3d75e30a4311f41a92d3a85 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 22 May 2018 13:04:15 -0700 Subject: [PATCH 02/13] Code review --- src/services/getEditsForFileRename.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index df34aa54b941c..c363f928d8796 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -25,23 +25,17 @@ namespace ts { } function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater, host: LanguageServiceHost): void { - const checker = program.getTypeChecker(); for (const sourceFile of program.getSourceFiles()) { for (const ref of sourceFile.referencedFiles) { - if (program.getSourceFileFromReference(sourceFile, ref)) continue; - const updated = pathUpdater(resolveTripleslashReference(ref.fileName, sourceFile.fileName), ref.fileName, /*isImport*/ false); if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); } for (const importStringLiteral of sourceFile.imports) { - // If it resolved to something already, ignore. - if (checker.getSymbolAtLocation(importStringLiteral)) continue; - const resolved = host.resolveModuleNames ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName) : program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName); - const updated = resolved && firstDefined(resolved.failedLookupLocations, path => pathUpdater(path, importStringLiteral.text, /*isImport*/ true)); + const updated = resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => pathUpdater(path, importStringLiteral.text, /*isImport*/ true)); if (updated !== undefined) { changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); } @@ -57,20 +51,20 @@ namespace ts { function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): PathUpdater { // Get the relative path from old to new location, and append it on to the end of imports and normalize. const rel = getRelativePathFromFile(oldFileOrDirPath, newFileOrDirPath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); - return (fullPath, relPath, isImport) => { + return (fullPath, importText, isImport) => { if (fullPath === oldFileOrDirPath) { - const newRelPath = pathIsRelative(relPath) - ? combinePathsSafe(tryRemoveIndexOrPackage(fullPath) && !endsWithSomeIndex(relPath) ? relPath : getDirectoryPath(relPath), rel) + const newImportText = pathIsRelative(importText) + ? combinePathsSafe(tryRemoveIndexOrPackage(fullPath) && !endsWithSomeIndex(importText) ? importText : getDirectoryPath(importText), rel) : newFileOrDirPath; - return isImport ? pathToImportPath(newRelPath) : newRelPath; + return isImport ? pathToImportPath(newImportText) : newImportText; } else { // e.g., Importing from "/old/a/b", suffix is "/a/b", and we'll leave that part alone. const relToOld = tryRemovePrefix(fullPath, oldFileOrDirPath); if (relToOld === undefined || !startsWith(relToOld, "/")) return undefined; const suffix = isImport ? pathToImportPath(relToOld) : relToOld; - const newPrefix = pathIsRelative(relPath) - ? combinePathsSafe(getDirectoryPath(Debug.assertDefined(tryRemoveSuffix(relPath, suffix))), rel) + const newPrefix = pathIsRelative(importText) + ? combinePathsSafe(getDirectoryPath(Debug.assertDefined(tryRemoveSuffix(importText, suffix))), rel) : newFileOrDirPath; return newPrefix + suffix; } From 2f8edbd3c0dc35079450e306329ed4785fad75d3 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 23 May 2018 11:50:27 -0700 Subject: [PATCH 03/13] Handle imports inside the new file/directory --- src/services/getEditsForFileRename.ts | 85 ++++++++++++++----- .../getEditsForFileRename_directory.ts | 33 +++++-- .../getEditsForFileRename_directory_down.ts | 63 ++++++++++++++ ...ame_directory_noUpdateNodeModulesImport.ts | 16 ++++ .../getEditsForFileRename_directory_up.ts | 63 ++++++++++++++ .../fourslash/getEditsForFileRename_subDir.ts | 13 +++ 6 files changed, 245 insertions(+), 28 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_directory_down.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_directory_up.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_subDir.ts diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 228141bc1254a..81d87584452d6 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,10 +1,11 @@ /* @internal */ namespace ts { - export function getEditsForFileRename(program: Program, oldFilePath: string, newFilePath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { - const pathUpdater = getPathUpdater(oldFilePath, newFilePath, host); + export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { + const pathUpdater = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, host); + const internalPathUpdater = getMovedFilePathUpdater(oldFileOrDirPath, newFileOrDirPath); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { updateTsconfigFiles(program, changeTracker, pathUpdater); - updateImports(program, changeTracker, pathUpdater, host); + updateImports(program, changeTracker, pathUpdater, internalPathUpdater, oldFileOrDirPath, newFileOrDirPath, host); }); } @@ -25,25 +26,62 @@ namespace ts { } } - function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater, host: LanguageServiceHost): void { + function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater, internalPathUpdater: MovedFilePathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): void { for (const sourceFile of program.getSourceFiles()) { - for (const ref of sourceFile.referencedFiles) { - const updated = pathUpdater(resolveTripleslashReference(ref.fileName, sourceFile.fileName), ref.fileName, /*isImport*/ false); - if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); + if (sourceFile.fileName === oldFileOrDirPath || removeDirectoryPrefixFromPath(oldFileOrDirPath, sourceFile.fileName) || + sourceFile.fileName === newFileOrDirPath || removeDirectoryPrefixFromPath(newFileOrDirPath, sourceFile.fileName)) { + // Update imports in a moved file. + updateImportsWorker(sourceFile, changeTracker, referenceText => internalPathUpdater(sourceFile, referenceText), importText => internalPathUpdater(sourceFile, importText)); } - - for (const importStringLiteral of sourceFile.imports) { - const resolved = host.resolveModuleNames - ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName) - : program.getResolvedModuleWithFailedLookupLocationsFromCache(importStringLiteral.text, sourceFile.fileName); - const updated = resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => pathUpdater(path, importStringLiteral.text, /*isImport*/ true)); - if (updated !== undefined) { - changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); - } + else { + // This is not a moved file, but may reference a moved file. + updateImportsWorker(sourceFile, changeTracker, + referenceText => pathUpdater(resolveTripleslashReference(referenceText, sourceFile.fileName), referenceText, /*isImport*/ false), + importText => { + const resolved = host.resolveModuleNames + ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName) + : program.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName); + return resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => pathUpdater(path, importText, /*isImport*/ true)); + }); } } } + function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (r: string) => string | undefined, updateImport: (importText: string) => string | undefined) { + for (const ref of sourceFile.referencedFiles) { + const updated = updateRef(ref.fileName); + if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); + } + + for (const importStringLiteral of sourceFile.imports) { + const updated = updateImport(importStringLiteral.text); + if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); + } + } + + // Path updater for imports in the file(s) being moved + type MovedFilePathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; + function getMovedFilePathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): MovedFilePathUpdater { + const relativeNewDirToOldDir = getRelativePathFromDirectory(toDirectory(newFileOrDirPath), toDirectory(oldFileOrDirPath), /*ignoreCase*/ false); + + if (relativeNewDirToOldDir === ".") return () => undefined; + + return (sourceFile, importText) => + !pathIsRelative(importText) || + importIsInternalToDirectory(oldFileOrDirPath, sourceFile.fileName, importText) || + importIsInternalToDirectory(newFileOrDirPath, sourceFile.fileName, importText) + ? undefined + : combineNormal(relativeNewDirToOldDir, importText); + } + + function toDirectory(path: string): string { + return isAnySupportedFileExtension(path) ? getDirectoryPath(path) : path; + } + + function importIsInternalToDirectory(oldDir: string, sourceFilePath: string, importText: string): boolean { + return !!removeDirectoryPrefixFromPath(oldDir, combineNormal(getDirectoryPath(sourceFilePath), importText)); + } + /** * @param oldFullPath Absolute path to a failed lookup location * @param oldRelPath Actual import text @@ -61,8 +99,8 @@ namespace ts { } else { // e.g., Importing from "/old/a/b", suffix is "/a/b", and we'll leave that part alone. - const relToOld = tryRemovePrefix(fullPath, oldFileOrDirPath); - if (relToOld === undefined || !startsWith(relToOld, "/")) return undefined; + const relToOld = removeDirectoryPrefixFromPath(oldFileOrDirPath, fullPath); + if (relToOld === undefined) return undefined; const suffix = isImport ? pathToImportPath(relToOld) : relToOld; const newPrefix = pathIsRelative(importText) ? combinePathsSafe(getDirectoryPath(Debug.assertDefined(tryRemoveSuffix(importText, suffix))), rel) @@ -72,8 +110,17 @@ namespace ts { }; } + function combineNormal(pathA: string, pathB: string): string { + return normalizePath(combinePaths(pathA, pathB)); + } + + function removeDirectoryPrefixFromPath(directory: string, path: string): string | undefined { + const withoutDir = tryRemovePrefix(path, directory); + return withoutDir === undefined || !startsWith(withoutDir, "/") ? undefined : withoutDir; + } + function combinePathsSafe(pathA: string, pathB: string): string { - return ensurePathIsNonModuleName(normalizePath(combinePaths(pathA, pathB))); + return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); } function endsWithSomeIndex(path: string): boolean { diff --git a/tests/cases/fourslash/getEditsForFileRename_directory.ts b/tests/cases/fourslash/getEditsForFileRename_directory.ts index ff00ce4e7c935..bfbaa9d7919f5 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory.ts @@ -4,19 +4,29 @@ /////// ////import old from "./src/old"; ////import old2 from "./src/old/file"; +////export default 0; -// @Filename: /src/a.ts +// @Filename: /src/b.ts /////// ////import old from "./old"; ////import old2 from "./old/file"; +////export default 0; -// @Filename: /src/foo/a.ts +// @Filename: /src/foo/c.ts /////// ////import old from "../old"; ////import old2 from "../old/file"; +////export default 0; + +// @Filename: /src/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old/file.ts"] } +////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old", @@ -25,19 +35,24 @@ verify.getEditsForFileRename({ "/a.ts": `/// import old from "./src/new"; -import old2 from "./src/new/file";`, +import old2 from "./src/new/file"; +export default 0;`, - "/src/a.ts": + "/src/b.ts": `/// import old from "./new"; -import old2 from "./new/file";`, +import old2 from "./new/file"; +export default 0;`, - "/src/foo/a.ts": + "/src/foo/c.ts": `/// import old from "../new"; -import old2 from "../new/file";`, +import old2 from "../new/file"; +export default 0;`, + + // No change to /src/new "/tsconfig.json": -`{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new/file.ts"] }`, +`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/new/index.ts", "/src/new/file.ts"] }`, }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts new file mode 100644 index 0000000000000..880b8f0d3983e --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts @@ -0,0 +1,63 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; +////export default 0; + +// @Filename: /src/b.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; +////export default 0; + +// @Filename: /src/foo/c.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; +////export default 0; + +// @Filename: /src/newDir/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; + +// @Filename: /tsconfig.json +////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/newDir/new", + newFileContents: { + "/a.ts": +`/// +import old from "./src/newDir/new"; +import old2 from "./src/newDir/new/file"; +export default 0;`, + + "/src/b.ts": +`/// +import old from "./newDir/new"; +import old2 from "./newDir/new/file"; +export default 0;`, + + "/src/foo/c.ts": +`/// +import old from "../newDir/new"; +import old2 from "../newDir/new/file"; +export default 0;`, + + "/src/newDir/new/index.ts": +`import a from "../../../a"; +import a2 from "../../b"; +import a3 from "../../foo/c"; +import f from "./file"; +export default 0;`, + + "/tsconfig.json": +`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/newDir/new/index.ts", "/src/newDir/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts new file mode 100644 index 0000000000000..66d24e0dad5d6 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: /a/b/file1.ts +////import { foo } from "foo"; + +// @Filename: /a/b/node_modules/foo/index.d.ts +////export const foo = 0; + +verify.getEditsForFileRename({ + oldPath: "/a/b", + newPath: "/a/d", + newFileContents: { + "/a/b/file1.ts": +`import { foo } from "foo";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts new file mode 100644 index 0000000000000..f2ac0a9875575 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts @@ -0,0 +1,63 @@ +/// + +// @Filename: /a.ts +/////// +////import old from "./src/old"; +////import old2 from "./src/old/file"; +////export default 0; + +// @Filename: /src/b.ts +/////// +////import old from "./old"; +////import old2 from "./old/file"; +////export default 0; + +// @Filename: /src/foo/c.ts +/////// +////import old from "../old"; +////import old2 from "../old/file"; +////export default 0; + +// @Filename: /newDir/new/index.ts +////import a from "../../a"; +////import a2 from "../b"; +////import a3 from "../foo/c"; +////import f from "./file"; +////export default 0; + +// @Filename: /tsconfig.json +////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/newDir/new", + newFileContents: { + "/a.ts": +`/// +import old from "./newDir/new"; +import old2 from "./newDir/new/file"; +export default 0;`, + + "/src/b.ts": +`/// +import old from "../newDir/new"; +import old2 from "../newDir/new/file"; +export default 0;`, + + "/src/foo/c.ts": +`/// +import old from "../../newDir/new"; +import old2 from "../../newDir/new/file"; +export default 0;`, + + "/newDir/new/index.ts": +`import a from "../../a"; +import a2 from "../../src/b"; +import a3 from "../../src/foo/c"; +import f from "./file"; +export default 0;`, + + "/tsconfig.json": +`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/newDir/new/index.ts", "/newDir/new/file.ts"] }`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_subDir.ts b/tests/cases/fourslash/getEditsForFileRename_subDir.ts new file mode 100644 index 0000000000000..a9f111433cb7f --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_subDir.ts @@ -0,0 +1,13 @@ +/// + +// @Filename: /src/dir/new.ts +////import a from "./foo/a"; + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/dir/new.ts", + newFileContents: { + "/src/dir/new.ts": +`import a from "../foo/a";`, + }, +}); From f9c59b1b435dd2a90a9330bcb7f9b36c3c218188 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 24 May 2018 11:48:58 -0700 Subject: [PATCH 04/13] Document path updaters --- src/services/getEditsForFileRename.ts | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 81d87584452d6..096175a9f6482 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,15 +1,15 @@ /* @internal */ namespace ts { export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { - const pathUpdater = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, host); - const internalPathUpdater = getMovedFilePathUpdater(oldFileOrDirPath, newFileOrDirPath); + const externalPathUpdater = getExternalPathUpdater(oldFileOrDirPath, newFileOrDirPath, host); + const internalPathUpdater = getInternalPathUpdater(oldFileOrDirPath, newFileOrDirPath); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - updateTsconfigFiles(program, changeTracker, pathUpdater); - updateImports(program, changeTracker, pathUpdater, internalPathUpdater, oldFileOrDirPath, newFileOrDirPath, host); + updateTsconfigFiles(program, changeTracker, externalPathUpdater); + updateImports(program, changeTracker, externalPathUpdater, internalPathUpdater, oldFileOrDirPath, newFileOrDirPath, host); }); } - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater): void { + function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: ExternalPathUpdater): void { const configFile = program.getCompilerOptions().configFile; if (!configFile) return; for (const property of getTsConfigPropArray(configFile, "files")) { @@ -26,7 +26,7 @@ namespace ts { } } - function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: PathUpdater, internalPathUpdater: MovedFilePathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): void { + function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, externalPathUpdater: ExternalPathUpdater, internalPathUpdater: InternalPathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): void { for (const sourceFile of program.getSourceFiles()) { if (sourceFile.fileName === oldFileOrDirPath || removeDirectoryPrefixFromPath(oldFileOrDirPath, sourceFile.fileName) || sourceFile.fileName === newFileOrDirPath || removeDirectoryPrefixFromPath(newFileOrDirPath, sourceFile.fileName)) { @@ -36,18 +36,18 @@ namespace ts { else { // This is not a moved file, but may reference a moved file. updateImportsWorker(sourceFile, changeTracker, - referenceText => pathUpdater(resolveTripleslashReference(referenceText, sourceFile.fileName), referenceText, /*isImport*/ false), + referenceText => externalPathUpdater(resolveTripleslashReference(referenceText, sourceFile.fileName), referenceText, /*isImport*/ false), importText => { const resolved = host.resolveModuleNames ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName) : program.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName); - return resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => pathUpdater(path, importText, /*isImport*/ true)); + return resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => externalPathUpdater(path, importText, /*isImport*/ true)); }); } } } - function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (r: string) => string | undefined, updateImport: (importText: string) => string | undefined) { + function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importText: string) => string | undefined) { for (const ref of sourceFile.referencedFiles) { const updated = updateRef(ref.fileName); if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); @@ -59,9 +59,13 @@ namespace ts { } } - // Path updater for imports in the file(s) being moved - type MovedFilePathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; - function getMovedFilePathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): MovedFilePathUpdater { + /** + * Path updater for imports internal to the file(s) being moved. + * E.g., if we are moving "./old" to "./newDir/new", an import inside "./old/a.ts" of "../b" will now need to import "../../b". + * An import that comes from inside the directory and resolves to inside the directory won't need to be updated. + */ + type InternalPathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; + function getInternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): InternalPathUpdater { const relativeNewDirToOldDir = getRelativePathFromDirectory(toDirectory(newFileOrDirPath), toDirectory(oldFileOrDirPath), /*ignoreCase*/ false); if (relativeNewDirToOldDir === ".") return () => undefined; @@ -83,11 +87,14 @@ namespace ts { } /** + * Path updater for imports external to the file(s) being moved -- we will update these if they imported from the moved file. + * E.g., an import from "./old" will need to import from "./new". + * * @param oldFullPath Absolute path to a failed lookup location * @param oldRelPath Actual import text */ - type PathUpdater = (oldFullPath: string, oldRelPath: string, isImport: boolean) => string | undefined; - function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): PathUpdater { + type ExternalPathUpdater = (oldFullPath: string, oldRelPath: string, isImport: boolean) => string | undefined; + function getExternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): ExternalPathUpdater { // Get the relative path from old to new location, and append it on to the end of imports and normalize. const rel = getRelativePathFromFile(oldFileOrDirPath, newFileOrDirPath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); return (fullPath, importText, isImport) => { From 82317280ffc1675776dd4845a26ff0522a749c4e Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 24 May 2018 12:05:58 -0700 Subject: [PATCH 05/13] Shorten relative paths where possible --- src/services/getEditsForFileRename.ts | 19 +++++++++++++------ ...EditsForFileRename_shortenRelativePaths.ts | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 096175a9f6482..336962daeffb8 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -66,16 +66,23 @@ namespace ts { */ type InternalPathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; function getInternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): InternalPathUpdater { - const relativeNewDirToOldDir = getRelativePathFromDirectory(toDirectory(newFileOrDirPath), toDirectory(oldFileOrDirPath), /*ignoreCase*/ false); + const relativeNewDirToOldDir = ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(newFileOrDirPath), toDirectory(oldFileOrDirPath), /*ignoreCase*/ false)); if (relativeNewDirToOldDir === ".") return () => undefined; - return (sourceFile, importText) => - !pathIsRelative(importText) || + const relativeOldDirToNewDir = ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(oldFileOrDirPath), toDirectory(newFileOrDirPath), /*ignoreCase*/ false)); + + return (sourceFile, importText) => { + if (!pathIsRelative(importText) || importIsInternalToDirectory(oldFileOrDirPath, sourceFile.fileName, importText) || - importIsInternalToDirectory(newFileOrDirPath, sourceFile.fileName, importText) - ? undefined - : combineNormal(relativeNewDirToOldDir, importText); + importIsInternalToDirectory(newFileOrDirPath, sourceFile.fileName, importText)) { + return undefined; + } + // If we're renaming "./a" to "./foo/a", an import from "./foo/z" can just import "./a". + const short = tryRemovePrefix(importText, relativeOldDirToNewDir + "/"); + if (short !== undefined) return ensurePathIsNonModuleName(short); + return combineNormal(relativeNewDirToOldDir, importText); + }; } function toDirectory(path: string): string { diff --git a/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts new file mode 100644 index 0000000000000..47b089f1cee6c --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts @@ -0,0 +1,16 @@ +/// + +// @Filename: src/foo/x.ts +////export const x = 0; + +// @Filename: /src/foo/new.ts +////import { x } from "./foo/x"; + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/foo/new.ts", + newFileContents: { + "/src/foo/new.ts": +`import { x } from "./x";`, + }, +}); From 49757c6f2f73d0d75224b0ce3378265c6154f947 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 24 May 2018 12:09:38 -0700 Subject: [PATCH 06/13] Reduce duplicate code --- src/services/getEditsForFileRename.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 336962daeffb8..5885b2c137f8b 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -66,11 +66,9 @@ namespace ts { */ type InternalPathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; function getInternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): InternalPathUpdater { - const relativeNewDirToOldDir = ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(newFileOrDirPath), toDirectory(oldFileOrDirPath), /*ignoreCase*/ false)); - + const relativeNewDirToOldDir = getRelative(newFileOrDirPath, oldFileOrDirPath); if (relativeNewDirToOldDir === ".") return () => undefined; - - const relativeOldDirToNewDir = ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(oldFileOrDirPath), toDirectory(newFileOrDirPath), /*ignoreCase*/ false)); + const relativeOldDirToNewDir = getRelative(oldFileOrDirPath, newFileOrDirPath); return (sourceFile, importText) => { if (!pathIsRelative(importText) || @@ -85,6 +83,10 @@ namespace ts { }; } + function getRelative(from: string, to: string): string { + return ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(from), toDirectory(to), /*ignoreCase*/ false)); + } + function toDirectory(path: string): string { return isAnySupportedFileExtension(path) ? getDirectoryPath(path) : path; } From 27f71fafa1a8298412da0dfda52b1e7c51c09415 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 24 May 2018 15:43:54 -0700 Subject: [PATCH 07/13] Rewrite, use moduleSpecifiers.ts to get module specifiers from scratch instead of updating relative paths --- src/harness/fourslash.ts | 2 +- .../unittests/tsserverProjectSystem.ts | 14 +- src/server/session.ts | 2 +- src/services/codefixes/moduleSpecifiers.ts | 151 ++++++++------ src/services/getEditsForFileRename.ts | 193 +++++++----------- src/services/services.ts | 4 +- src/services/types.ts | 2 +- .../reference/api/tsserverlibrary.d.ts | 2 +- tests/baselines/reference/api/typescript.d.ts | 2 +- .../cases/fourslash/getEditsForFileRename.ts | 3 + .../fourslash/getEditsForFileRename_amd.ts | 18 ++ .../getEditsForFileRename_directory.ts | 3 + .../getEditsForFileRename_directory_down.ts | 3 + .../getEditsForFileRename_directory_up.ts | 3 + .../getEditsForFileRename_renameFromIndex.ts | 3 + .../getEditsForFileRename_renameToIndex.ts | 3 + ...EditsForFileRename_shortenRelativePaths.ts | 4 +- .../fourslash/getEditsForFileRename_subDir.ts | 3 + 18 files changed, 223 insertions(+), 192 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_amd.ts diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 369709c31e514..2564225159755 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3360,7 +3360,7 @@ Actual: ${stringify(fullActual)}`); } public getEditsForFileRename(options: FourSlashInterface.GetEditsForFileRenameOptions): void { - const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings); + const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings, ts.defaultPreferences); this.applyChanges(changes); for (const fileName in options.newFileContents) { this.openFile(fileName); diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index 28a8bdb114a7d..68f0daf202c29 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -8403,15 +8403,23 @@ new C();` path: "/user.ts", content: 'import { x } from "./old";', }; + const newTs: File = { + path: "/new.ts", + content: "export const x = 0;", + }; + const tsconfig: File = { + path: "/tsconfig.json", + content: "{}", + }; - const host = createServerHost([userTs]); + const host = createServerHost([userTs, newTs, tsconfig]); const projectService = createProjectService(host); projectService.openClientFile(userTs.path); - const project = first(projectService.inferredProjects); + const project = projectService.configuredProjects.get(tsconfig.path)!; Debug.assert(!!project.resolveModuleNames); - const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions); + const edits = project.getLanguageService().getEditsForFileRename("/old.ts", "/new.ts", testFormatOptions, defaultPreferences); assert.deepEqual>(edits, [{ fileName: "/user.ts", textChanges: [{ diff --git a/src/server/session.ts b/src/server/session.ts index 43e36dee8b50f..e6d1d73d82364 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -1695,7 +1695,7 @@ namespace ts.server { private getEditsForFileRename(args: protocol.GetEditsForFileRenameRequestArgs, simplifiedResult: boolean): ReadonlyArray | ReadonlyArray { const { file, project } = this.getFileAndProject(args); - const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file)); + const changes = project.getLanguageService().getEditsForFileRename(args.oldFilePath, args.newFilePath, this.getFormatOptions(file), this.getPreferences(file)); return simplifiedResult ? this.mapTextChangesToCodeEdits(project, changes) : changes; } diff --git a/src/services/codefixes/moduleSpecifiers.ts b/src/services/codefixes/moduleSpecifiers.ts index 5279c3cb84f75..6fc8d3017895b 100644 --- a/src/services/codefixes/moduleSpecifiers.ts +++ b/src/services/codefixes/moduleSpecifiers.ts @@ -1,6 +1,12 @@ // Used by importFixes to synthesize import module specifiers. /* @internal */ namespace ts.moduleSpecifiers { + // Note: fromSourceFile is just for usesJsExtensionOnImports + export function getModuleSpecifier(program: Program, fromSourceFile: SourceFile, fromSourceFileName: string, toFileName: string, host: LanguageServiceHost, preferences: UserPreferences) { + const info = getInfo(program.getCompilerOptions(), fromSourceFile, fromSourceFileName, host); + return first(getModuleSpecifiersForFileName(toFileName, info, host, program.getCompilerOptions(), preferences)); + } + // For each symlink/original for a module, returns a list of ways to import that file. export function getModuleSpecifiers( moduleSymbol: Symbol, @@ -10,80 +16,101 @@ namespace ts.moduleSpecifiers { preferences: UserPreferences, ): ReadonlyArray> { const compilerOptions = program.getCompilerOptions(); - const { baseUrl, paths, rootDirs } = compilerOptions; + const info = getInfo(compilerOptions, importingSourceFile, importingSourceFile.fileName, host); + return getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => { + const global = tryGetModuleNameFromAmbientModule(moduleSymbol); + return global ? [global] : getModuleSpecifiersForFileName(moduleFileName, info, host, compilerOptions, preferences); + }); + } + + interface Info { + readonly moduleResolutionKind: ModuleResolutionKind; + readonly addJsExtension: boolean; + readonly getCanonicalFileName: GetCanonicalFileName; + readonly sourceDirectory: string; + } + // importingSourceFileName is separate because getEditsForFileRename may need to specify an updated path + function getInfo(compilerOptions: CompilerOptions, importingSourceFile: SourceFile, importingSourceFileName: string, host: LanguageServiceHost): Info { const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions); const addJsExtension = usesJsExtensionOnImports(importingSourceFile); const getCanonicalFileName = hostGetCanonicalFileName(host); - const sourceDirectory = getDirectoryPath(importingSourceFile.fileName); + const sourceDirectory = getDirectoryPath(importingSourceFileName); + return { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }; + } - return getAllModulePaths(program, moduleSymbol.valueDeclaration.getSourceFile()).map(moduleFileName => { - const global = tryGetModuleNameFromAmbientModule(moduleSymbol) - || tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) - || tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory) - || rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName); - if (global) { - return [global]; - } + function getModuleSpecifiersForFileName( + moduleFileName: string, + { moduleResolutionKind, addJsExtension, getCanonicalFileName, sourceDirectory }: Info, + host: LanguageServiceHost, + compilerOptions: CompilerOptions, + preferences: UserPreferences, + ) { + const { baseUrl, paths, rootDirs } = compilerOptions; + const global = tryGetModuleNameFromTypeRoots(compilerOptions, host, getCanonicalFileName, moduleFileName, addJsExtension) + || tryGetModuleNameAsNodeModule(compilerOptions, moduleFileName, host, getCanonicalFileName, sourceDirectory) + || rootDirs && tryGetModuleNameFromRootDirs(rootDirs, moduleFileName, sourceDirectory, getCanonicalFileName); + if (global) { + return [global]; + } - const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); - if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { - return [relativePath]; - } + const relativePath = removeExtensionAndIndexPostFix(ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, moduleFileName, getCanonicalFileName)), moduleResolutionKind, addJsExtension); + if (!baseUrl || preferences.importModuleSpecifierPreference === "relative") { + return [relativePath]; + } - const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); - if (!relativeToBaseUrl) { - return [relativePath]; - } + const relativeToBaseUrl = getRelativePathIfInDirectory(moduleFileName, baseUrl, getCanonicalFileName); + if (!relativeToBaseUrl) { + return [relativePath]; + } - const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension); - if (paths) { - const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); - if (fromPaths) { - return [fromPaths]; - } + const importRelativeToBaseUrl = removeExtensionAndIndexPostFix(relativeToBaseUrl, moduleResolutionKind, addJsExtension); + if (paths) { + const fromPaths = tryGetModuleNameFromPaths(removeFileExtension(relativeToBaseUrl), importRelativeToBaseUrl, paths); + if (fromPaths) { + return [fromPaths]; } + } - if (preferences.importModuleSpecifierPreference === "non-relative") { - return [importRelativeToBaseUrl]; - } + if (preferences.importModuleSpecifierPreference === "non-relative") { + return [importRelativeToBaseUrl]; + } - if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); + if (preferences.importModuleSpecifierPreference !== undefined) Debug.assertNever(preferences.importModuleSpecifierPreference); - if (isPathRelativeToParent(relativeToBaseUrl)) { - return [relativePath]; - } + if (isPathRelativeToParent(relativeToBaseUrl)) { + return [relativePath]; + } - /* - Prefer a relative import over a baseUrl import if it doesn't traverse up to baseUrl. - - Suppose we have: - baseUrl = /base - sourceDirectory = /base/a/b - moduleFileName = /base/foo/bar - Then: - relativePath = ../../foo/bar - getRelativePathNParents(relativePath) = 2 - pathFromSourceToBaseUrl = ../../ - getRelativePathNParents(pathFromSourceToBaseUrl) = 2 - 2 < 2 = false - In this case we should prefer using the baseUrl path "/a/b" instead of the relative path "../../foo/bar". - - Suppose we have: - baseUrl = /base - sourceDirectory = /base/foo/a - moduleFileName = /base/foo/bar - Then: - relativePath = ../a - getRelativePathNParents(relativePath) = 1 - pathFromSourceToBaseUrl = ../../ - getRelativePathNParents(pathFromSourceToBaseUrl) = 2 - 1 < 2 = true - In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". - */ - const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName)); - const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); - return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; - }); + /* + Prefer a relative import over a baseUrl import if it doesn't traverse up to baseUrl. + + Suppose we have: + baseUrl = /base + sourceDirectory = /base/a/b + moduleFileName = /base/foo/bar + Then: + relativePath = ../../foo/bar + getRelativePathNParents(relativePath) = 2 + pathFromSourceToBaseUrl = ../../ + getRelativePathNParents(pathFromSourceToBaseUrl) = 2 + 2 < 2 = false + In this case we should prefer using the baseUrl path "/a/b" instead of the relative path "../../foo/bar". + + Suppose we have: + baseUrl = /base + sourceDirectory = /base/foo/a + moduleFileName = /base/foo/bar + Then: + relativePath = ../a + getRelativePathNParents(relativePath) = 1 + pathFromSourceToBaseUrl = ../../ + getRelativePathNParents(pathFromSourceToBaseUrl) = 2 + 1 < 2 = true + In this case we should prefer using the relative path "../a" instead of the baseUrl path "foo/a". + */ + const pathFromSourceToBaseUrl = ensurePathIsNonModuleName(getRelativePathFromDirectory(sourceDirectory, baseUrl, getCanonicalFileName)); + const relativeFirst = getRelativePathNParents(relativePath) < getRelativePathNParents(pathFromSourceToBaseUrl); + return relativeFirst ? [relativePath, importRelativeToBaseUrl] : [importRelativeToBaseUrl, relativePath]; } function usesJsExtensionOnImports({ imports }: SourceFile): boolean { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 5885b2c137f8b..2d616752d91ca 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,15 +1,25 @@ /* @internal */ namespace ts { - export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext): ReadonlyArray { - const externalPathUpdater = getExternalPathUpdater(oldFileOrDirPath, newFileOrDirPath, host); - const internalPathUpdater = getInternalPathUpdater(oldFileOrDirPath, newFileOrDirPath); + export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray { + const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath); + const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - updateTsconfigFiles(program, changeTracker, externalPathUpdater); - updateImports(program, changeTracker, externalPathUpdater, internalPathUpdater, oldFileOrDirPath, newFileOrDirPath, host); + updateTsconfigFiles(program, changeTracker, oldToNew); + updateImports(program, changeTracker, oldToNew, newToOld, host, preferences); }); } - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, pathUpdater: ExternalPathUpdater): void { + /** If 'path' refers to an old directory, returns path in the new directory. */ + type PathUpdater = (path: string) => string | undefined; + function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): PathUpdater { + return path => { + if (path === oldFileOrDirPath) return newFileOrDirPath; + const suffix = tryRemovePrefix(path, oldFileOrDirPath); + return suffix === undefined ? undefined : newFileOrDirPath + suffix; + }; + } + + function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater): void { const configFile = program.getCompilerOptions().configFile; if (!configFile) return; for (const property of getTsConfigPropArray(configFile, "files")) { @@ -18,7 +28,7 @@ namespace ts { for (const element of property.initializer.elements) { if (!isStringLiteral(element)) continue; - const updated = pathUpdater(element.text, element.text, /*isImport*/ false); + const updated = oldToNew(element.text); if (updated !== undefined) { changeTracker.replaceRangeWithText(configFile, createStringRange(element, configFile), updated); } @@ -26,132 +36,79 @@ namespace ts { } } - function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, externalPathUpdater: ExternalPathUpdater, internalPathUpdater: InternalPathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): void { - for (const sourceFile of program.getSourceFiles()) { - if (sourceFile.fileName === oldFileOrDirPath || removeDirectoryPrefixFromPath(oldFileOrDirPath, sourceFile.fileName) || - sourceFile.fileName === newFileOrDirPath || removeDirectoryPrefixFromPath(newFileOrDirPath, sourceFile.fileName)) { - // Update imports in a moved file. - updateImportsWorker(sourceFile, changeTracker, referenceText => internalPathUpdater(sourceFile, referenceText), importText => internalPathUpdater(sourceFile, importText)); - } - else { - // This is not a moved file, but may reference a moved file. - updateImportsWorker(sourceFile, changeTracker, - referenceText => externalPathUpdater(resolveTripleslashReference(referenceText, sourceFile.fileName), referenceText, /*isImport*/ false), - importText => { - const resolved = host.resolveModuleNames - ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName) - : program.getResolvedModuleWithFailedLookupLocationsFromCache(importText, sourceFile.fileName); - return resolved && firstDefined(resolved.resolvedModule ? [resolved.resolvedModule.resolvedFileName] : resolved.failedLookupLocations, path => externalPathUpdater(path, importText, /*isImport*/ true)); - }); - } - } - } - - function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importText: string) => string | undefined) { - for (const ref of sourceFile.referencedFiles) { - const updated = updateRef(ref.fileName); - if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, ref, updated); - } + function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, newToOld: PathUpdater, host: LanguageServiceHost, preferences: UserPreferences): void { + const getCanonicalFileName = hostGetCanonicalFileName(host); - for (const importStringLiteral of sourceFile.imports) { - const updated = updateImport(importStringLiteral.text); - if (updated !== undefined) changeTracker.replaceRangeWithText(sourceFile, createStringRange(importStringLiteral, sourceFile), updated); + for (const sourceFile of program.getSourceFiles()) { + const newImportFromPath = oldToNew(sourceFile.fileName) || sourceFile.fileName; + const newImportFromDirectory = getDirectoryPath(newImportFromPath); + + const oldFromNew: string | undefined = newToOld(sourceFile.fileName); + const oldImportFromPath: string = oldFromNew || sourceFile.fileName; + const oldImportFromDirectory = getDirectoryPath(oldImportFromPath); + + 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 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(resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, program) + : getSourceFileToImport(importLiteral, sourceFile, program, host, oldToNew); + return toImport === undefined ? undefined : moduleSpecifiers.getModuleSpecifier(program, sourceFile, newImportFromPath, toImport, host, preferences); + }); } } - /** - * Path updater for imports internal to the file(s) being moved. - * E.g., if we are moving "./old" to "./newDir/new", an import inside "./old/a.ts" of "../b" will now need to import "../../b". - * An import that comes from inside the directory and resolves to inside the directory won't need to be updated. - */ - type InternalPathUpdater = (sourceFile: SourceFile, importText: string) => string | undefined; - function getInternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): InternalPathUpdater { - const relativeNewDirToOldDir = getRelative(newFileOrDirPath, oldFileOrDirPath); - if (relativeNewDirToOldDir === ".") return () => undefined; - const relativeOldDirToNewDir = getRelative(oldFileOrDirPath, newFileOrDirPath); - - return (sourceFile, importText) => { - if (!pathIsRelative(importText) || - importIsInternalToDirectory(oldFileOrDirPath, sourceFile.fileName, importText) || - importIsInternalToDirectory(newFileOrDirPath, sourceFile.fileName, importText)) { - return undefined; - } - // If we're renaming "./a" to "./foo/a", an import from "./foo/z" can just import "./a". - const short = tryRemovePrefix(importText, relativeOldDirToNewDir + "/"); - if (short !== undefined) return ensurePathIsNonModuleName(short); - return combineNormal(relativeNewDirToOldDir, importText); - }; - } - - function getRelative(from: string, to: string): string { - return ensurePathIsNonModuleName(getRelativePathFromDirectory(toDirectory(from), toDirectory(to), /*ignoreCase*/ false)); - } - - function toDirectory(path: string): string { - return isAnySupportedFileExtension(path) ? getDirectoryPath(path) : path; - } - - function importIsInternalToDirectory(oldDir: string, sourceFilePath: string, importText: string): boolean { - return !!removeDirectoryPrefixFromPath(oldDir, combineNormal(getDirectoryPath(sourceFilePath), importText)); - } - - /** - * Path updater for imports external to the file(s) being moved -- we will update these if they imported from the moved file. - * E.g., an import from "./old" will need to import from "./new". - * - * @param oldFullPath Absolute path to a failed lookup location - * @param oldRelPath Actual import text - */ - type ExternalPathUpdater = (oldFullPath: string, oldRelPath: string, isImport: boolean) => string | undefined; - function getExternalPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost): ExternalPathUpdater { - // Get the relative path from old to new location, and append it on to the end of imports and normalize. - const rel = getRelativePathFromFile(oldFileOrDirPath, newFileOrDirPath, createGetCanonicalFileName(hostUsesCaseSensitiveFileNames(host))); - return (fullPath, importText, isImport) => { - if (fullPath === oldFileOrDirPath) { - const newImportText = pathIsRelative(importText) - ? combinePathsSafe(tryRemoveIndexOrPackage(fullPath) && !endsWithSomeIndex(importText) ? importText : getDirectoryPath(importText), rel) - : newFileOrDirPath; - return isImport ? pathToImportPath(newImportText) : newImportText; - } - else { - // e.g., Importing from "/old/a/b", suffix is "/a/b", and we'll leave that part alone. - const relToOld = removeDirectoryPrefixFromPath(oldFileOrDirPath, fullPath); - if (relToOld === undefined) return undefined; - const suffix = isImport ? pathToImportPath(relToOld) : relToOld; - const newPrefix = pathIsRelative(importText) - ? combinePathsSafe(getDirectoryPath(Debug.assertDefined(tryRemoveSuffix(importText, suffix))), rel) - : newFileOrDirPath; - return newPrefix + suffix; - } - }; - } - function combineNormal(pathA: string, pathB: string): string { return normalizePath(combinePaths(pathA, pathB)); } - - function removeDirectoryPrefixFromPath(directory: string, path: string): string | undefined { - const withoutDir = tryRemovePrefix(path, directory); - return withoutDir === undefined || !startsWith(withoutDir, "/") ? undefined : withoutDir; - } - function combinePathsSafe(pathA: string, pathB: string): string { return ensurePathIsNonModuleName(combineNormal(pathA, pathB)); } - function endsWithSomeIndex(path: string): boolean { - return endsWith(removeFileExtension(path), "index"); + function getSourceFileToImport(importLiteral: StringLiteralLike, importingSourceFile: SourceFile, program: Program, host: LanguageServiceHost, oldToNew: PathUpdater): string | undefined { + const symbol = program.getTypeChecker().getSymbolAtLocation(importLiteral); + if (symbol) { + if (symbol.declarations.some(d => isAmbientModule(d))) return undefined; // No need to update if it's an ambient module + const oldFileName = find(symbol.declarations, isSourceFile)!.fileName; + return oldToNew(oldFileName) || oldFileName; + } + else { + const resolved = host.resolveModuleNames + ? host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName) + : program.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName); + return getSourceFileToImportFromResolved(resolved, oldToNew, program); + } } - /** Strips file extension and "/index" */ - function pathToImportPath(name: string): string { - const withoutIndex = tryRemoveIndexOrPackage(name); - return withoutIndex !== undefined ? withoutIndex : removeFileExtension(name); + function getSourceFileToImportFromResolved(resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, program: Program): string | undefined { + return resolved && ( + (resolved.resolvedModule && getIfInProgram(resolved.resolvedModule.resolvedFileName)) || firstDefined(resolved.failedLookupLocations, getIfInProgram)); + + function getIfInProgram(oldLocation: string): string | undefined { + const newLocation = oldToNew(oldLocation); + return program.getSourceFile(oldLocation) || newLocation !== undefined && program.getSourceFile(newLocation) + ? newLocation || oldLocation + : undefined; + } } - const indexEndings: ReadonlyArray = ["/package.json", "/index.js", "/index.jsx", "/index.ts", "/index.d.ts", "/index.tsx"]; - function tryRemoveIndexOrPackage(name: string): string | undefined { - return firstDefined(indexEndings, e => tryRemoveSuffix(name, e)); + function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) { + for (const ref of sourceFile.referencedFiles) { + 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 { diff --git a/src/services/services.ts b/src/services/services.ts index dcea57b71591c..02b77ce7dc290 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1993,8 +1993,8 @@ namespace ts { return OrganizeImports.organizeImports(sourceFile, formatContext, host, program, preferences); } - function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray { - return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions)); + function getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences = defaultPreferences): ReadonlyArray { + return ts.getEditsForFileRename(getProgram()!, oldFilePath, newFilePath, host, formatting.getFormatContext(formatOptions), preferences); } function applyCodeActionCommand(action: CodeActionCommand): Promise; diff --git a/src/services/types.ts b/src/services/types.ts index 4dcd59d22b554..a102e72cb97e4 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -342,7 +342,7 @@ namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 3adc0f014b696..d56a4b121ffe5 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -4568,7 +4568,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program | undefined; dispose(): void; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 70f2e0d0cdbf5..7c2fe762fb4b0 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4568,7 +4568,7 @@ declare namespace ts { getApplicableRefactors(fileName: string, positionOrRange: number | TextRange, preferences: UserPreferences | undefined): ApplicableRefactorInfo[]; getEditsForRefactor(fileName: string, formatOptions: FormatCodeSettings, positionOrRange: number | TextRange, refactorName: string, actionName: string, preferences: UserPreferences | undefined): RefactorEditInfo | undefined; organizeImports(scope: OrganizeImportsScope, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; - getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings): ReadonlyArray; + getEditsForFileRename(oldFilePath: string, newFilePath: string, formatOptions: FormatCodeSettings, preferences: UserPreferences | undefined): ReadonlyArray; getEmitOutput(fileName: string, emitOnlyDtsFiles?: boolean): EmitOutput; getProgram(): Program | undefined; dispose(): void; diff --git a/tests/cases/fourslash/getEditsForFileRename.ts b/tests/cases/fourslash/getEditsForFileRename.ts index 8e53883b37e0f..cb610721cd18e 100644 --- a/tests/cases/fourslash/getEditsForFileRename.ts +++ b/tests/cases/fourslash/getEditsForFileRename.ts @@ -14,6 +14,9 @@ /////// ////import old from "../old"; +// @Filename: /src/new.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_amd.ts b/tests/cases/fourslash/getEditsForFileRename_amd.ts new file mode 100644 index 0000000000000..22bf01e823cdf --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_amd.ts @@ -0,0 +1,18 @@ +/// + +// @moduleResolution: classic + +// @Filename: /src/user.ts +////import { x } from "old"; + +// @Filename: /src/old.ts +//// + +verify.getEditsForFileRename({ + oldPath: "/src/old.ts", + newPath: "/src/new.ts", + newFileContents: { + "/src/user.ts": +`import { x } from "./new";`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory.ts b/tests/cases/fourslash/getEditsForFileRename_directory.ts index bfbaa9d7919f5..a8648794f8d18 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory.ts @@ -25,6 +25,9 @@ ////import f from "./file"; ////export default 0; +// @Filename: /src/new/file.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts index 880b8f0d3983e..8b3604312f012 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts @@ -25,6 +25,9 @@ ////import f from "./file"; ////export default 0; +// @Filename: /src/newDir/new/file.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts index f2ac0a9875575..9a22a365be276 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts @@ -25,6 +25,9 @@ ////import f from "./file"; ////export default 0; +// @Filename: /newDir/new/file.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts index 228612cadda31..8b136dcbde23a 100644 --- a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts +++ b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts @@ -15,6 +15,9 @@ ////import old from ".."; ////import old2 from "../index"; +// @Filename: /src/index.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/index.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts index 69d6331e9f486..1c444b97e53b0 100644 --- a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts +++ b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts @@ -12,6 +12,9 @@ /////// ////import old from "../old"; +// @Filename: /src/old.ts +//// + // @Filename: /tsconfig.json ////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } diff --git a/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts index 47b089f1cee6c..d5a62f219d59f 100644 --- a/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts +++ b/tests/cases/fourslash/getEditsForFileRename_shortenRelativePaths.ts @@ -1,7 +1,7 @@ /// -// @Filename: src/foo/x.ts -////export const x = 0; +// @Filename: /src/foo/x.ts +//// // @Filename: /src/foo/new.ts ////import { x } from "./foo/x"; diff --git a/tests/cases/fourslash/getEditsForFileRename_subDir.ts b/tests/cases/fourslash/getEditsForFileRename_subDir.ts index a9f111433cb7f..ae75f190b9d4d 100644 --- a/tests/cases/fourslash/getEditsForFileRename_subDir.ts +++ b/tests/cases/fourslash/getEditsForFileRename_subDir.ts @@ -1,5 +1,8 @@ /// +// @Filename: /src/foo/a.ts +//// + // @Filename: /src/dir/new.ts ////import a from "./foo/a"; From e4fa6da1ddd77e37b0b5198f5896aaadb53f8a7a Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Tue, 29 May 2018 11:30:37 -0700 Subject: [PATCH 08/13] Update additional tsconfig.json fields --- src/services/getEditsForFileRename.ts | 32 +++++++++++++------ .../getEditsForFileRename_tsconfig.ts | 31 ++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_tsconfig.ts diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 2d616752d91ca..23fdcaca68196 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -20,18 +20,32 @@ namespace ts { } function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater): void { - const configFile = program.getCompilerOptions().configFile; + const { configFile } = program.getCompilerOptions(); if (!configFile) return; - for (const property of getTsConfigPropArray(configFile, "files")) { - if (!isArrayLiteralExpression(property.initializer)) continue; + const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile); + if (!jsonObjectLiteral) return; - for (const element of property.initializer.elements) { - if (!isStringLiteral(element)) continue; + for (const property of jsonObjectLiteral.properties) { + if (!isPropertyAssignment(property) || !isStringLiteral(property.name)) continue; + switch (property.name.text) { + case "files": + case "include": + case "exclude": + case "baseUrl": + case "typeRoots": + case "mapRoot": + case "rootDir": + case "rootDirs": + const elements = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; + for (const element of elements) { + if (!isStringLiteral(element)) continue; + + const updated = oldToNew(element.text); + if (updated !== undefined) { + changeTracker.replaceRangeWithText(configFile, createStringRange(element, configFile), updated); + } + } - const updated = oldToNew(element.text); - if (updated !== undefined) { - changeTracker.replaceRangeWithText(configFile, createStringRange(element, configFile), updated); - } } } } diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts new file mode 100644 index 0000000000000..3c8f762596fc5 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts @@ -0,0 +1,31 @@ +/// + +// @Filename: /tsconfig.json +////{ +//// "files": ["/src/old/a.ts"], +//// "include": ["/src/old"], +//// "exclude": ["/src/old"], +//// "baseUrl": "/src/old", +//// "typeRoots": ["/src/old"], +//// "mapRoot": ["/src/old"], +//// "rootDir": "/src/old", +//// "rootDirs": ["/src/old"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/old", + newPath: "/src/new", + newFileContents: { + "/tsconfig.json": +`{ + "files": ["/src/new/a.ts"], + "include": ["/src/new"], + "exclude": ["/src/new"], + "baseUrl": "/src/new", + "typeRoots": ["/src/new"], + "mapRoot": ["/src/new"], + "rootDir": "/src/new", + "rootDirs": ["/src/new"], +}`, + }, +}); From 84ad342982a9457a4768817edea290e1419c8d17 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 30 May 2018 13:44:17 -0700 Subject: [PATCH 09/13] Add test with '.js' extension --- .../getEditsForFileRename_jsExtension.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/cases/fourslash/getEditsForFileRename_jsExtension.ts diff --git a/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts b/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts new file mode 100644 index 0000000000000..9c4e423a76a2b --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_jsExtension.ts @@ -0,0 +1,18 @@ +/// + +// @allowJs: true + +// @Filename: /src/a.js +////export const a = 0; + +// @Filename: /b.js +////import { a } from "./src/a.js"; + +verify.getEditsForFileRename({ + oldPath: "/b.js", + newPath: "/src/b.js", + newFileContents: { + "/b.js": +`import { a } from "./a.js";`, + }, +}); From 25503e909701ce9e8d721b2b5e1a2da1e68e1bd2 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Wed, 30 May 2018 15:12:48 -0700 Subject: [PATCH 10/13] Handle case-insensitive paths --- src/compiler/core.ts | 12 ++++++++ src/services/getEditsForFileRename.ts | 29 ++++++++++++------- .../getEditsForFileRename_caseInsensitive.ts | 15 ++++++++++ 3 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index bb0616b914b03..4cb849ad1a21a 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2592,6 +2592,18 @@ namespace ts { return startsWith(str, prefix) ? str.substring(prefix.length) : undefined; } + export function tryRemoveDirectoryPrefix(path: string, dirPath: string): string | undefined { + const a = tryRemovePrefix(path, dirPath); + if (a === undefined) return undefined; + switch (a.charCodeAt(0)) { + case CharacterCodes.slash: + case CharacterCodes.backslash: + return a.slice(1); + default: + return undefined; + } + } + export function endsWith(str: string, suffix: string): boolean { const expectedPos = str.length - suffix.length; return expectedPos >= 0 && str.indexOf(suffix, expectedPos) === expectedPos; diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 23fdcaca68196..5d80f4769f487 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,21 +1,24 @@ /* @internal */ namespace ts { export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray { - const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath); - const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath); + const getCanonicalFileName = hostGetCanonicalFileName(host); + const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName); + const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { updateTsconfigFiles(program, changeTracker, oldToNew); - updateImports(program, changeTracker, oldToNew, newToOld, host, preferences); + updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences); }); } /** If 'path' refers to an old directory, returns path in the new directory. */ type PathUpdater = (path: string) => string | undefined; - function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string): PathUpdater { + function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName): PathUpdater { + const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath); return path => { - if (path === oldFileOrDirPath) return newFileOrDirPath; - const suffix = tryRemovePrefix(path, oldFileOrDirPath); - return suffix === undefined ? undefined : newFileOrDirPath + suffix; + const canonicalPath = getCanonicalFileName(path); + if (canonicalPath === canonicalOldPath) return newFileOrDirPath; + const suffix = tryRemoveDirectoryPrefix(canonicalPath, canonicalOldPath); + return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix; }; } @@ -50,9 +53,15 @@ namespace ts { } } - function updateImports(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, newToOld: PathUpdater, host: LanguageServiceHost, preferences: UserPreferences): void { - const getCanonicalFileName = hostGetCanonicalFileName(host); - + function updateImports( + program: Program, + changeTracker: textChanges.ChangeTracker, + oldToNew: PathUpdater, + newToOld: PathUpdater, + host: LanguageServiceHost, + getCanonicalFileName: GetCanonicalFileName, + preferences: UserPreferences, + ): void { for (const sourceFile of program.getSourceFiles()) { const newImportFromPath = oldToNew(sourceFile.fileName) || sourceFile.fileName; const newImportFromDirectory = getDirectoryPath(newImportFromPath); diff --git a/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts b/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts new file mode 100644 index 0000000000000..506c93ad3440c --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_caseInsensitive.ts @@ -0,0 +1,15 @@ +/// + +// @Filename: /a.ts +////export const a = 0; + +// @Filename: /b.ts +////import { a } from "./A"; + +verify.getEditsForFileRename({ + oldPath: "/a.ts", + newPath: "/eh.ts", + newFileContents: { + "/b.ts": 'import { a } from "./eh";', + }, +}); From bc75e673e4a8ec3b5d49f04af0d4926ffbb9ac78 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 31 May 2018 09:04:59 -0700 Subject: [PATCH 11/13] Better tsconfig handling --- src/compiler/core.ts | 13 ++- src/harness/fourslash.ts | 18 ++-- src/services/getEditsForFileRename.ts | 83 ++++++++++++++----- src/services/textChanges.ts | 2 +- .../cases/fourslash/getEditsForFileRename.ts | 4 +- .../getEditsForFileRename_directory.ts | 4 +- .../getEditsForFileRename_directory_down.ts | 4 +- ...ame_directory_noUpdateNodeModulesImport.ts | 5 +- .../getEditsForFileRename_directory_up.ts | 4 +- ...tEditsForFileRename_oldFileStillPresent.ts | 4 +- .../getEditsForFileRename_renameFromIndex.ts | 4 +- .../getEditsForFileRename_renameToIndex.ts | 4 +- .../getEditsForFileRename_tsconfig.ts | 42 ++++++---- ...EditsForFileRename_tsconfig_include_add.ts | 17 ++++ ...ForFileRename_tsconfig_include_noChange.ts | 12 +++ 15 files changed, 150 insertions(+), 70 deletions(-) create mode 100644 tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts create mode 100644 tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 4cb849ad1a21a..bb6b4a8d65b06 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -2817,6 +2817,7 @@ namespace ts { basePaths: ReadonlyArray; } + /** @param path directory of the tsconfig.json */ export function getFileMatcherPatterns(path: string, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string): FileMatcherPatterns { path = normalizePath(path); currentDirectory = normalizePath(currentDirectory); @@ -2831,16 +2832,20 @@ namespace ts { }; } + export function getRegexFromPattern(pattern: string, useCaseSensitiveFileNames: boolean): RegExp { + return new RegExp(pattern, useCaseSensitiveFileNames ? "" : "i"); + } + + /** @param path directory of the tsconfig.json */ export function matchFiles(path: string, extensions: ReadonlyArray | undefined, excludes: ReadonlyArray | undefined, includes: ReadonlyArray | undefined, useCaseSensitiveFileNames: boolean, currentDirectory: string, depth: number | undefined, getFileSystemEntries: (path: string) => FileSystemEntries): string[] { path = normalizePath(path); currentDirectory = normalizePath(currentDirectory); const patterns = getFileMatcherPatterns(path, excludes, includes, useCaseSensitiveFileNames, currentDirectory); - const regexFlag = useCaseSensitiveFileNames ? "" : "i"; - const includeFileRegexes = patterns.includeFilePatterns && patterns.includeFilePatterns.map(pattern => new RegExp(pattern, regexFlag)); - const includeDirectoryRegex = patterns.includeDirectoryPattern && new RegExp(patterns.includeDirectoryPattern, regexFlag); - const excludeRegex = patterns.excludePattern && new RegExp(patterns.excludePattern, regexFlag); + const includeFileRegexes = patterns.includeFilePatterns && patterns.includeFilePatterns.map(pattern => getRegexFromPattern(pattern, useCaseSensitiveFileNames)); + const includeDirectoryRegex = patterns.includeDirectoryPattern && getRegexFromPattern(patterns.includeDirectoryPattern, useCaseSensitiveFileNames); + const excludeRegex = patterns.excludePattern && getRegexFromPattern(patterns.excludePattern, useCaseSensitiveFileNames); // Associate an array of results with each include regex. This keeps results in order of the "include" order. // If there are no "includes", then just put everything in results[0]. diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index 2564225159755..b5e9ed895de6c 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -3131,8 +3131,12 @@ Actual: ${stringify(fullActual)}`); assert(action.name === "Move to a new file" && action.description === "Move to a new file"); const editInfo = this.languageService.getEditsForRefactor(this.activeFile.fileName, this.formatCodeSettings, range, refactor.name, action.name, options.preferences || ts.defaultPreferences)!; - for (const edit of editInfo.edits) { - const newContent = options.newFileContents[edit.fileName]; + this.testNewFileContents(editInfo.edits, options.newFileContents); + } + + private testNewFileContents(edits: ReadonlyArray, newFileContents: { [fileName: string]: string }): void { + for (const edit of edits) { + const newContent = newFileContents[edit.fileName]; if (newContent === undefined) { this.raiseError(`There was an edit in ${edit.fileName} but new content was not specified.`); } @@ -3149,8 +3153,8 @@ Actual: ${stringify(fullActual)}`); } } - for (const fileName in options.newFileContents) { - assert(editInfo.edits.some(e => e.fileName === fileName)); + for (const fileName in newFileContents) { + assert(edits.some(e => e.fileName === fileName)); } } @@ -3361,11 +3365,7 @@ Actual: ${stringify(fullActual)}`); public getEditsForFileRename(options: FourSlashInterface.GetEditsForFileRenameOptions): void { const changes = this.languageService.getEditsForFileRename(options.oldPath, options.newPath, this.formatCodeSettings, ts.defaultPreferences); - this.applyChanges(changes); - for (const fileName in options.newFileContents) { - this.openFile(fileName); - this.verifyCurrentFileContent(options.newFileContents[fileName]); - } + this.testNewFileContents(changes, options.newFileContents); } private getApplicableRefactors(positionOrRange: number | ts.TextRange, preferences = ts.defaultPreferences): ReadonlyArray { diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 5d80f4769f487..9b7d051eb5eb0 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -1,11 +1,12 @@ /* @internal */ namespace ts { export function getEditsForFileRename(program: Program, oldFileOrDirPath: string, newFileOrDirPath: string, host: LanguageServiceHost, formatContext: formatting.FormatContext, preferences: UserPreferences): ReadonlyArray { - const getCanonicalFileName = hostGetCanonicalFileName(host); + const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host); + const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName); const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName); return textChanges.ChangeTracker.with({ host, formatContext }, changeTracker => { - updateTsconfigFiles(program, changeTracker, oldToNew); + updateTsconfigFiles(program, changeTracker, oldToNew, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames); updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName, preferences); }); } @@ -22,34 +23,76 @@ namespace ts { }; } - function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater): void { + function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, 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; for (const property of jsonObjectLiteral.properties) { if (!isPropertyAssignment(property) || !isStringLiteral(property.name)) continue; - switch (property.name.text) { - case "files": - case "include": - case "exclude": - case "baseUrl": - case "typeRoots": - case "mapRoot": - case "rootDir": - case "rootDirs": - const elements = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; - for (const element of elements) { - if (!isStringLiteral(element)) continue; - - const updated = oldToNew(element.text); - if (updated !== undefined) { - changeTracker.replaceRangeWithText(configFile, createStringRange(element, configFile), updated); - } + const propertyName = property.name.text; + + if (isPathsPropertyName(propertyName)) { + // Type annotation needed due to #7294 + const elements: ReadonlyArray = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; + let foundExactMatch = false; + for (const element of elements) { + foundExactMatch = tryUpdateString(element) || foundExactMatch; + } + if (!foundExactMatch && propertyName === "include") { + const includes = mapDefined(elements, e => isStringLiteral(e) ? e.text : undefined); + const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + // If there isn't some include for this, add a new one. + if (!getRegexFromPattern(Debug.assertDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { + changeTracker.insertNodeAfter(configFile, last(elements), createStringLiteral(relativePath(newFileOrDirPath))); + } + } + } + else if (propertyName === "paths") { + if (!isObjectLiteralExpression(property.initializer)) continue; + for (const pathsProperty of property.initializer.properties) { + if (!isPropertyAssignment(pathsProperty) || !isArrayLiteralExpression(pathsProperty.initializer)) continue; + for (const e of pathsProperty.initializer.elements) { + tryUpdateString(e); } + } + } + } + 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 isPathsPropertyName(propertyName: string): boolean { + switch (propertyName) { + case "includes": + case "files": + case "include": + case "exclude": + case "baseUrl": + case "typeRoots": + case "mapRoot": + case "rootDir": + case "rootDirs": + return true; + default: + return false; } } diff --git a/src/services/textChanges.ts b/src/services/textChanges.ts index af32b88e9a1c9..b82818bc61780 100644 --- a/src/services/textChanges.ts +++ b/src/services/textChanges.ts @@ -490,7 +490,7 @@ namespace ts.textChanges { else if (isStatement(node) || isClassOrTypeElement(node)) { return { suffix: this.newLineCharacter }; } - else if (isVariableDeclaration(node)) { + else if (isVariableDeclaration(node) || isStringLiteral(node)) { return { prefix: ", " }; } else if (isPropertyAssignment(node)) { diff --git a/tests/cases/fourslash/getEditsForFileRename.ts b/tests/cases/fourslash/getEditsForFileRename.ts index cb610721cd18e..03aab26097841 100644 --- a/tests/cases/fourslash/getEditsForFileRename.ts +++ b/tests/cases/fourslash/getEditsForFileRename.ts @@ -18,7 +18,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old.ts", @@ -27,6 +27,6 @@ verify.getEditsForFileRename({ "/a.ts": '/// \nimport old from "./src/new";', "/src/a.ts": '/// \nimport old from "./new";', "/src/foo/a.ts": '/// \nimport old from "../new";', - "/tsconfig.json": '{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', + "/tsconfig.json": '{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory.ts b/tests/cases/fourslash/getEditsForFileRename_directory.ts index a8648794f8d18..8089d6fa13822 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory.ts @@ -29,7 +29,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old", @@ -56,6 +56,6 @@ export default 0;`, // No change to /src/new "/tsconfig.json": -`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/new/index.ts", "/src/new/file.ts"] }`, +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/new/index.ts", "src/new/file.ts"] }`, }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts index 8b3604312f012..c4f42e3cc52e3 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory_down.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory_down.ts @@ -29,7 +29,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old", @@ -61,6 +61,6 @@ import f from "./file"; export default 0;`, "/tsconfig.json": -`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/newDir/new/index.ts", "/src/newDir/new/file.ts"] }`, +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/newDir/new/index.ts", "src/newDir/new/file.ts"] }`, }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts index 66d24e0dad5d6..c2674cc167026 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory_noUpdateNodeModulesImport.ts @@ -9,8 +9,5 @@ verify.getEditsForFileRename({ oldPath: "/a/b", newPath: "/a/d", - newFileContents: { - "/a/b/file1.ts": -`import { foo } from "foo";`, - }, + newFileContents: {}, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts index 9a22a365be276..c63cfe888f1a0 100644 --- a/tests/cases/fourslash/getEditsForFileRename_directory_up.ts +++ b/tests/cases/fourslash/getEditsForFileRename_directory_up.ts @@ -29,7 +29,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/src/old/index.ts", "/src/old/file.ts"] } +////{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "src/old/index.ts", "src/old/file.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old", @@ -61,6 +61,6 @@ import f from "./file"; export default 0;`, "/tsconfig.json": -`{ "files": ["/a.ts", "/src/b.ts", "/src/foo/c.ts", "/newDir/new/index.ts", "/newDir/new/file.ts"] }`, +`{ "files": ["a.ts", "src/b.ts", "src/foo/c.ts", "newDir/new/index.ts", "newDir/new/file.ts"] }`, }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts b/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts index c4ebf341a5e07..34084d9d9dffc 100644 --- a/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts +++ b/tests/cases/fourslash/getEditsForFileRename_oldFileStillPresent.ts @@ -18,7 +18,7 @@ ////import old from "../old"; // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old.ts", @@ -27,6 +27,6 @@ verify.getEditsForFileRename({ "/a.ts": '/// \nimport old from "./src/new";', "/src/a.ts": '/// \nimport old from "./new";', "/src/foo/a.ts": '/// \nimport old from "../new";', - "/tsconfig.json": '{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', + "/tsconfig.json": '{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts index 8b136dcbde23a..cddf212128cca 100644 --- a/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts +++ b/tests/cases/fourslash/getEditsForFileRename_renameFromIndex.ts @@ -19,7 +19,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/index.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/index.ts"] } verify.getEditsForFileRename({ oldPath: "/src/index.ts", @@ -38,6 +38,6 @@ import old2 from "./new";`, import old from "../new"; import old2 from "../new";`, "/tsconfig.json": -'{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/new.ts"] }', +'{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/new.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts index 1c444b97e53b0..a1a052ee55712 100644 --- a/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts +++ b/tests/cases/fourslash/getEditsForFileRename_renameToIndex.ts @@ -16,7 +16,7 @@ //// // @Filename: /tsconfig.json -////{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/old.ts"] } +////{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/old.ts"] } verify.getEditsForFileRename({ oldPath: "/src/old.ts", @@ -32,6 +32,6 @@ import old from ".";`, `/// import old from "..";`, "/tsconfig.json": -'{ "files": ["/a.ts", "/src/a.ts", "/src/foo/a.ts", "/src/index.ts"] }', +'{ "files": ["a.ts", "src/a.ts", "src/foo/a.ts", "src/index.ts"] }', }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts index 3c8f762596fc5..a9f12eec10098 100644 --- a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts @@ -1,31 +1,37 @@ /// -// @Filename: /tsconfig.json +// @Filename: /src/tsconfig.json ////{ -//// "files": ["/src/old/a.ts"], -//// "include": ["/src/old"], -//// "exclude": ["/src/old"], -//// "baseUrl": "/src/old", -//// "typeRoots": ["/src/old"], -//// "mapRoot": ["/src/old"], -//// "rootDir": "/src/old", -//// "rootDirs": ["/src/old"], +//// "files": ["old/a.ts"], +//// "include": ["old/*.ts"], +//// "exclude": ["old"], +//// "baseUrl": "old", +//// "typeRoots": ["old"], +//// "mapRoot": ["old"], +//// "rootDir": "old", +//// "rootDirs": ["old"], +//// "paths": { +//// "foo": ["old"], +//// }, ////} verify.getEditsForFileRename({ oldPath: "/src/old", newPath: "/src/new", newFileContents: { - "/tsconfig.json": + "/src/tsconfig.json": `{ - "files": ["/src/new/a.ts"], - "include": ["/src/new"], - "exclude": ["/src/new"], - "baseUrl": "/src/new", - "typeRoots": ["/src/new"], - "mapRoot": ["/src/new"], - "rootDir": "/src/new", - "rootDirs": ["/src/new"], + "files": ["new/a.ts"], + "include": ["new/*.ts"], + "exclude": ["new"], + "baseUrl": "new", + "typeRoots": ["new"], + "mapRoot": ["new"], + "rootDir": "new", + "rootDirs": ["new"], + "paths": { + "foo": ["new"], + }, }`, }, }); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts new file mode 100644 index 0000000000000..5b1ef6791c0cc --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_add.ts @@ -0,0 +1,17 @@ +/// + +// @Filename: /src/tsconfig.json +////{ +//// "include": ["dir"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/dir/a.ts", + newPath: "/src/newDir/b.ts", + newFileContents: { + "/src/tsconfig.json": +`{ + "include": ["dir", "newDir/b.ts"], +}`, + }, +}); diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts new file mode 100644 index 0000000000000..dc2200e17e881 --- /dev/null +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig_include_noChange.ts @@ -0,0 +1,12 @@ +/// + +// @Filename: /src/tsconfig.json +////{ +//// "include": ["dir"], +////} + +verify.getEditsForFileRename({ + oldPath: "/src/dir/a.ts", + newPath: "/src/dir/b.ts", + newFileContents: {}, +}); From a43fae13c834846aa32777bc643a2a4d11fc2543 Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 31 May 2018 13:56:00 -0700 Subject: [PATCH 12/13] Handle properties inside compilerOptions --- src/services/getEditsForFileRename.ts | 95 +++++++++++-------- .../getEditsForFileRename_tsconfig.ts | 36 +++---- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 9b7d051eb5eb0..6c10932bf7d42 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -31,35 +31,54 @@ namespace ts { const jsonObjectLiteral = getTsConfigObjectLiteralExpression(configFile); if (!jsonObjectLiteral) return; - for (const property of jsonObjectLiteral.properties) { - if (!isPropertyAssignment(property) || !isStringLiteral(property.name)) continue; - const propertyName = property.name.text; - - if (isPathsPropertyName(propertyName)) { - // Type annotation needed due to #7294 - const elements: ReadonlyArray = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer]; - let foundExactMatch = false; - for (const element of elements) { - foundExactMatch = tryUpdateString(element) || foundExactMatch; - } - if (!foundExactMatch && propertyName === "include") { - const includes = mapDefined(elements, e => isStringLiteral(e) ? e.text : undefined); - const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); - // If there isn't some include for this, add a new one. - if (!getRegexFromPattern(Debug.assertDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { - changeTracker.insertNodeAfter(configFile, last(elements), createStringLiteral(relativePath(newFileOrDirPath))); + forEachProperty(jsonObjectLiteral, (property, propertyName) => { + switch (propertyName) { + case "files": + case "include": + case "exclude": { + const foundExactMatch = updatePaths(property); + if (!foundExactMatch && propertyName === "include" && isArrayLiteralExpression(property.initializer)) { + const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined); + const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory); + // If there isn't some include for this, add a new one. + if (!getRegexFromPattern(Debug.assertDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)) { + changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), createStringLiteral(relativePath(newFileOrDirPath))); + } } + break; } + case "compilerOptions": + forEachProperty(property.initializer, (property, propertyName) => { + switch (propertyName) { + case "baseUrl": + case "typeRoots": + case "mapRoot": + case "rootDir": + case "rootDirs": + updatePaths(property); + break; + case "paths": + forEachProperty(property.initializer, (pathsProperty) => { + if (!isArrayLiteralExpression(pathsProperty.initializer)) return; + for (const e of pathsProperty.initializer.elements) { + tryUpdateString(e); + } + }); + break; + } + }); + break; } - else if (propertyName === "paths") { - if (!isObjectLiteralExpression(property.initializer)) continue; - for (const pathsProperty of property.initializer.properties) { - if (!isPropertyAssignment(pathsProperty) || !isArrayLiteralExpression(pathsProperty.initializer)) continue; - for (const e of pathsProperty.initializer.elements) { - tryUpdateString(e); - } - } + }); + + function updatePaths(property: PropertyAssignment): boolean { + // Type annotation needed due to #7294 + const elements: ReadonlyArray = 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 { @@ -79,23 +98,6 @@ namespace ts { } } - function isPathsPropertyName(propertyName: string): boolean { - switch (propertyName) { - case "includes": - case "files": - case "include": - case "exclude": - case "baseUrl": - case "typeRoots": - case "mapRoot": - case "rootDir": - case "rootDirs": - return true; - default: - return false; - } - } - function updateImports( program: Program, changeTracker: textChanges.ChangeTracker, @@ -180,4 +182,13 @@ namespace ts { function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange { return createTextRange(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); + } + } + } } diff --git a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts index a9f12eec10098..611bff185393a 100644 --- a/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts +++ b/tests/cases/fourslash/getEditsForFileRename_tsconfig.ts @@ -2,17 +2,19 @@ // @Filename: /src/tsconfig.json ////{ +//// "compilerOptions": { +//// "baseUrl": "./old", +//// "mapRoot": "../src/old", +//// "paths": { +//// "foo": ["old"], +//// }, +//// "rootDir": "old", +//// "rootDirs": ["old"], +//// "typeRoots": ["old"], +//// }, //// "files": ["old/a.ts"], //// "include": ["old/*.ts"], //// "exclude": ["old"], -//// "baseUrl": "old", -//// "typeRoots": ["old"], -//// "mapRoot": ["old"], -//// "rootDir": "old", -//// "rootDirs": ["old"], -//// "paths": { -//// "foo": ["old"], -//// }, ////} verify.getEditsForFileRename({ @@ -21,17 +23,19 @@ verify.getEditsForFileRename({ newFileContents: { "/src/tsconfig.json": `{ + "compilerOptions": { + "baseUrl": "new", + "mapRoot": "new", + "paths": { + "foo": ["new"], + }, + "rootDir": "new", + "rootDirs": ["new"], + "typeRoots": ["new"], + }, "files": ["new/a.ts"], "include": ["new/*.ts"], "exclude": ["new"], - "baseUrl": "new", - "typeRoots": ["new"], - "mapRoot": ["new"], - "rootDir": "new", - "rootDirs": ["new"], - "paths": { - "foo": ["new"], - }, }`, }, }); From 2a73698e9050477fcf9a2655831a2941de0823bb Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Thu, 31 May 2018 14:51:12 -0700 Subject: [PATCH 13/13] Use getOptionFromName --- src/compiler/commandLineParser.ts | 3 ++- src/services/getEditsForFileRename.ts | 27 +++++++++++---------------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/compiler/commandLineParser.ts b/src/compiler/commandLineParser.ts index 2f205f1e44e31..621ba3d1cd89c 100644 --- a/src/compiler/commandLineParser.ts +++ b/src/compiler/commandLineParser.ts @@ -929,7 +929,8 @@ namespace ts { } } - function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined { + /** @internal */ + export function getOptionFromName(optionName: string, allowShort = false): CommandLineOption | undefined { optionName = optionName.toLowerCase(); const { optionNameMap, shortOptionNames } = getOptionNameMap(); // Try to translate short option names to their full equivalents. diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts index 6c10932bf7d42..4bac4a2516dc9 100644 --- a/src/services/getEditsForFileRename.ts +++ b/src/services/getEditsForFileRename.ts @@ -49,22 +49,17 @@ namespace ts { } case "compilerOptions": forEachProperty(property.initializer, (property, propertyName) => { - switch (propertyName) { - case "baseUrl": - case "typeRoots": - case "mapRoot": - case "rootDir": - case "rootDirs": - updatePaths(property); - break; - case "paths": - forEachProperty(property.initializer, (pathsProperty) => { - if (!isArrayLiteralExpression(pathsProperty.initializer)) return; - for (const e of pathsProperty.initializer.elements) { - tryUpdateString(e); - } - }); - break; + const option = getOptionFromName(propertyName); + if (option && (option.isFilePath || option.type === "list" && (option as CommandLineOptionOfListType).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); + } + }); } }); break;