@@ -114,8 +114,13 @@ impl<'a> ImportElisionAnalyzer<'a> {
114114 type_only_specifiers. insert ( name. clone ( ) ) ;
115115 }
116116 }
117- ImportDeclarationSpecifier :: ImportNamespaceSpecifier ( _) => {
118- // Namespace imports are generally kept (hard to determine type-only usage)
117+ ImportDeclarationSpecifier :: ImportNamespaceSpecifier ( spec) => {
118+ // Check if the namespace import is type-only using semantic analysis.
119+ // e.g., `import * as moment from 'moment'` where `moment` is only
120+ // used in type annotations like `moment.Moment`.
121+ if Self :: is_type_only_import ( & spec. local , semantic) {
122+ type_only_specifiers. insert ( spec. local . name . clone ( ) ) ;
123+ }
119124 }
120125 }
121126 }
@@ -587,9 +592,25 @@ impl<'a> ImportElisionAnalyzer<'a> {
587592 }
588593 }
589594 }
590- Expression :: ArrowFunctionExpression ( _) => {
591- // For arrow functions, we don't need to deeply analyze the body
592- // since we're only checking decorator positions
595+ Expression :: ArrowFunctionExpression ( arrow) => {
596+ // Arrow function bodies may contain value references, e.g.,
597+ // `forwardRef(() => TagPickerComponent)` in Component imports.
598+ for stmt in & arrow. body . statements {
599+ match stmt {
600+ Statement :: ExpressionStatement ( expr_stmt) => {
601+ Self :: collect_value_uses_from_expr (
602+ & expr_stmt. expression ,
603+ other_value_uses,
604+ ) ;
605+ }
606+ Statement :: ReturnStatement ( ret) => {
607+ if let Some ( arg) = & ret. argument {
608+ Self :: collect_value_uses_from_expr ( arg, other_value_uses) ;
609+ }
610+ }
611+ _ => { }
612+ }
613+ }
593614 }
594615 _ => { }
595616 }
@@ -701,13 +722,24 @@ impl<'a> ImportElisionAnalyzer<'a> {
701722/// Filter import declarations to remove type-only specifiers.
702723///
703724/// Returns a new source string with type-only import specifiers removed.
704- /// Entire import declarations are removed if all their specifiers are type-only.
725+ /// Entire import declarations are removed if all their specifiers are type-only,
726+ /// or if the import has no specifiers at all (`import {} from 'module'`).
705727pub fn filter_imports < ' a > (
706728 source : & str ,
707729 program : & Program < ' a > ,
708730 analyzer : & ImportElisionAnalyzer < ' a > ,
709731) -> String {
710- if !analyzer. has_type_only_imports ( ) {
732+ // Check if there are empty imports that need removal (import {} from '...')
733+ let has_empty_imports = program. body . iter ( ) . any ( |stmt| {
734+ if let Statement :: ImportDeclaration ( import_decl) = stmt {
735+ if let Some ( specifiers) = & import_decl. specifiers {
736+ return specifiers. is_empty ( ) ;
737+ }
738+ }
739+ false
740+ } ) ;
741+
742+ if !analyzer. has_type_only_imports ( ) && !has_empty_imports {
711743 return source. to_string ( ) ;
712744 }
713745
@@ -740,8 +772,8 @@ pub fn filter_imports<'a>(
740772 !analyzer. should_elide ( name)
741773 } ) ;
742774
743- if removed. is_empty ( ) {
744- // All specifiers kept, no changes needed
775+ if removed. is_empty ( ) && !kept . is_empty ( ) {
776+ // All specifiers kept and at least one exists , no changes needed
745777 continue ;
746778 }
747779
@@ -1870,4 +1902,104 @@ class MyComponent {
18701902 "myKey in parenthesized type literal should be preserved"
18711903 ) ;
18721904 }
1905+
1906+ // =========================================================================
1907+ // Regression tests for ClickUp import elision mismatches
1908+ // =========================================================================
1909+
1910+ #[ test]
1911+ fn test_namespace_import_type_only_should_be_elided ( ) {
1912+ // Reproduces: bookmark.component.ts
1913+ // `import * as moment from 'moment'` where `moment` is only used as
1914+ // `moment.Moment` in type annotations should be elided.
1915+ let source = r#"
1916+ import * as moment from 'moment';
1917+ import { Component } from '@angular/core';
1918+
1919+ @Component({ selector: 'app-bookmark' })
1920+ class BookmarkComponent {
1921+ dueDate: moment.Moment = null;
1922+ }
1923+ "# ;
1924+ let type_only = analyze_source ( source) ;
1925+ // `moment` is only referenced in type position (moment.Moment)
1926+ // so it should be marked for elision
1927+ assert ! (
1928+ type_only. contains( "moment" ) ,
1929+ "Namespace import `moment` used only in type annotation `moment.Moment` should be elided"
1930+ ) ;
1931+ }
1932+
1933+ #[ test]
1934+ fn test_namespace_import_with_value_usage_preserved ( ) {
1935+ // Namespace import that IS used at runtime should be preserved
1936+ let source = r#"
1937+ import * as moment from 'moment';
1938+ import { Component } from '@angular/core';
1939+
1940+ @Component({ selector: 'app-test' })
1941+ class TestComponent {
1942+ now = moment();
1943+ }
1944+ "# ;
1945+ let type_only = analyze_source ( source) ;
1946+ assert ! (
1947+ !type_only. contains( "moment" ) ,
1948+ "Namespace import `moment` used in value expression `moment()` should be preserved"
1949+ ) ;
1950+ }
1951+
1952+ #[ test]
1953+ fn test_forward_ref_arrow_function_preserves_value_use ( ) {
1954+ // Reproduces: tags.component.ts
1955+ // `forwardRef(() => TagPickerComponent)` in @Component imports array
1956+ // uses TagPickerComponent as a value inside an arrow function.
1957+ // The arrow function body MUST be traversed to find value uses.
1958+ let source = r#"
1959+ import { Component, forwardRef, Inject, Optional, SkipSelf } from '@angular/core';
1960+ import { TagPickerComponent } from './tag-picker/tag-picker.component';
1961+
1962+ @Component({
1963+ selector: 'app-tags',
1964+ imports: [forwardRef(() => TagPickerComponent)]
1965+ })
1966+ class TagsComponent {
1967+ constructor(
1968+ @Optional()
1969+ @SkipSelf()
1970+ @Inject(TagPickerComponent)
1971+ readonly tagPickerComponent: TagPickerComponent,
1972+ ) {}
1973+ }
1974+ "# ;
1975+ let type_only = analyze_source ( source) ;
1976+ // TagPickerComponent is used as a value in forwardRef(() => TagPickerComponent)
1977+ // and as @Inject(TagPickerComponent) argument. It must NOT be elided.
1978+ assert ! (
1979+ !type_only. contains( "TagPickerComponent" ) ,
1980+ "TagPickerComponent used in forwardRef arrow function and @Inject should be preserved"
1981+ ) ;
1982+ }
1983+
1984+ #[ test]
1985+ fn test_empty_import_should_be_elided ( ) {
1986+ // Reproduces: users-table.component.ts
1987+ // `import {} from '@cu/teams-pulse/types'` is an empty import that
1988+ // should be completely removed (no specifiers to keep).
1989+ let source = r#"
1990+ import { Component } from '@angular/core';
1991+ import {} from '@cu/teams-pulse/types';
1992+
1993+ @Component({ selector: 'app-users-table' })
1994+ class UsersTableComponent {}
1995+ "# ;
1996+ let filtered = filter_source ( source) ;
1997+
1998+ // The empty import should be removed entirely
1999+ assert ! (
2000+ !filtered. contains( "@cu/teams-pulse/types" ) ,
2001+ "Empty import `import {{}} from '@cu/teams-pulse/types'` should be removed.\n Filtered:\n {}" ,
2002+ filtered
2003+ ) ;
2004+ }
18732005}
0 commit comments