fix(55237): TypeScript 5.2 "move to file" results in duplicate imports (#56187)

This commit is contained in:
Oleksandr T 2023-10-30 19:50:05 +02:00 committed by GitHub
parent f0374ce2a9
commit bf78d175d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 155 additions and 6 deletions

View File

@ -68,6 +68,7 @@ import {
Identifier,
ImportDeclaration,
ImportEqualsDeclaration,
importFromModuleSpecifier,
insertImports,
InterfaceDeclaration,
InternalSymbolName,
@ -84,6 +85,8 @@ import {
isImportDeclaration,
isImportEqualsDeclaration,
isNamedExports,
isNamedImports,
isObjectBindingPattern,
isObjectLiteralExpression,
isOmittedExpression,
isPrologueDirective,
@ -96,6 +99,7 @@ import {
isStringLiteralLike,
isValidTypeOnlyAliasUseSite,
isVariableDeclaration,
isVariableDeclarationInitializedToRequire,
isVariableDeclarationList,
isVariableStatement,
LanguageServiceHost,
@ -194,16 +198,15 @@ function error(notApplicableReason: string) {
function doChange(context: RefactorContext, oldFile: SourceFile, targetFile: string, program: Program, toMove: ToMove, changes: textChanges.ChangeTracker, host: LanguageServiceHost, preferences: UserPreferences): void {
const checker = program.getTypeChecker();
const usage = getUsageInfo(oldFile, toMove.all, checker);
// For a new file
if (!host.fileExists(targetFile)) {
changes.createNewFile(oldFile, targetFile, getNewStatementsAndRemoveFromOldFile(oldFile, targetFile, usage, changes, toMove, program, host, preferences));
changes.createNewFile(oldFile, targetFile, getNewStatementsAndRemoveFromOldFile(oldFile, targetFile, getUsageInfo(oldFile, toMove.all, checker), changes, toMove, program, host, preferences));
addNewFileToTsconfig(program, changes, oldFile.fileName, targetFile, hostGetCanonicalFileName(host));
}
else {
const targetSourceFile = Debug.checkDefined(program.getSourceFile(targetFile));
const importAdder = codefix.createImportAdder(targetSourceFile, context.program, context.preferences, context.host);
getNewStatementsAndRemoveFromOldFile(oldFile, targetSourceFile, usage, changes, toMove, program, host, preferences, importAdder);
getNewStatementsAndRemoveFromOldFile(oldFile, targetSourceFile, getUsageInfo(oldFile, toMove.all, checker, getExistingImports(targetSourceFile, checker)), changes, toMove, program, host, preferences, importAdder);
}
}
@ -993,7 +996,7 @@ function isPureImport(node: Node): boolean {
}
/** @internal */
export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker): UsageInfo {
export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[], checker: TypeChecker, existingTargetImports: ReadonlySet<Symbol> = new Set()): UsageInfo {
const movedSymbols = new Set<Symbol>();
const oldImportsNeededByTargetFile = new Map<Symbol, /*isValidTypeOnlyUseSite*/ boolean>();
const targetFileImportsFromOldFile = new Set<Symbol>();
@ -1010,9 +1013,17 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[],
movedSymbols.add(Debug.checkDefined(isExpressionStatement(decl) ? checker.getSymbolAtLocation(decl.expression.left) : decl.symbol, "Need a symbol here"));
});
}
const unusedImportsFromOldFile = new Set<Symbol>();
for (const statement of toMove) {
forEachReference(statement, checker, (symbol, isValidTypeOnlyUseSite) => {
if (!symbol.declarations) return;
if (!symbol.declarations) {
return;
}
if (existingTargetImports.has(skipAlias(symbol, checker))) {
unusedImportsFromOldFile.add(symbol);
return;
}
for (const decl of symbol.declarations) {
if (isInImport(decl)) {
const prevIsTypeOnly = oldImportsNeededByTargetFile.get(symbol);
@ -1024,7 +1035,10 @@ export function getUsageInfo(oldFile: SourceFile, toMove: readonly Statement[],
}
});
}
const unusedImportsFromOldFile = new Set(oldImportsNeededByTargetFile.keys());
for (const unusedImport of oldImportsNeededByTargetFile.keys()) {
unusedImportsFromOldFile.add(unusedImport);
}
const oldFileImportsFromTargetFile = new Set<Symbol>();
for (const statement of oldFile.statements) {
@ -1228,3 +1242,30 @@ function getOverloadRangeToMove(sourceFile: SourceFile, statement: Statement) {
}
return undefined;
}
function getExistingImports(sourceFile: SourceFile, checker: TypeChecker) {
const imports = new Set<Symbol>();
for (const moduleSpecifier of sourceFile.imports) {
const declaration = importFromModuleSpecifier(moduleSpecifier);
if (
isImportDeclaration(declaration) && declaration.importClause &&
declaration.importClause.namedBindings && isNamedImports(declaration.importClause.namedBindings)
) {
for (const e of declaration.importClause.namedBindings.elements) {
const symbol = checker.getSymbolAtLocation(e.propertyName || e.name);
if (symbol) {
imports.add(skipAlias(symbol, checker));
}
}
}
if (isVariableDeclarationInitializedToRequire(declaration.parent) && isObjectBindingPattern(declaration.parent.name)) {
for (const e of declaration.parent.name.elements) {
const symbol = checker.getSymbolAtLocation(e.propertyName || e.name);
if (symbol) {
imports.add(skipAlias(symbol, checker));
}
}
}
}
return imports;
}

View File

@ -0,0 +1,24 @@
/// <reference path='fourslash.ts' />
// @filename: /common.ts
////export const x = 1;
// @filename: /a.ts
////import { x } from "./common";
////[|export const bar = x;|]
// @filename: /b.ts
////import { x } from "./common";
////export const foo = x;
verify.moveToFile({
newFileContents: {
"/a.ts": "",
"/b.ts":
`import { x } from "./common";
export const foo = x;
export const bar = x;
`,
},
interactiveRefactorArguments: { targetFile: "/b.ts" },
});

View File

@ -0,0 +1,28 @@
/// <reference path='fourslash.ts' />
// @filename: /common.ts
////export const x = 1;
// @filename: /a.ts
////import { x } from "./common";
////export const a = x;
////[|export const b = x;|]
// @filename: /b.ts
////import { x } from "./common";
////export const a = x;
verify.moveToFile({
newFileContents: {
"/a.ts":
`import { x } from "./common";
export const a = x;
`,
"/b.ts":
`import { x } from "./common";
export const a = x;
export const b = x;
`,
},
interactiveRefactorArguments: { targetFile: "/b.ts" },
});

View File

@ -0,0 +1,26 @@
/// <reference path='fourslash.ts' />
// @allowJs: true
// @module: commonjs
// @filename: /common.js
////export const x = 1;
// @filename: /a.js
////const { x } = require("./common");
////[|module.exports.b = x;|]
// @filename: /b.js
////const { x } = require("./common");
////module.exports.a = x;
verify.moveToFile({
newFileContents: {
"/a.js": "",
"/b.js":
`const { x } = require("./common");
module.exports.a = x;
module.exports.b = x;
`,
},
interactiveRefactorArguments: { targetFile: "/b.js" },
});

View File

@ -0,0 +1,30 @@
/// <reference path='fourslash.ts' />
// @allowJs: true
// @module: commonjs
// @filename: /common.js
////export const x = 1;
// @filename: /a.js
////const { x } = require("./common");
////module.exports.a = x;
////[|module.exports.b = x;|]
// @filename: /b.js
////const { x } = require("./common");
////module.exports.a = x;
verify.moveToFile({
newFileContents: {
"/a.js":
`const { x } = require("./common");
module.exports.a = x;
`,
"/b.js":
`const { x } = require("./common");
module.exports.a = x;
module.exports.b = x;
`,
},
interactiveRefactorArguments: { targetFile: "/b.js" },
});