Skip to content

Commit eceacbc

Browse files
crisbetothePunderWoman
authored andcommitted
fix(migrations): preserve comments when removing unused imports (#61674)
Updates the unused imports schematic to preserve comments inside the array. THis is necessary for some internal use cases. PR Close #61674
1 parent 6ca7590 commit eceacbc

File tree

3 files changed

+133
-17
lines changed

3 files changed

+133
-17
lines changed

packages/core/schematics/ng-generate/cleanup-unused-imports/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ ts_project(
2121
"//packages/compiler-cli/src/ngtsc/core:api",
2222
"//packages/core/schematics/utils",
2323
"//packages/core/schematics/utils/tsurge",
24+
"//packages/core/schematics/utils/tsurge/helpers/ast",
2425
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
2526
],
2627
deps = [

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

Lines changed: 58 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ import {
1717
TsurgeFunnelMigration,
1818
} from '../../utils/tsurge';
1919
import {ErrorCode, FileSystem, ngErrorCode} from '@angular/compiler-cli';
20-
import {DiagnosticCategoryLabel, NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
20+
import {DiagnosticCategoryLabel} from '@angular/compiler-cli/src/ngtsc/core/api';
2121
import {ImportManager} from '@angular/compiler-cli/private/migrations';
2222
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
23+
import {getLeadingLineWhitespaceOfNode} from '../../utils/tsurge/helpers/ast/leading_space';
2324

2425
/** Data produced by the migration for each compilation unit. */
2526
export interface CompilationUnitData {
@@ -283,6 +284,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
283284
const {fullRemovals, partialRemovals, allRemovedIdentifiers} = removalLocations;
284285
const {importedSymbols, identifierCounts} = usages;
285286
const importManager = new ImportManager();
287+
const sourceText = sourceFile.getFullText();
286288

287289
// Replace full arrays with empty ones. This allows preserves more of the user's formatting.
288290
fullRemovals.forEach((node) => {
@@ -299,22 +301,15 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
299301
});
300302

301303
// Filter out the unused identifiers from an array.
302-
partialRemovals.forEach((toRemove, node) => {
303-
const newNode = ts.factory.updateArrayLiteralExpression(
304-
node,
305-
node.elements.filter((el) => !toRemove.has(el)),
306-
);
307-
308-
replacements.push(
309-
new Replacement(
310-
projectFile(sourceFile, info),
311-
new TextUpdate({
312-
position: node.getStart(),
313-
end: node.getEnd(),
314-
toInsert: this.printer.printNode(ts.EmitHint.Unspecified, newNode, sourceFile),
315-
}),
316-
),
317-
);
304+
partialRemovals.forEach((toRemove, parent) => {
305+
toRemove.forEach((node) => {
306+
replacements.push(
307+
new Replacement(
308+
projectFile(sourceFile, info),
309+
getArrayElementRemovalUpdate(node, parent, sourceText),
310+
),
311+
);
312+
});
318313
});
319314

320315
// Attempt to clean up unused import declarations. Note that this isn't foolproof, because we
@@ -336,3 +331,49 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
336331
applyImportManagerChanges(importManager, replacements, [sourceFile], info);
337332
}
338333
}
334+
335+
/** Generates a `TextUpdate` for the removal of an array element. */
336+
function getArrayElementRemovalUpdate(
337+
node: ts.Expression,
338+
parent: ts.ArrayLiteralExpression,
339+
sourceText: string,
340+
): TextUpdate {
341+
let position = node.getStart();
342+
let end = node.getEnd();
343+
let toInsert = '';
344+
const whitespaceOrLineFeed = /\s/;
345+
346+
// Usually the way we'd remove the nodes would be to recreate the `parent` while excluding
347+
// the nodes that should be removed. The problem with this is that it'll strip out comments
348+
// inside the array which can have special meaning internally. We work around it by removing
349+
// only the node's own offsets. This comes with another problem in that it won't remove the commas
350+
// that separate array elements which in turn can look weird if left in place (e.g.
351+
// `[One, Two, Three, Four]` can turn into `[One,,Four]`). To account for them, we start with the
352+
// node's end offset and then expand it to include trailing commas, whitespace and line breaks.
353+
for (let i = end; i < sourceText.length; i++) {
354+
if (sourceText[i] === ',' || whitespaceOrLineFeed.test(sourceText[i])) {
355+
end++;
356+
} else {
357+
break;
358+
}
359+
}
360+
361+
// If we're removing the last element in the array, adjust the starting offset so that
362+
// it includes the previous comma on the same line. This avoids turning something like
363+
// `[One, Two, Three]` into `[One,]`. We only do this within the same like, because
364+
// trailing comma at the end of the line is fine.
365+
if (parent.elements[parent.elements.length - 1] === node) {
366+
for (let i = position - 1; i >= 0; i--) {
367+
if (sourceText[i] === ',' || sourceText[i] === ' ') {
368+
position--;
369+
} else {
370+
break;
371+
}
372+
}
373+
374+
// Replace the node with its leading whitespace to preserve the formatting.
375+
toInsert = getLeadingLineWhitespaceOfNode(node);
376+
}
377+
378+
return new TextUpdate({position, end, toInsert});
379+
}

packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,4 +297,78 @@ describe('cleanup unused imports schematic', () => {
297297
`),
298298
);
299299
});
300+
301+
it('should preserve comments when removing unused imports', async () => {
302+
writeFile(
303+
'comp.ts',
304+
`
305+
import {Component} from '@angular/core';
306+
import {One, Two, Three} from './directives';
307+
308+
@Component({
309+
imports: [
310+
// Start
311+
Three,
312+
One,
313+
Two,
314+
// End
315+
],
316+
template: '<div one></div>',
317+
})
318+
export class Comp {}
319+
`,
320+
);
321+
322+
await runMigration();
323+
324+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
325+
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
326+
stripWhitespace(`
327+
import {Component} from '@angular/core';
328+
import {One} from './directives';
329+
330+
@Component({
331+
imports: [
332+
// Start
333+
One,
334+
// End
335+
],
336+
template: '<div one></div>',
337+
})
338+
export class Comp {}
339+
`),
340+
);
341+
});
342+
343+
it('should preserve inline comments and strip trailing comma when removing imports from same line', async () => {
344+
writeFile(
345+
'comp.ts',
346+
`
347+
import {Component} from '@angular/core';
348+
import {One, Two, Three} from './directives';
349+
350+
@Component({
351+
imports: [/* Start */ Three, One, Two /* End */],
352+
template: '<div one></div>',
353+
})
354+
export class Comp {}
355+
`,
356+
);
357+
358+
await runMigration();
359+
360+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
361+
expect(stripWhitespace(tree.readContent('comp.ts'))).toBe(
362+
stripWhitespace(`
363+
import {Component} from '@angular/core';
364+
import {One} from './directives';
365+
366+
@Component({
367+
imports: [/* Start */ One /* End */],
368+
template: '<div one></div>',
369+
})
370+
export class Comp {}
371+
`),
372+
);
373+
});
300374
});

0 commit comments

Comments
 (0)