@@ -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,105 @@ 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 ! (
2109+ !filtered. contains( "Foo" ) ,
2110+ "Foo should be removed.\n Filtered:\n {}" ,
2111+ filtered
2112+ ) ;
2113+ assert ! (
2114+ !filtered. contains( "Bar" ) ,
2115+ "Bar should be removed.\n Filtered:\n {}" ,
2116+ filtered
2117+ ) ;
2118+ }
2119+
2120+ #[ test]
2121+ fn test_export_mixed_type_and_value_specifiers ( ) {
2122+ let source = r#"
2123+ import { Component } from '@angular/core';
2124+ import { Foo, Bar } from './types';
2125+ export { type Foo, Bar };
2126+
2127+ @Component({ selector: 'test' })
2128+ class TestComponent {
2129+ value = Bar;
2130+ }
2131+ "# ;
2132+ let filtered = filter_source ( source) ;
2133+ // type Foo should be removed, Bar should remain
2134+ assert ! (
2135+ !filtered. contains( "Foo" ) ,
2136+ "type-only Foo should be removed from export.\n Filtered:\n {}" ,
2137+ filtered
2138+ ) ;
2139+ assert ! (
2140+ filtered. contains( "export { Bar }" ) ,
2141+ "Value export Bar should remain.\n Filtered:\n {}" ,
2142+ filtered
2143+ ) ;
2144+ }
2145+
2146+ #[ test]
2147+ fn test_export_type_with_source_removed ( ) {
2148+ let source = r#"
2149+ import { Component } from '@angular/core';
2150+ export type { Config } from './config';
2151+
2152+ @Component({ selector: 'test' })
2153+ class TestComponent {}
2154+ "# ;
2155+ let filtered = filter_source ( source) ;
2156+ assert ! (
2157+ !filtered. contains( "Config" ) ,
2158+ "export type with source should be removed.\n Filtered:\n {}" ,
2159+ filtered
2160+ ) ;
2161+ }
2162+
2163+ #[ test]
2164+ fn test_value_export_not_affected ( ) {
2165+ let source = r#"
2166+ import { Component } from '@angular/core';
2167+ import { helper } from './utils';
2168+ export { helper };
2169+
2170+ @Component({ selector: 'test' })
2171+ class TestComponent {
2172+ value = helper();
2173+ }
2174+ "# ;
2175+ let filtered = filter_source ( source) ;
2176+ assert ! (
2177+ filtered. contains( "export { helper }" ) ,
2178+ "Value export should not be affected.\n Filtered:\n {}" ,
2179+ filtered
2180+ ) ;
2181+ }
19932182}
0 commit comments