Skip to content

Commit 9e1cd49

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 24bab55 commit 9e1cd49

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
@@ -18,9 +18,10 @@ import {
1818
TsurgeFunnelMigration,
1919
} from '../../utils/tsurge';
2020
import {ErrorCode, FileSystem, ngErrorCode} from '@angular/compiler-cli';
21-
import {DiagnosticCategoryLabel, NgCompilerOptions} from '@angular/compiler-cli/src/ngtsc/core/api';
21+
import {DiagnosticCategoryLabel} from '@angular/compiler-cli/src/ngtsc/core/api';
2222
import {ImportManager} from '@angular/compiler-cli/private/migrations';
2323
import {applyImportManagerChanges} from '../../utils/tsurge/helpers/apply_import_manager';
24+
import {getLeadingLineWhitespaceOfNode} from '../../utils/tsurge/helpers/ast/leading_space';
2425

2526
/** Data produced by the migration for each compilation unit. */
2627
export interface CompilationUnitData {
@@ -286,6 +287,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
286287
const {fullRemovals, partialRemovals, allRemovedIdentifiers} = removalLocations;
287288
const {importedSymbols, identifierCounts} = usages;
288289
const importManager = new ImportManager();
290+
const sourceText = sourceFile.getFullText();
289291

290292
// Replace full arrays with empty ones. This allows preserves more of the user's formatting.
291293
fullRemovals.forEach((node) => {
@@ -302,22 +304,15 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
302304
});
303305

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

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

packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts

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

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy