@@ -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. */
6468export 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 } ) ;
0 commit comments