@@ -726,6 +726,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
726726/// Returns edits that remove type-only import specifiers from the source.
727727/// Entire import declarations are removed if all their specifiers are type-only,
728728/// or if the import has no specifiers at all (`import {} from 'module'`).
729+ /// Type-only export declarations (`export type { X }`, `export { type X }`) are also removed.
729730pub fn import_elision_edits < ' a > (
730731 source : & str ,
731732 program : & Program < ' a > ,
@@ -741,7 +742,21 @@ pub fn import_elision_edits<'a>(
741742 false
742743 } ) ;
743744
744- if !analyzer. has_type_only_imports ( ) && !has_empty_imports {
745+ // Check if there are type-only exports that need removal
746+ let has_type_only_exports = program. body . iter ( ) . any ( |stmt| {
747+ if let Statement :: ExportNamedDeclaration ( export_decl) = stmt {
748+ if export_decl. source . is_some ( ) || export_decl. declaration . is_some ( ) {
749+ return export_decl. export_kind . is_type ( ) ;
750+ }
751+ if export_decl. export_kind . is_type ( ) {
752+ return true ;
753+ }
754+ return export_decl. specifiers . iter ( ) . any ( |spec| spec. export_kind . is_type ( ) ) ;
755+ }
756+ false
757+ } ) ;
758+
759+ if !analyzer. has_type_only_imports ( ) && !has_empty_imports && !has_type_only_exports {
745760 return Vec :: new ( ) ;
746761 }
747762
@@ -856,6 +871,79 @@ pub fn import_elision_edits<'a>(
856871 }
857872 }
858873
874+ // Process type-only export declarations
875+ for stmt in & program. body {
876+ let Statement :: ExportNamedDeclaration ( export_decl) = stmt else {
877+ continue ;
878+ } ;
879+
880+ // Skip exports with declarations (e.g. `export class X {}`)
881+ if export_decl. declaration . is_some ( ) {
882+ continue ;
883+ }
884+
885+ if export_decl. export_kind . is_type ( ) {
886+ // `export type { X }` or `export type { X } from './foo'` — remove entirely
887+ let start = export_decl. span . start as usize ;
888+ let mut end = export_decl. span . end as usize ;
889+ let bytes = source. as_bytes ( ) ;
890+ while end < bytes. len ( ) && ( bytes[ end] == b'\n' || bytes[ end] == b'\r' ) {
891+ end += 1 ;
892+ }
893+ edits. push ( Edit :: delete ( start as u32 , end as u32 ) ) ;
894+ continue ;
895+ }
896+
897+ // Check for individual type-only specifiers (`export { type X, Y }`)
898+ let ( type_specs, value_specs) : ( Vec < _ > , Vec < _ > ) =
899+ export_decl. specifiers . iter ( ) . partition ( |spec| spec. export_kind . is_type ( ) ) ;
900+
901+ if type_specs. is_empty ( ) {
902+ continue ;
903+ }
904+
905+ let start = export_decl. span . start as usize ;
906+ let mut end = export_decl. span . end as usize ;
907+ let bytes = source. as_bytes ( ) ;
908+ while end < bytes. len ( ) && ( bytes[ end] == b'\n' || bytes[ end] == b'\r' ) {
909+ end += 1 ;
910+ }
911+
912+ if value_specs. is_empty ( ) {
913+ // All specifiers are type-only — remove entire statement
914+ edits. push ( Edit :: delete ( start as u32 , end as u32 ) ) ;
915+ } else {
916+ // Partial removal — reconstruct with only value specifiers
917+ let mut named_specifiers: Vec < String > = Vec :: new ( ) ;
918+ for spec in & value_specs {
919+ let local_name = spec. local . name ( ) . as_str ( ) ;
920+ let exported_name = spec. exported . name ( ) . as_str ( ) ;
921+ if local_name == exported_name {
922+ named_specifiers. push ( local_name. to_string ( ) ) ;
923+ } else {
924+ named_specifiers. push ( format ! ( "{local_name} as {exported_name}" ) ) ;
925+ }
926+ }
927+
928+ let mut new_export = String :: from ( "export { " ) ;
929+ new_export. push_str ( & named_specifiers. join ( ", " ) ) ;
930+ new_export. push_str ( " }" ) ;
931+
932+ if let Some ( source_lit) = & export_decl. source {
933+ new_export. push_str ( " from \" " ) ;
934+ new_export. push_str ( source_lit. value . as_str ( ) ) ;
935+ new_export. push ( '"' ) ;
936+ }
937+ new_export. push ( ';' ) ;
938+
939+ if end > export_decl. span . end as usize {
940+ new_export. push ( '\n' ) ;
941+ }
942+
943+ edits. push ( Edit :: replace ( start as u32 , end as u32 , new_export) ) ;
944+ }
945+ }
946+
859947 edits
860948}
861949
@@ -1990,4 +2078,97 @@ class UsersTableComponent {}
19902078 filtered
19912079 ) ;
19922080 }
2081+
2082+ #[ test]
2083+ fn test_export_type_with_import_both_removed ( ) {
2084+ let source = r#"
2085+ import { Config } from './config';
2086+ export type { Config };
2087+ "# ;
2088+ let filtered = filter_source ( source) ;
2089+ assert ! (
2090+ !filtered. contains( "Config" ) ,
2091+ "Both import and export type should be removed.\n Filtered:\n {}" ,
2092+ filtered
2093+ ) ;
2094+ assert ! (
2095+ !filtered. contains( "export" ) ,
2096+ "export type declaration should be removed.\n Filtered:\n {}" ,
2097+ filtered
2098+ ) ;
2099+ }
2100+
2101+ #[ test]
2102+ fn test_export_type_multiple_specifiers_removed ( ) {
2103+ let source = r#"
2104+ import { Foo, Bar } from './types';
2105+ export type { Foo, Bar };
2106+ "# ;
2107+ let filtered = filter_source ( source) ;
2108+ assert ! ( !filtered. contains( "Foo" ) , "Foo should be removed.\n Filtered:\n {}" , filtered) ;
2109+ assert ! ( !filtered. contains( "Bar" ) , "Bar should be removed.\n Filtered:\n {}" , filtered) ;
2110+ }
2111+
2112+ #[ test]
2113+ fn test_export_mixed_type_and_value_specifiers ( ) {
2114+ let source = r#"
2115+ import { Component } from '@angular/core';
2116+ import { Foo, Bar } from './types';
2117+ export { type Foo, Bar };
2118+
2119+ @Component({ selector: 'test' })
2120+ class TestComponent {
2121+ value = Bar;
2122+ }
2123+ "# ;
2124+ let filtered = filter_source ( source) ;
2125+ // type Foo should be removed, Bar should remain
2126+ assert ! (
2127+ !filtered. contains( "Foo" ) ,
2128+ "type-only Foo should be removed from export.\n Filtered:\n {}" ,
2129+ filtered
2130+ ) ;
2131+ assert ! (
2132+ filtered. contains( "export { Bar }" ) ,
2133+ "Value export Bar should remain.\n Filtered:\n {}" ,
2134+ filtered
2135+ ) ;
2136+ }
2137+
2138+ #[ test]
2139+ fn test_export_type_with_source_removed ( ) {
2140+ let source = r#"
2141+ import { Component } from '@angular/core';
2142+ export type { Config } from './config';
2143+
2144+ @Component({ selector: 'test' })
2145+ class TestComponent {}
2146+ "# ;
2147+ let filtered = filter_source ( source) ;
2148+ assert ! (
2149+ !filtered. contains( "Config" ) ,
2150+ "export type with source should be removed.\n Filtered:\n {}" ,
2151+ filtered
2152+ ) ;
2153+ }
2154+
2155+ #[ test]
2156+ fn test_value_export_not_affected ( ) {
2157+ let source = r#"
2158+ import { Component } from '@angular/core';
2159+ import { helper } from './utils';
2160+ export { helper };
2161+
2162+ @Component({ selector: 'test' })
2163+ class TestComponent {
2164+ value = helper();
2165+ }
2166+ "# ;
2167+ let filtered = filter_source ( source) ;
2168+ assert ! (
2169+ filtered. contains( "export { helper }" ) ,
2170+ "Value export should not be affected.\n Filtered:\n {}" ,
2171+ filtered
2172+ ) ;
2173+ }
19932174}
0 commit comments