mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-04 21:53:42 -06:00
fix(55237): TypeScript 5.2 "move to file" results in duplicate imports (#56187)
This commit is contained in:
parent
f0374ce2a9
commit
bf78d175d0
@ -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;
|
||||
}
|
||||
|
||||
24
tests/cases/fourslash/moveToFile_existingImports1.ts
Normal file
24
tests/cases/fourslash/moveToFile_existingImports1.ts
Normal 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" },
|
||||
});
|
||||
28
tests/cases/fourslash/moveToFile_existingImports2.ts
Normal file
28
tests/cases/fourslash/moveToFile_existingImports2.ts
Normal 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" },
|
||||
});
|
||||
26
tests/cases/fourslash/moveToFile_existingImports3.ts
Normal file
26
tests/cases/fourslash/moveToFile_existingImports3.ts
Normal 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" },
|
||||
});
|
||||
30
tests/cases/fourslash/moveToFile_existingImports4.ts
Normal file
30
tests/cases/fourslash/moveToFile_existingImports4.ts
Normal 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" },
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user