Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a62c84b

Browse files
crisbetoAndrewKushnir
authored andcommitted
fix(migrations): avoid applying the same replacements twice when cleaning up unused imports (#59656)
If a file ends up in multiple programs, the unused imports migration was counting it twice. This was fine since the string replacements were handling it correctly, but it was printing out incorrect data. These changes rework the migration to de-duplicate the replacements and produce the data from the de-duplicated results. PR Close #59656
1 parent 89b391c commit a62c84b

File tree

2 files changed

+70
-20
lines changed

2 files changed

+70
-20
lines changed

‎packages/core/schematics/ng-generate/cleanup-unused-imports/unused_imports_migration.ts

Lines changed: 60 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
MigrationStats,
1414
ProgramInfo,
1515
projectFile,
16+
ProjectFileID,
1617
Replacement,
1718
Serializable,
1819
TextUpdate,
@@ -28,8 +29,8 @@ export interface CompilationUnitData {
2829
/** Text changes that should be performed. */
2930
replacements: Replacement[];
3031

31-
/** Total number of imports that were removed. */
32-
removedImports: number;
32+
/** Identifiers that have been removed from each file. */
33+
removedIdentifiers: NodeID[];
3334

3435
/** Total number of files that were changed. */
3536
changedFiles: number;
@@ -44,7 +45,7 @@ interface RemovalLocations {
4445
partialRemovals: Map<ts.ArrayLiteralExpression, Set<ts.Expression>>;
4546

4647
/** Text of all identifiers that have been removed. */
47-
allRemovedIdentifiers: Set<string>;
48+
allRemovedIdentifiers: Set<ts.Identifier>;
4849
}
4950

5051
/** Tracks how identifiers are used across a single file. */
@@ -60,6 +61,9 @@ interface UsageAnalysis {
6061
identifierCounts: Map<string, number>;
6162
}
6263

64+
/** ID of a node based on its location. */
65+
type NodeID = string & {__nodeID: true};
66+
6367
/** Migration that cleans up unused imports from a project. */
6468
export class UnusedImportsMigration extends TsurgeFunnelMigration<
6569
CompilationUnitData,
@@ -81,7 +85,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
8185
override async analyze(info: ProgramInfo): Promise<Serializable<CompilationUnitData>> {
8286
const nodePositions = new Map<ts.SourceFile, Set<string>>();
8387
const replacements: Replacement[] = [];
84-
let removedImports = 0;
88+
const removedIdentifiers: NodeID[] = [];
8589
let changedFiles = 0;
8690

8791
info.ngCompiler?.getDiagnostics().forEach((diag) => {
@@ -94,7 +98,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
9498
if (!nodePositions.has(diag.file)) {
9599
nodePositions.set(diag.file, new Set());
96100
}
97-
nodePositions.get(diag.file)!.add(this.getNodeKey(diag.start, diag.length));
101+
nodePositions.get(diag.file)!.add(this.getNodeID(diag.start, diag.length));
98102
}
99103
});
100104

@@ -103,14 +107,15 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
103107
const usageAnalysis = this.analyzeUsages(sourceFile, resolvedLocations);
104108

105109
if (resolvedLocations.allRemovedIdentifiers.size > 0) {
106-
removedImports += resolvedLocations.allRemovedIdentifiers.size;
107110
changedFiles++;
111+
resolvedLocations.allRemovedIdentifiers.forEach((identifier) => {
112+
removedIdentifiers.push(this.getNodeID(identifier.getStart(), identifier.getWidth()));
113+
});
108114
}
109-
110115
this.generateReplacements(sourceFile, resolvedLocations, usageAnalysis, info, replacements);
111116
});
112117

113-
return confirmAsSerializable({replacements, removedImports, changedFiles});
118+
return confirmAsSerializable({replacements, removedIdentifiers, changedFiles});
114119
}
115120

116121
override async migrate(globalData: CompilationUnitData) {
@@ -121,10 +126,34 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
121126
unitA: CompilationUnitData,
122127
unitB: CompilationUnitData,
123128
): Promise<Serializable<CompilationUnitData>> {
129+
const combinedReplacements: Replacement[] = [];
130+
const combinedRemovedIdentifiers: NodeID[] = [];
131+
const seenReplacements = new Set<string>();
132+
const seenIdentifiers = new Set<NodeID>();
133+
const changedFileIds = new Set<ProjectFileID>();
134+
135+
[unitA, unitB].forEach((unit) => {
136+
for (const replacement of unit.replacements) {
137+
const key = this.getReplacementID(replacement);
138+
changedFileIds.add(replacement.projectFile.id);
139+
if (!seenReplacements.has(key)) {
140+
seenReplacements.add(key);
141+
combinedReplacements.push(replacement);
142+
}
143+
}
144+
145+
for (const identifier of unit.removedIdentifiers) {
146+
if (!seenIdentifiers.has(identifier)) {
147+
seenIdentifiers.add(identifier);
148+
combinedRemovedIdentifiers.push(identifier);
149+
}
150+
}
151+
});
152+
124153
return confirmAsSerializable({
125-
replacements: [...unitA.replacements, ...unitB.replacements],
126-
removedImports: unitA.removedImports + unitB.removedImports,
127-
changedFiles: unitA.changedFiles + unitB.changedFiles,
154+
replacements: combinedReplacements,
155+
removedIdentifiers: combinedRemovedIdentifiers,
156+
changedFiles: changedFileIds.size,
128157
});
129158
}
130159

@@ -137,15 +166,21 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
137166
override async stats(globalMetadata: CompilationUnitData): Promise<MigrationStats> {
138167
return {
139168
counters: {
140-
removedImports: globalMetadata.removedImports,
169+
removedImports: globalMetadata.removedIdentifiers.length,
141170
changedFiles: globalMetadata.changedFiles,
142171
},
143172
};
144173
}
145174

