@@ -44,6 +44,8 @@ use oxc_semantic::{Semantic, SemanticBuilder, SymbolFlags};
4444use oxc_span:: Atom ;
4545use rustc_hash:: FxHashSet ;
4646
47+ use crate :: optimizer:: Edit ;
48+
4749/// Angular constructor parameter decorators that are removed during compilation.
4850/// These decorators are downleveled to factory metadata and their imports can be elided.
4951///
@@ -719,16 +721,16 @@ impl<'a> ImportElisionAnalyzer<'a> {
719721 }
720722}
721723
722- /// Filter import declarations to remove type-only specifiers .
724+ /// Compute import elision edits as `Vec<Edit>` objects .
723725///
724- /// Returns a new source string with type-only import specifiers removed .
726+ /// Returns edits that remove type-only import specifiers from the source .
725727/// Entire import declarations are removed if all their specifiers are type-only,
726728/// or if the import has no specifiers at all (`import {} from 'module'`).
727- pub fn filter_imports < ' a > (
729+ pub fn import_elision_edits < ' a > (
728730 source : & str ,
729731 program : & Program < ' a > ,
730732 analyzer : & ImportElisionAnalyzer < ' a > ,
731- ) -> String {
733+ ) -> Vec < Edit > {
732734 // Check if there are empty imports that need removal (import {} from '...')
733735 let has_empty_imports = program. body . iter ( ) . any ( |stmt| {
734736 if let Statement :: ImportDeclaration ( import_decl) = stmt {
@@ -740,13 +742,10 @@ pub fn filter_imports<'a>(
740742 } ) ;
741743
742744 if !analyzer. has_type_only_imports ( ) && !has_empty_imports {
743- return source . to_string ( ) ;
745+ return Vec :: new ( ) ;
744746 }
745747
746- // Collect spans to remove (in reverse order for safe removal)
747- let mut removals: Vec < ( usize , usize ) > = Vec :: new ( ) ;
748- // Collect partial replacements (start, end, replacement_string)
749- let mut partial_replacements: Vec < ( usize , usize , String ) > = Vec :: new ( ) ;
748+ let mut edits: Vec < Edit > = Vec :: new ( ) ;
750749
751750 for stmt in & program. body {
752751 let oxc_ast:: ast:: Statement :: ImportDeclaration ( import_decl) = stmt else {
@@ -788,12 +787,10 @@ pub fn filter_imports<'a>(
788787 end += 1 ;
789788 }
790789
791- removals . push ( ( start, end) ) ;
790+ edits . push ( Edit :: delete ( start as u32 , end as u32 ) ) ;
792791 } else {
793792 // Partial removal - reconstruct import with only kept specifiers
794- // We need to rebuild the import statement preserving the original structure
795793
796- // Find default import and named specifiers among kept
797794 let mut default_import: Option < & str > = None ;
798795 let mut named_specifiers: Vec < String > = Vec :: new ( ) ;
799796
@@ -812,7 +809,6 @@ pub fn filter_imports<'a>(
812809 }
813810 }
814811 ImportDeclarationSpecifier :: ImportNamespaceSpecifier ( s) => {
815- // Namespace import: import * as foo
816812 named_specifiers. push ( format ! ( "* as {}" , s. local. name) ) ;
817813 }
818814 }
@@ -830,7 +826,6 @@ pub fn filter_imports<'a>(
830826 }
831827
832828 if !named_specifiers. is_empty ( ) {
833- // Check if any is a namespace import
834829 if named_specifiers. len ( ) == 1 && named_specifiers[ 0 ] . starts_with ( "* as " ) {
835830 new_import. push_str ( & named_specifiers[ 0 ] ) ;
836831 } else {
@@ -852,45 +847,38 @@ pub fn filter_imports<'a>(
852847 let bytes = source. as_bytes ( ) ;
853848 while end < bytes. len ( ) && ( bytes[ end] == b'\n' || bytes[ end] == b'\r' ) {
854849 end += 1 ;
855- // Add newline to replacement
856850 if !new_import. ends_with ( '\n' ) {
857851 new_import. push ( '\n' ) ;
858852 }
859853 }
860854
861- // Store as replacement (start, end, replacement)
862- // We'll handle this differently - store removals with replacement
863- partial_replacements. push ( ( start, end, new_import) ) ;
855+ edits. push ( Edit :: replace ( start as u32 , end as u32 , new_import) ) ;
864856 }
865857 }
866858
867- // Combine full removals (replacement = empty string) with partial replacements
868- // and sort in reverse order for safe string manipulation
869- let mut all_operations: Vec < ( usize , usize , String ) > = Vec :: new ( ) ;
870-
871- for ( start, end) in removals {
872- all_operations. push ( ( start, end, String :: new ( ) ) ) ;
873- }
874- all_operations. extend ( partial_replacements) ;
875-
876- // Sort by start position in reverse order
877- all_operations. sort_by ( |a, b| b. 0 . cmp ( & a. 0 ) ) ;
878-
879- let mut result = source. to_string ( ) ;
880- for ( start, end, replacement) in all_operations {
881- result. replace_range ( start..end, & replacement) ;
882- }
883-
884- result
859+ edits
885860}
886861
887862#[ cfg( test) ]
888863mod tests {
889864 use super :: * ;
865+ use crate :: optimizer:: apply_edits;
890866 use oxc_allocator:: Allocator ;
891867 use oxc_parser:: Parser ;
892868 use oxc_span:: SourceType ;
893869
870+ fn filter_imports < ' a > (
871+ source : & str ,
872+ program : & Program < ' a > ,
873+ analyzer : & ImportElisionAnalyzer < ' a > ,
874+ ) -> String {
875+ let edits = import_elision_edits ( source, program, analyzer) ;
876+ if edits. is_empty ( ) {
877+ return source. to_string ( ) ;
878+ }
879+ apply_edits ( source, edits)
880+ }
881+
894882 fn analyze_source ( source : & str ) -> FxHashSet < String > {
895883 let allocator = Allocator :: default ( ) ;
896884 let source_type = SourceType :: ts ( ) ;
0 commit comments