Skip to content

Commit 82bf984

Browse files
crisbetopkozlowski-opensource
authored andcommitted
fix(migrations): more robust trailing comma removal in unused imports migration (#62118)
Fixes the following issues with the logic in the unused imports migration that deals with trailing commas: 1. It was generating overlapping text ranges which can break internally. 2. It wasn't handling some cases that produce trailing commas. PR Close #62118
1 parent ae212b5 commit 82bf984

File tree

2 files changed

+66
-20
lines changed

2 files changed

+66
-20
lines changed

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

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {ErrorCode, FileSystem, ngErrorCode} from '@angular/compiler-cli';
2020
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';
2423

2524
/** Data produced by the migration for each compilation unit. */
2625
export interface CompilationUnitData {
@@ -306,10 +305,14 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
306305
replacements.push(
307306
new Replacement(
308307
projectFile(sourceFile, info),
309-
getArrayElementRemovalUpdate(node, parent, sourceText),
308+
getArrayElementRemovalUpdate(node, sourceText),
310309
),
311310
);
312311
});
312+
313+
stripTrailingSameLineCommas(parent, toRemove, sourceText)?.forEach((update) => {
314+
replacements.push(new Replacement(projectFile(sourceFile, info), update));
315+
});
313316
});
314317

315318
// Attempt to clean up unused import declarations. Note that this isn't foolproof, because we
@@ -333,11 +336,7 @@ export class UnusedImportsMigration extends TsurgeFunnelMigration<
333336
}
334337

335338
/** 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 {
339+
function getArrayElementRemovalUpdate(node: ts.Expression, sourceText: string): TextUpdate {
341340
let position = node.getStart();
342341
let end = node.getEnd();
343342
let toInsert = '';
@@ -358,25 +357,49 @@ function getArrayElementRemovalUpdate(
358357
}
359358
}
360359

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-
const char = sourceText[i];
360+
return new TextUpdate({position, end, toInsert});
361+
}
362+
363+
/** Returns `TextUpdate`s that will remove any leftover trailing commas on the same line. */
364+
function stripTrailingSameLineCommas(
365+
node: ts.ArrayLiteralExpression,
366+
toRemove: Set<ts.Expression>,
367+
sourceText: string,
368+
) {
369+
let updates: TextUpdate[] | null = null;
370+
371+
for (let i = 0; i < node.elements.length; i++) {
372+
// Skip over elements that are being removed already.
373+
if (toRemove.has(node.elements[i])) {
374+
continue;
375+
}
376+
377+
// An element might have a trailing comma if all elements after it have been removed.
378+
const mightHaveTrailingComma = node.elements.slice(i + 1).every((e) => toRemove.has(e));
379+
380+
if (!mightHaveTrailingComma) {
381+
continue;
382+
}
383+
384+
const position = node.elements[i].getEnd();
385+
let end = position;
386+
387+
// If the item might have a trailing comma, start looking after it until we hit a line break.
388+
for (let charIndex = position; charIndex < node.getEnd(); charIndex++) {
389+
const char = sourceText[charIndex];
390+
368391
if (char === ',' || char === ' ') {
369-
position--;
392+
end++;
370393
} else {
371-
if (whitespaceOrLineFeed.test(char)) {
372-
// Replace the node with its leading whitespace to preserve the formatting.
373-
// This only needs to happen if we're breaking on a newline.
374-
toInsert = getLeadingLineWhitespaceOfNode(node);
394+
if (char !== '\n' && position !== end) {
395+
updates ??= [];
396+
updates.push(new TextUpdate({position, end, toInsert: ''}));
375397
}
398+
376399
break;
377400
}
378401
}
379402
}
380403

381-
return new TextUpdate({position, end, toInsert});
404+
return updates;
382405
}

packages/core/schematics/test/cleanup_unused_imports_migration_spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,27 @@ describe('cleanup unused imports schematic', () => {
306306
expect(contents).toContain(' imports: [/* Start */ One/* End */],');
307307
expect(contents).toContain(`import {One} from './directives';`);
308308
});
309+
310+
it('should handle all items except the first one being removed', async () => {
311+
writeFile(
312+
'comp.ts',
313+
`
314+
import {Component} from '@angular/core';
315+
import {One, Two, Three} from './directives';
316+
317+
@Component({
318+
imports: [Three, One, Two],
319+
template: '<div three></div>',
320+
})
321+
export class Comp {}
322+
`,
323+
);
324+
325+
await runMigration();
326+
327+
const contents = tree.readContent('comp.ts');
328+
expect(logs.pop()).toBe('Removed 2 imports in 1 file');
329+
expect(contents).toContain('imports: [Three],');
330+
expect(contents).toContain(`import {Three} from './directives';`);
331+
});
309332
});

0 commit comments

Comments
 (0)