diff --git a/src/compiler/types.ts b/src/compiler/types.ts
index cdba2e880afc3..77c66574093de 100644
--- a/src/compiler/types.ts
+++ b/src/compiler/types.ts
@@ -10564,6 +10564,13 @@ export interface UserPreferences {
* Default: `500`
*/
readonly maximumHoverLength?: number;
+ /**
+ * If enabled, file rename operations will update string literals in test framework
+ * mocking functions such as jest.mock(), vitest.mock(), etc.
+ *
+ * Default: false
+ */
+ readonly updateImportsInTestFrameworkCalls?: boolean;
}
export type OrganizeImportsTypeOrder = "last" | "inline" | "first";
diff --git a/src/services/getEditsForFileRename.ts b/src/services/getEditsForFileRename.ts
index cffc15aeeabb3..7e4a34e0c8641 100644
--- a/src/services/getEditsForFileRename.ts
+++ b/src/services/getEditsForFileRename.ts
@@ -1,4 +1,5 @@
import {
+ CallExpression,
combinePaths,
createGetCanonicalFileName,
createModuleSpecifierResolutionHost,
@@ -12,6 +13,7 @@ import {
FileTextChanges,
find,
forEach,
+ forEachChild,
formatting,
GetCanonicalFileName,
getDirectoryPath,
@@ -24,15 +26,22 @@ import {
hostUsesCaseSensitiveFileNames,
isAmbientModule,
isArrayLiteralExpression,
+ isCallExpression,
+ isIdentifier,
+ isImportCall,
isObjectLiteralExpression,
+ isPropertyAccessExpression,
isPropertyAssignment,
+ isRequireCall,
isSourceFile,
isStringLiteral,
+ isStringLiteralLike,
LanguageServiceHost,
last,
mapDefined,
ModuleResolutionHost,
moduleSpecifiers,
+ Node,
normalizePath,
pathIsRelative,
Program,
@@ -61,13 +70,44 @@ export function getEditsForFileRename(
sourceMapper: SourceMapper,
): readonly FileTextChanges[] {
const useCaseSensitiveFileNames = hostUsesCaseSensitiveFileNames(host);
- const getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
- const oldToNew = getPathUpdater(oldFileOrDirPath, newFileOrDirPath, getCanonicalFileName, sourceMapper);
- const newToOld = getPathUpdater(newFileOrDirPath, oldFileOrDirPath, getCanonicalFileName, sourceMapper);
- return textChanges.ChangeTracker.with({ host, formatContext, preferences }, changeTracker => {
- updateTsconfigFiles(program, changeTracker, oldToNew, oldFileOrDirPath, newFileOrDirPath, host.getCurrentDirectory(), useCaseSensitiveFileNames);
- updateImports(program, changeTracker, oldToNew, newToOld, host, getCanonicalFileName);
- });
+ const getCanonicalFileName = createGetCanonicalFileName(
+ useCaseSensitiveFileNames,
+ );
+ const oldToNew = getPathUpdater(
+ oldFileOrDirPath,
+ newFileOrDirPath,
+ getCanonicalFileName,
+ sourceMapper,
+ );
+ const newToOld = getPathUpdater(
+ newFileOrDirPath,
+ oldFileOrDirPath,
+ getCanonicalFileName,
+ sourceMapper,
+ );
+ return textChanges.ChangeTracker.with(
+ { host, formatContext, preferences },
+ changeTracker => {
+ updateTsconfigFiles(
+ program,
+ changeTracker,
+ oldToNew,
+ oldFileOrDirPath,
+ newFileOrDirPath,
+ host.getCurrentDirectory(),
+ useCaseSensitiveFileNames,
+ );
+ updateImports(
+ program,
+ changeTracker,
+ oldToNew,
+ newToOld,
+ host,
+ getCanonicalFileName,
+ preferences,
+ );
+ },
+ );
}
/**
@@ -78,30 +118,155 @@ export function getEditsForFileRename(
export type PathUpdater = (path: string) => string | undefined;
// exported for tests
/** @internal */
-export function getPathUpdater(oldFileOrDirPath: string, newFileOrDirPath: string, getCanonicalFileName: GetCanonicalFileName, sourceMapper: SourceMapper | undefined): PathUpdater {
+export function getPathUpdater(
+ oldFileOrDirPath: string,
+ newFileOrDirPath: string,
+ getCanonicalFileName: GetCanonicalFileName,
+ sourceMapper: SourceMapper | undefined,
+): PathUpdater {
const canonicalOldPath = getCanonicalFileName(oldFileOrDirPath);
return path => {
- const originalPath = sourceMapper && sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 });
- const updatedPath = getUpdatedPath(originalPath ? originalPath.fileName : path);
+ const originalPath = sourceMapper &&
+ sourceMapper.tryGetSourcePosition({ fileName: path, pos: 0 });
+ const updatedPath = getUpdatedPath(
+ originalPath ? originalPath.fileName : path,
+ );
return originalPath
- ? updatedPath === undefined ? undefined : makeCorrespondingRelativeChange(originalPath.fileName, updatedPath, path, getCanonicalFileName)
+ ? updatedPath === undefined
+ ? undefined
+ : makeCorrespondingRelativeChange(
+ originalPath.fileName,
+ updatedPath,
+ path,
+ getCanonicalFileName,
+ )
: updatedPath;
};
function getUpdatedPath(pathToUpdate: string): string | undefined {
if (getCanonicalFileName(pathToUpdate) === canonicalOldPath) return newFileOrDirPath;
- const suffix = tryRemoveDirectoryPrefix(pathToUpdate, canonicalOldPath, getCanonicalFileName);
- return suffix === undefined ? undefined : newFileOrDirPath + "/" + suffix;
+ const suffix = tryRemoveDirectoryPrefix(
+ pathToUpdate,
+ canonicalOldPath,
+ getCanonicalFileName,
+ );
+ return suffix === undefined
+ ? undefined
+ : newFileOrDirPath + "/" + suffix;
}
}
// Relative path from a0 to b0 should be same as relative path from a1 to b1. Returns b1.
-function makeCorrespondingRelativeChange(a0: string, b0: string, a1: string, getCanonicalFileName: GetCanonicalFileName): string {
+function makeCorrespondingRelativeChange(
+ a0: string,
+ b0: string,
+ a1: string,
+ getCanonicalFileName: GetCanonicalFileName,
+): string {
const rel = getRelativePathFromFile(a0, b0, getCanonicalFileName);
return combinePathsSafe(getDirectoryPath(a1), rel);
}
-function updateTsconfigFiles(program: Program, changeTracker: textChanges.ChangeTracker, oldToNew: PathUpdater, oldFileOrDirPath: string, newFileOrDirPath: string, currentDirectory: string, useCaseSensitiveFileNames: boolean): void {
+/**
+ * Configuration for test framework function patterns that should be updated during file renames.
+ */
+interface TestFrameworkPattern {
+ /** Object name (e.g., "jest", "vitest") */
+ readonly object: string;
+ /** Method name (e.g., "mock", "requireActual") */
+ readonly method: string;
+ /** 0-based index of the argument that contains the module path */
+ readonly pathArgumentIndex: number;
+}
+
+/**
+ * List of test framework patterns to recognize.
+ * Extensible design - add new patterns here.
+ */
+const testFrameworkPatterns: readonly TestFrameworkPattern[] = [
+ // Jest patterns
+ { object: "jest", method: "mock", pathArgumentIndex: 0 },
+ { object: "jest", method: "unmock", pathArgumentIndex: 0 },
+ { object: "jest", method: "requireActual", pathArgumentIndex: 0 },
+ { object: "jest", method: "requireMock", pathArgumentIndex: 0 },
+ { object: "jest", method: "doMock", pathArgumentIndex: 0 },
+ { object: "jest", method: "dontMock", pathArgumentIndex: 0 },
+
+ // Vitest patterns
+ { object: "vitest", method: "mock", pathArgumentIndex: 0 },
+ { object: "vi", method: "mock", pathArgumentIndex: 0 },
+];
+
+/**
+ * Checks if a call expression is a module loading pattern that should be updated.
+ * Returns the string literal argument if it matches, undefined otherwise.
+ */
+function getModulePathArgument(
+ node: CallExpression,
+ preferences: UserPreferences,
+): StringLiteralLike | undefined {
+ // Check for dynamic import: import("./module")
+ if (isImportCall(node)) {
+ const arg = node.arguments[0];
+ if (arg && isStringLiteralLike(arg)) {
+ return arg;
+ }
+ return undefined;
+ }
+
+ // Check for require: require("./module")
+ if (isRequireCall(node, /*requireStringLiteralLikeArgument*/ true)) {
+ return node.arguments[0];
+ }
+
+ // Check for test framework patterns (opt-in only)
+ if (preferences.updateImportsInTestFrameworkCalls) {
+ // Must have form: object.method(...)
+ if (!isPropertyAccessExpression(node.expression)) {
+ return undefined;
+ }
+
+ const propertyAccess = node.expression;
+
+ // Check if it's a simple identifier (not a nested property access)
+ if (!isIdentifier(propertyAccess.expression)) {
+ return undefined;
+ }
+
+ const objectName = propertyAccess.expression.text;
+ const methodName = propertyAccess.name.text;
+
+ // Find matching pattern
+ const pattern = find(
+ testFrameworkPatterns,
+ p => p.object === objectName && p.method === methodName,
+ );
+
+ if (!pattern) {
+ return undefined;
+ }
+
+ // Get the argument at the specified index
+ const arg = node.arguments[pattern.pathArgumentIndex];
+
+ // Only handle string literals, not template literals with substitutions
+ if (arg && isStringLiteral(arg)) {
+ return arg;
+ }
+ }
+
+ return undefined;
+}
+
+function updateTsconfigFiles(
+ program: Program,
+ changeTracker: textChanges.ChangeTracker,
+ oldToNew: PathUpdater,
+ oldFileOrDirPath: string,
+ newFileOrDirPath: string,
+ currentDirectory: string,
+ useCaseSensitiveFileNames: boolean,
+): void {
const { configFile } = program.getCompilerOptions();
if (!configFile) return;
const configDir = getDirectoryPath(configFile.fileName);
@@ -115,41 +280,86 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change
case "include":
case "exclude": {
const foundExactMatch = updatePaths(property);
- if (foundExactMatch || propertyName !== "include" || !isArrayLiteralExpression(property.initializer)) return;
- const includes = mapDefined(property.initializer.elements, e => isStringLiteral(e) ? e.text : undefined);
+ if (
+ foundExactMatch ||
+ propertyName !== "include" ||
+ !isArrayLiteralExpression(property.initializer)
+ ) return;
+ const includes = mapDefined(
+ property.initializer.elements,
+ e => (isStringLiteral(e) ? e.text : undefined),
+ );
if (includes.length === 0) return;
- const matchers = getFileMatcherPatterns(configDir, /*excludes*/ [], includes, useCaseSensitiveFileNames, currentDirectory);
+ const matchers = getFileMatcherPatterns(
+ configDir,
+ /*excludes*/ [],
+ includes,
+ useCaseSensitiveFileNames,
+ currentDirectory,
+ );
// If there isn't some include for this, add a new one.
if (
- getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(oldFileOrDirPath) &&
- !getRegexFromPattern(Debug.checkDefined(matchers.includeFilePattern), useCaseSensitiveFileNames).test(newFileOrDirPath)
+ getRegexFromPattern(
+ Debug.checkDefined(matchers.includeFilePattern),
+ useCaseSensitiveFileNames,
+ ).test(oldFileOrDirPath) &&
+ !getRegexFromPattern(
+ Debug.checkDefined(matchers.includeFilePattern),
+ useCaseSensitiveFileNames,
+ ).test(newFileOrDirPath)
) {
- changeTracker.insertNodeAfter(configFile, last(property.initializer.elements), factory.createStringLiteral(relativePath(newFileOrDirPath)));
+ changeTracker.insertNodeAfter(
+ configFile,
+ last(property.initializer.elements),
+ factory.createStringLiteral(
+ relativePath(newFileOrDirPath),
+ ),
+ );
}
return;
}
case "compilerOptions":
- forEachProperty(property.initializer, (property, propertyName) => {
- const option = getOptionFromName(propertyName);
- Debug.assert(option?.type !== "listOrElement");
- if (option && (option.isFilePath || option.type === "list" && option.element.isFilePath)) {
- updatePaths(property);
- }
- else if (propertyName === "paths") {
- forEachProperty(property.initializer, pathsProperty => {
- if (!isArrayLiteralExpression(pathsProperty.initializer)) return;
- for (const e of pathsProperty.initializer.elements) {
- tryUpdateString(e);
- }
- });
- }
- });
+ forEachProperty(
+ property.initializer,
+ (property, propertyName) => {
+ const option = getOptionFromName(propertyName);
+ Debug.assert(option?.type !== "listOrElement");
+ if (
+ option &&
+ (option.isFilePath ||
+ (option.type === "list" &&
+ option.element.isFilePath))
+ ) {
+ updatePaths(property);
+ }
+ else if (propertyName === "paths") {
+ forEachProperty(
+ property.initializer,
+ pathsProperty => {
+ if (
+ !isArrayLiteralExpression(
+ pathsProperty.initializer,
+ )
+ ) return;
+ for (
+ const e of pathsProperty.initializer
+ .elements
+ ) {
+ tryUpdateString(e);
+ }
+ },
+ );
+ }
+ },
+ );
return;
}
});
function updatePaths(property: PropertyAssignment): boolean {
- const elements = isArrayLiteralExpression(property.initializer) ? property.initializer.elements : [property.initializer];
+ const elements = isArrayLiteralExpression(property.initializer)
+ ? property.initializer.elements
+ : [property.initializer];
let foundExactMatch = false;
for (const element of elements) {
foundExactMatch = tryUpdateString(element) || foundExactMatch;
@@ -163,14 +373,22 @@ function updateTsconfigFiles(program: Program, changeTracker: textChanges.Change
const updated = oldToNew(elementFileName);
if (updated !== undefined) {
- changeTracker.replaceRangeWithText(configFile!, createStringRange(element, configFile!), relativePath(updated));
+ changeTracker.replaceRangeWithText(
+ configFile!,
+ createStringRange(element, configFile!),
+ relativePath(updated),
+ );
return true;
}
return false;
}
function relativePath(path: string): string {
- return getRelativePathFromDirectory(configDir, path, /*ignoreCase*/ !useCaseSensitiveFileNames);
+ return getRelativePathFromDirectory(
+ configDir,
+ path,
+ /*ignoreCase*/ !useCaseSensitiveFileNames,
+ );
}
}
@@ -181,6 +399,7 @@ function updateImports(
newToOld: PathUpdater,
host: LanguageServiceHost,
getCanonicalFileName: GetCanonicalFileName,
+ preferences: UserPreferences,
): void {
const allFiles = program.getSourceFiles();
for (const sourceFile of allFiles) {
@@ -194,27 +413,76 @@ function updateImports(
const importingSourceFileMoved = newFromOld !== undefined || oldFromNew !== undefined;
- updateImportsWorker(sourceFile, changeTracker, referenceText => {
- if (!pathIsRelative(referenceText)) return undefined;
- const oldAbsolute = combinePathsSafe(oldImportFromDirectory, referenceText);
- const newAbsolute = oldToNew(oldAbsolute);
- return newAbsolute === undefined ? undefined : ensurePathIsNonModuleName(getRelativePathFromDirectory(newImportFromDirectory, newAbsolute, getCanonicalFileName));
- }, importLiteral => {
- const importedModuleSymbol = program.getTypeChecker().getSymbolAtLocation(importLiteral);
- // No need to update if it's an ambient module^M
- if (importedModuleSymbol?.declarations && importedModuleSymbol.declarations.some(d => isAmbientModule(d))) return undefined;
-
- const toImport = oldFromNew !== undefined
- // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location.
- // TODO:GH#18217
- ? getSourceFileToImportFromResolved(importLiteral, resolveModuleName(importLiteral.text, oldImportFromPath, program.getCompilerOptions(), host as ModuleResolutionHost), oldToNew, allFiles)
- : getSourceFileToImport(importedModuleSymbol, importLiteral, sourceFile, program, host, oldToNew);
-
- // Need an update if the imported file moved, or the importing file moved and was using a relative path.
- return toImport !== undefined && (toImport.updated || (importingSourceFileMoved && pathIsRelative(importLiteral.text)))
- ? moduleSpecifiers.updateModuleSpecifier(program.getCompilerOptions(), sourceFile, newImportFromPath, toImport.newFileName, createModuleSpecifierResolutionHost(program, host), importLiteral.text)
- : undefined;
- });
+ updateImportsWorker(
+ sourceFile,
+ changeTracker,
+ referenceText => {
+ if (!pathIsRelative(referenceText)) return undefined;
+ const oldAbsolute = combinePathsSafe(
+ oldImportFromDirectory,
+ referenceText,
+ );
+ const newAbsolute = oldToNew(oldAbsolute);
+ return newAbsolute === undefined
+ ? undefined
+ : ensurePathIsNonModuleName(
+ getRelativePathFromDirectory(
+ newImportFromDirectory,
+ newAbsolute,
+ getCanonicalFileName,
+ ),
+ );
+ },
+ importLiteral => {
+ const importedModuleSymbol = program
+ .getTypeChecker()
+ .getSymbolAtLocation(importLiteral);
+ // No need to update if it's an ambient module^M
+ if (
+ importedModuleSymbol?.declarations &&
+ importedModuleSymbol.declarations.some(d => isAmbientModule(d))
+ ) return undefined;
+
+ const toImport = oldFromNew !== undefined
+ // If we're at the new location (file was already renamed), need to redo module resolution starting from the old location.
+ // TODO:GH#18217
+ ? getSourceFileToImportFromResolved(
+ importLiteral,
+ resolveModuleName(
+ importLiteral.text,
+ oldImportFromPath,
+ program.getCompilerOptions(),
+ host as ModuleResolutionHost,
+ ),
+ oldToNew,
+ allFiles,
+ )
+ : getSourceFileToImport(
+ importedModuleSymbol,
+ importLiteral,
+ sourceFile,
+ program,
+ host,
+ oldToNew,
+ );
+
+ // Need an update if the imported file moved, or the importing file moved and was using a relative path.
+ return toImport !== undefined &&
+ (toImport.updated ||
+ (importingSourceFileMoved &&
+ pathIsRelative(importLiteral.text)))
+ ? moduleSpecifiers.updateModuleSpecifier(
+ program.getCompilerOptions(),
+ sourceFile,
+ newImportFromPath,
+ toImport.newFileName,
+ createModuleSpecifierResolutionHost(program, host),
+ importLiteral.text,
+ )
+ : undefined;
+ },
+ preferences,
+ );
}
}
@@ -240,20 +508,46 @@ function getSourceFileToImport(
): ToImport | undefined {
if (importedModuleSymbol) {
// `find` should succeed because we checked for ambient modules before calling this function.
- const oldFileName = find(importedModuleSymbol.declarations, isSourceFile)!.fileName;
+ const oldFileName = find(
+ importedModuleSymbol.declarations,
+ isSourceFile,
+ )!.fileName;
const newFileName = oldToNew(oldFileName);
- return newFileName === undefined ? { newFileName: oldFileName, updated: false } : { newFileName, updated: true };
+ return newFileName === undefined
+ ? { newFileName: oldFileName, updated: false }
+ : { newFileName, updated: true };
}
else {
- const mode = program.getModeForUsageLocation(importingSourceFile, importLiteral);
- const resolved = host.resolveModuleNameLiterals || !host.resolveModuleNames ?
- program.getResolvedModuleFromModuleSpecifier(importLiteral, importingSourceFile) :
- host.getResolvedModuleWithFailedLookupLocationsFromCache && host.getResolvedModuleWithFailedLookupLocationsFromCache(importLiteral.text, importingSourceFile.fileName, mode);
- return getSourceFileToImportFromResolved(importLiteral, resolved, oldToNew, program.getSourceFiles());
+ const mode = program.getModeForUsageLocation(
+ importingSourceFile,
+ importLiteral,
+ );
+ const resolved = host.resolveModuleNameLiterals || !host.resolveModuleNames
+ ? program.getResolvedModuleFromModuleSpecifier(
+ importLiteral,
+ importingSourceFile,
+ )
+ : host.getResolvedModuleWithFailedLookupLocationsFromCache &&
+ host.getResolvedModuleWithFailedLookupLocationsFromCache(
+ importLiteral.text,
+ importingSourceFile.fileName,
+ mode,
+ );
+ return getSourceFileToImportFromResolved(
+ importLiteral,
+ resolved,
+ oldToNew,
+ program.getSourceFiles(),
+ );
}
}
-function getSourceFileToImportFromResolved(importLiteral: StringLiteralLike, resolved: ResolvedModuleWithFailedLookupLocations | undefined, oldToNew: PathUpdater, sourceFiles: readonly SourceFile[]): ToImport | undefined {
+function getSourceFileToImportFromResolved(
+ importLiteral: StringLiteralLike,
+ resolved: ResolvedModuleWithFailedLookupLocations | undefined,
+ oldToNew: PathUpdater,
+ sourceFiles: readonly SourceFile[],
+): ToImport | undefined {
// Search through all locations looking for a moved file, and only then test already existing files.
// This is because if `a.ts` is compiled to `a.js` and `a.ts` is moved, we don't want to resolve anything to `a.js`, but to `a.ts`'s new location.
if (!resolved) return undefined;
@@ -265,23 +559,39 @@ function getSourceFileToImportFromResolved(importLiteral: StringLiteralLike, res
}
// Then failed lookups that are in the list of sources
- const result = forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJsonExisting)
+ const result = forEach(
+ resolved.failedLookupLocations,
+ tryChangeWithIgnoringPackageJsonExisting,
+ ) ||
// Then failed lookups except package.json since we dont want to touch them (only included ts/js files).
// At this point, the confidence level of this fix being correct is too low to change bare specifiers or absolute paths.
- || pathIsRelative(importLiteral.text) && forEach(resolved.failedLookupLocations, tryChangeWithIgnoringPackageJson);
+ (pathIsRelative(importLiteral.text) &&
+ forEach(
+ resolved.failedLookupLocations,
+ tryChangeWithIgnoringPackageJson,
+ ));
if (result) return result;
// If nothing changed, then result is resolved module file thats not updated
- return resolved.resolvedModule && { newFileName: resolved.resolvedModule.resolvedFileName, updated: false };
+ return (
+ resolved.resolvedModule && {
+ newFileName: resolved.resolvedModule.resolvedFileName,
+ updated: false,
+ }
+ );
function tryChangeWithIgnoringPackageJsonExisting(oldFileName: string) {
const newFileName = oldToNew(oldFileName);
- return newFileName && find(sourceFiles, src => src.fileName === newFileName)
- ? tryChangeWithIgnoringPackageJson(oldFileName) : undefined;
+ return newFileName &&
+ find(sourceFiles, src => src.fileName === newFileName)
+ ? tryChangeWithIgnoringPackageJson(oldFileName)
+ : undefined;
}
function tryChangeWithIgnoringPackageJson(oldFileName: string) {
- return !endsWith(oldFileName, "/package.json") ? tryChange(oldFileName) : undefined;
+ return !endsWith(oldFileName, "/package.json")
+ ? tryChange(oldFileName)
+ : undefined;
}
function tryChange(oldFileName: string) {
@@ -290,23 +600,93 @@ function getSourceFileToImportFromResolved(importLiteral: StringLiteralLike, res
}
}
-function updateImportsWorker(sourceFile: SourceFile, changeTracker: textChanges.ChangeTracker, updateRef: (refText: string) => string | undefined, updateImport: (importLiteral: StringLiteralLike) => string | undefined) {
- for (const ref of sourceFile.referencedFiles || emptyArray) { // TODO: GH#26162
+function updateImportsWorker(
+ sourceFile: SourceFile,
+ changeTracker: textChanges.ChangeTracker,
+ updateRef: (refText: string) => string | undefined,
+ updateImport: (importLiteral: StringLiteralLike) => string | undefined,
+ preferences: UserPreferences,
+) {
+ for (const ref of sourceFile.referencedFiles || emptyArray) {
+ // TODO: GH#26162
const updated = updateRef(ref.fileName);
- if (updated !== undefined && updated !== sourceFile.text.slice(ref.pos, ref.end)) changeTracker.replaceRangeWithText(sourceFile, ref, updated);
+ 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);
+ if (updated !== undefined && updated !== importStringLiteral.text) {
+ changeTracker.replaceRangeWithText(
+ sourceFile,
+ createStringRange(importStringLiteral, sourceFile),
+ updated,
+ );
+ }
}
+
+ // Update additional module loading patterns (dynamic import, require, test mocks)
+ // This catches patterns that aren't in sourceFile.imports (like require in .ts files)
+ updateModuleLoadingCalls(
+ sourceFile,
+ changeTracker,
+ updateImport,
+ preferences,
+ );
+}
+
+/**
+ * Updates module loading calls (dynamic import, require, test framework mocks) in a source file.
+ * Handles import(), require(), jest.mock(), vitest.mock(), etc.
+ */
+function updateModuleLoadingCalls(
+ sourceFile: SourceFile,
+ changeTracker: textChanges.ChangeTracker,
+ updateImport: (importLiteral: StringLiteralLike) => string | undefined,
+ preferences: UserPreferences,
+): void {
+ // Create a set of string literals already processed from sourceFile.imports to avoid duplicates
+ const processedImports = new Set(sourceFile.imports);
+
+ // Visitor pattern - recursively walk the AST
+ function visitor(node: Node): void {
+ // Check for call expressions
+ if (isCallExpression(node)) {
+ const pathArgument = getModulePathArgument(node, preferences);
+ // Skip if this path argument was already processed from sourceFile.imports
+ if (pathArgument && !processedImports.has(pathArgument)) {
+ const updated = updateImport(pathArgument);
+ if (updated !== undefined && updated !== pathArgument.text) {
+ changeTracker.replaceRangeWithText(
+ sourceFile,
+ createStringRange(pathArgument, sourceFile),
+ updated,
+ );
+ }
+ }
+ }
+
+ // Continue walking the tree
+ forEachChild(node, visitor);
+ }
+
+ // Start the traversal
+ visitor(sourceFile);
}
-function createStringRange(node: StringLiteralLike, sourceFile: SourceFileLike): TextRange {
+function createStringRange(
+ node: StringLiteralLike,
+ sourceFile: SourceFileLike,
+): TextRange {
return createRange(node.getStart(sourceFile) + 1, node.end - 1);
}
-function forEachProperty(objectLiteral: Expression, cb: (property: PropertyAssignment, propertyName: string) => void) {
+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)) {
diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts
index ba6404355b3c3..a926b6d072bac 100644
--- a/tests/baselines/reference/api/typescript.d.ts
+++ b/tests/baselines/reference/api/typescript.d.ts
@@ -8430,6 +8430,13 @@ declare namespace ts {
* Default: `500`
*/
readonly maximumHoverLength?: number;
+ /**
+ * If enabled, file rename operations will update string literals in test framework
+ * mocking functions such as jest.mock(), vitest.mock(), etc.
+ *
+ * Default: false
+ */
+ readonly updateImportsInTestFrameworkCalls?: boolean;
}
type OrganizeImportsTypeOrder = "last" | "inline" | "first";
/** Represents a bigint literal value without requiring bigint support */
diff --git a/tests/cases/fourslash/getEditsForFileRename_dynamicImport.ts b/tests/cases/fourslash/getEditsForFileRename_dynamicImport.ts
new file mode 100644
index 0000000000000..2ca96188d3e0f
--- /dev/null
+++ b/tests/cases/fourslash/getEditsForFileRename_dynamicImport.ts
@@ -0,0 +1,20 @@
+///
+
+// @Filename: /src/old.ts
+////export const value = 1;
+
+// @Filename: /src/test.ts
+////import("./old");
+////const x = import("./old");
+////import { value } from "./old";
+
+verify.getEditsForFileRename({
+ oldPath: "/src/old.ts",
+ newPath: "/src/new.ts",
+ newFileContents: {
+ "/src/test.ts":
+`import("./new");
+const x = import("./new");
+import { value } from "./new";`,
+ },
+});
diff --git a/tests/cases/fourslash/getEditsForFileRename_jestMock.ts b/tests/cases/fourslash/getEditsForFileRename_jestMock.ts
new file mode 100644
index 0000000000000..6e9ee6f2ba38d
--- /dev/null
+++ b/tests/cases/fourslash/getEditsForFileRename_jestMock.ts
@@ -0,0 +1,23 @@
+///
+
+// @Filename: /src/utils/old.ts
+////export const helper = () => 42;
+
+// @Filename: /src/utils/__tests__/helper.test.ts
+////jest.mock("../old");
+////jest.requireActual("../old");
+////import { helper } from "../old";
+
+verify.getEditsForFileRename({
+ oldPath: "/src/utils/old.ts",
+ newPath: "/src/utils/new.ts",
+ newFileContents: {
+ "/src/utils/__tests__/helper.test.ts":
+`jest.mock("../new");
+jest.requireActual("../new");
+import { helper } from "../new";`,
+ },
+ preferences: {
+ updateImportsInTestFrameworkCalls: true,
+ },
+});
diff --git a/tests/cases/fourslash/getEditsForFileRename_requireInTs.ts b/tests/cases/fourslash/getEditsForFileRename_requireInTs.ts
new file mode 100644
index 0000000000000..4fbda1f8edac5
--- /dev/null
+++ b/tests/cases/fourslash/getEditsForFileRename_requireInTs.ts
@@ -0,0 +1,18 @@
+///
+
+// @Filename: /src/old.ts
+////export const value = 1;
+
+// @Filename: /src/test.ts
+////const old = require("./old");
+////import { value } from "./old";
+
+verify.getEditsForFileRename({
+ oldPath: "/src/old.ts",
+ newPath: "/src/new.ts",
+ newFileContents: {
+ "/src/test.ts":
+`const old = require("./new");
+import { value } from "./new";`,
+ },
+});