146-
/** Gets a key that can be used to look up a node based on its location. */
147-
private getNodeKey(start: number, length: number): string {
148-
return `${start}/${length}`;
175+
/** Gets an ID that can be used to look up a node based on its location. */
176+
private getNodeID(start: number, length: number): NodeID {
177+
return `${start}/${length}` as NodeID;
178+
}
179+
180+
/** Gets a unique ID for a replacement. */
181+
private getReplacementID(replacement: Replacement): string {
182+
const {position, end, toInsert} = replacement.update.data;
183+
return replacement.projectFile.id + '/' + position + '/' + end + '/' + toInsert;
149184
}
150185

151186
/**
@@ -176,7 +211,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
176211
return;
177212
}
178213

179-
if (locations.has(this.getNodeKey(node.getStart(), node.getWidth()))) {
214+
if (locations.has(this.getNodeID(node.getStart(), node.getWidth()))) {
180215
// When the entire array needs to be cleared, the diagnostic is
181216
// reported on the property assignment, rather than an array element.
182217
if (
@@ -187,15 +222,15 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
187222
result.fullRemovals.add(parent.initializer);
188223
parent.initializer.elements.forEach((element) => {
189224
if (ts.isIdentifier(element)) {
190-
result.allRemovedIdentifiers.add(element.text);
225+
result.allRemovedIdentifiers.add(element);
191226
}
192227
});
193228
} else if (ts.isArrayLiteralExpression(parent)) {
194229
if (!result.partialRemovals.has(parent)) {
195230
result.partialRemovals.set(parent, new Set());
196231
}
197232
result.partialRemovals.get(parent)!.add(node);
198-
result.allRemovedIdentifiers.add(node.text);
233+
result.allRemovedIdentifiers.add(node);
199234
}
200235
}
201236
};
@@ -326,8 +361,13 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
326361
names.forEach((symbolName, localName) => {
327362
// Note that in the `identifierCounts` lookup both zero and undefined
328363
// are valid and mean that the identifiers isn't being used anymore.
329-
if (allRemovedIdentifiers.has(localName) && !identifierCounts.get(localName)) {
330-
importManager.removeImport(sourceFile, symbolName, moduleName);
364+
if (!identifierCounts.get(localName)) {
365+
for (const identifier of allRemovedIdentifiers) {
366+
if (identifier.text === localName) {
367+
importManager.removeImport(sourceFile, symbolName, moduleName);
368+
break;
369+
}
370+
}
331371
}
332372
});
333373
});

‎packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('cleanup unused imports schematic', () => {
1919
let tree: UnitTestTree;
2020
let tmpDirPath: string;
2121
let previousWorkingDir: string;
22+
let logs: string[];
2223

2324
function writeFile(filePath: string, contents: string) {
2425
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
@@ -36,6 +37,7 @@ describe('cleanup unused imports schematic', () => {
3637
runner = new SchematicTestRunner('test', runfiles.resolvePackageRelative('../collection.json'));
3738
host = new TempScopedNodeJsSyncHost();
3839
tree = new UnitTestTree(new HostTree(host));
40+
logs = [];
3941

4042
writeFile('/tsconfig.json', '{}');
4143
writeFile(
@@ -48,6 +50,7 @@ describe('cleanup unused imports schematic', () => {
4850

4951
previousWorkingDir = shx.pwd();
5052
tmpDirPath = getSystemPath(host.root);
53+
runner.logger.subscribe((log) => logs.push(log.message));
5154

5255
// Switch into the temporary directory path. This allows us to run
5356
// the schematic against our custom unit test tree.
@@ -92,6 +95,7 @@ describe('cleanup unused imports schematic', () => {
9295

9396
await runMigration();
9497

98+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
9599
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
96100
stripWhitespace(`
97101
import {Component} from '@angular/core';
@@ -123,6 +127,7 @@ describe('cleanup unused imports schematic', () => {
123127

124128
await runMigration();
125129

130+
expect(logs.pop()).toBe('Removed 3 imports in 1 file');
126131
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
127132
stripWhitespace(`
128133
import {Component} from '@angular/core';
@@ -153,6 +158,7 @@ describe('cleanup unused imports schematic', () => {
153158

154159
await runMigration();
155160

161+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
156162
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
157163
stripWhitespace(`
158164
import {Component} from '@angular/core';
@@ -190,6 +196,7 @@ describe('cleanup unused imports schematic', () => {
190196

191197
await runMigration();
192198

199+
expect(logs.pop()).toBe('Removed 1 import in 1 file');
193200
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
194201
stripWhitespace(`
195202
import {Component} from '@angular/core';
@@ -226,6 +233,7 @@ describe('cleanup unused imports schematic', () => {
226233

227234
await runMigration();
228235

236+
expect(logs.pop()).toBe('Schematic could not find unused imports in the project');
229237
expect(tree.readContent('comp.ts')).toBe(initialContent);
230238
});
231239

@@ -242,6 +250,7 @@ describe('cleanup unused imports schematic', () => {
242250

243251
await runMigration();
244252

253+
expect(logs.pop()).toBe('Schematic could not find unused imports in the project');
245254
expect(tree.readContent('comp.ts')).toBe(initialContent);
246255
});
247256

@@ -274,6 +283,7 @@ describe('cleanup unused imports schematic', () => {
274283

275284
await runMigration();
276285

286+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
277287
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
278288
stripWhitespace(`
279289
import {Component} from '@angular/core';

0 commit comments

Comments
 (0)