@@ -17,9 +17,10 @@ import {
1717 TsurgeFunnelMigration ,
1818} from '../../utils/tsurge' ;
1919import { 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' ;
2121import { ImportManager } from '@angular/compiler-cli/private/migrations' ;
2222import { 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. */
2526export 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+ }
0 commit comments