@@ -50,6 +50,25 @@ use rustc_hash::FxHashSet;
5050/// Reference: packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts
5151const PARAM_DECORATORS : & [ & str ] = & [ "Inject" , "Optional" , "Self" , "SkipSelf" , "Host" , "Attribute" ] ;
5252
53+ /// Angular member decorators that are removed during compilation.
54+ /// These decorators are compiled into the component/directive definition and their
55+ /// imports can be elided. They are used on class properties and methods.
56+ ///
57+ /// Reference: packages/compiler-cli/src/ngtsc/annotations/directive/src/decorator.ts
58+ const MEMBER_DECORATORS : & [ & str ] = & [
59+ // Property decorators
60+ "Input" ,
61+ "Output" ,
62+ "HostBinding" ,
63+ // Method decorators
64+ "HostListener" ,
65+ // Query decorators
66+ "ViewChild" ,
67+ "ViewChildren" ,
68+ "ContentChild" ,
69+ "ContentChildren" ,
70+ ] ;
71+
5372/// Analyzer for determining which imports are type-only and can be elided.
5473pub struct ImportElisionAnalyzer < ' a > {
5574 /// Set of import specifier local names that should be removed (type-only).
@@ -123,26 +142,27 @@ impl<'a> ImportElisionAnalyzer<'a> {
123142 Self { type_only_specifiers }
124143 }
125144
126- /// Collect import names that are used ONLY in constructor parameter decorators.
145+ /// Collect import names that are used ONLY in Angular decorators that are removed during compilation .
127146 ///
128- /// Constructor parameter decorators like `@Inject`, `@Optional`, `@Self`, `@SkipSelf`,
129- /// `@Host`, and `@Attribute` are removed by Angular's compiler and converted to
130- /// factory metadata. The imports for these decorators and their arguments (like
131- /// `@Inject(TOKEN)`) should be elided.
147+ /// This includes:
148+ /// - Constructor parameter decorators (`@Inject`, `@Optional`, `@Self`, `@SkipSelf`, `@Host`, `@Attribute`)
149+ /// - Member decorators (`@Input`, `@Output`, `@HostBinding`, `@HostListener`, `@ViewChild`, etc.)
132150 ///
133- /// This function returns a set of local import names that should be elided because:
134- /// 1. The import is a known parameter decorator (Inject, Optional, etc.)
135- /// 2. The import is ONLY used as an argument to @Inject()
151+ /// These decorators are compiled into the component/directive definition by Angular's compiler,
152+ /// so their imports can be elided.
136153 ///
137154 /// Reference: packages/compiler-cli/src/ngtsc/transform/jit/src/downlevel_decorators_transform.ts
138155 fn collect_ctor_param_decorator_only_imports ( program : & ' a Program < ' a > ) -> FxHashSet < & ' a str > {
139156 let mut result = FxHashSet :: default ( ) ;
140157
141158 // Track:
142- // 1. Symbols used ONLY in ctor param decorator position (the decorator itself)
143- // 2. Symbols used ONLY as @Inject() arguments
159+ // 1. Symbols used in ctor param decorator position (the decorator itself)
160+ // 2. Symbols used as @Inject() arguments
161+ // 3. Symbols used in member decorator position (property/method decorators)
162+ // 4. Symbols used in other value positions (can't be elided)
144163 let mut ctor_param_decorator_uses: FxHashSet < & ' a str > = FxHashSet :: default ( ) ;
145164 let mut inject_arg_uses: FxHashSet < & ' a str > = FxHashSet :: default ( ) ;
165+ let mut member_decorator_uses: FxHashSet < & ' a str > = FxHashSet :: default ( ) ;
146166 let mut other_value_uses: FxHashSet < & ' a str > = FxHashSet :: default ( ) ;
147167
148168 // Walk the AST to find constructor parameters and their decorators
@@ -151,13 +171,12 @@ impl<'a> ImportElisionAnalyzer<'a> {
151171 stmt,
152172 & mut ctor_param_decorator_uses,
153173 & mut inject_arg_uses,
174+ & mut member_decorator_uses,
154175 & mut other_value_uses,
155176 ) ;
156177 }
157178
158- // A symbol can be elided if:
159- // 1. It's used in ctor param decorators AND NOT used elsewhere, OR
160- // 2. It's used in @Inject() args AND NOT used elsewhere
179+ // A symbol can be elided if it's used only in decorator positions and NOT used elsewhere
161180 for name in ctor_param_decorator_uses {
162181 if !other_value_uses. contains ( name) {
163182 result. insert ( name) ;
@@ -168,6 +187,11 @@ impl<'a> ImportElisionAnalyzer<'a> {
168187 result. insert ( name) ;
169188 }
170189 }
190+ for name in member_decorator_uses {
191+ if !other_value_uses. contains ( name) {
192+ result. insert ( name) ;
193+ }
194+ }
171195
172196 result
173197 }
@@ -177,6 +201,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
177201 stmt : & ' a Statement < ' a > ,
178202 ctor_param_decorator_uses : & mut FxHashSet < & ' a str > ,
179203 inject_arg_uses : & mut FxHashSet < & ' a str > ,
204+ member_decorator_uses : & mut FxHashSet < & ' a str > ,
180205 other_value_uses : & mut FxHashSet < & ' a str > ,
181206 ) {
182207 match stmt {
@@ -185,6 +210,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
185210 class,
186211 ctor_param_decorator_uses,
187212 inject_arg_uses,
213+ member_decorator_uses,
188214 other_value_uses,
189215 ) ;
190216 }
@@ -196,6 +222,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
196222 class,
197223 ctor_param_decorator_uses,
198224 inject_arg_uses,
225+ member_decorator_uses,
199226 other_value_uses,
200227 ) ;
201228 }
@@ -207,6 +234,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
207234 class,
208235 ctor_param_decorator_uses,
209236 inject_arg_uses,
237+ member_decorator_uses,
210238 other_value_uses,
211239 ) ;
212240 }
@@ -232,6 +260,7 @@ impl<'a> ImportElisionAnalyzer<'a> {
232260 class : & ' a oxc_ast:: ast:: Class < ' a > ,
233261 ctor_param_decorator_uses : & mut FxHashSet < & ' a str > ,
234262 inject_arg_uses : & mut FxHashSet < & ' a str > ,
263+ member_decorator_uses : & mut FxHashSet < & ' a str > ,
235264 other_value_uses : & mut FxHashSet < & ' a str > ,
236265 ) {
237266 // Process class decorators - these are NOT elided (they run at runtime)
@@ -243,9 +272,14 @@ impl<'a> ImportElisionAnalyzer<'a> {
243272 for element in & class. body . body {
244273 match element {
245274 ClassElement :: MethodDefinition ( method) => {
246- // Process method decorators (e.g., @HostListener) - NOT elided
275+ // Process method decorators (e.g., @HostListener)
276+ // Angular member decorators are elided; other decorators are kept
247277 for decorator in & method. decorators {
248- Self :: collect_value_uses_from_expr ( & decorator. expression , other_value_uses) ;
278+ Self :: collect_uses_from_member_decorator (
279+ & decorator. expression ,
280+ member_decorator_uses,
281+ other_value_uses,
282+ ) ;
249283 }
250284
251285 if method. kind == MethodDefinitionKind :: Constructor {
@@ -258,9 +292,14 @@ impl<'a> ImportElisionAnalyzer<'a> {
258292 }
259293 }
260294 ClassElement :: PropertyDefinition ( prop) => {
261- // Process property decorators (e.g., @Input, @ViewChild) - NOT elided
295+ // Process property decorators (e.g., @Input, @ViewChild, @HostBinding)
296+ // Angular member decorators are elided; other decorators are kept
262297 for decorator in & prop. decorators {
263- Self :: collect_value_uses_from_expr ( & decorator. expression , other_value_uses) ;
298+ Self :: collect_uses_from_member_decorator (
299+ & decorator. expression ,
300+ member_decorator_uses,
301+ other_value_uses,
302+ ) ;
264303 }
265304 // Process property initializers (e.g., doc = DOCUMENT)
266305 if let Some ( init) = & prop. value {
@@ -269,14 +308,56 @@ impl<'a> ImportElisionAnalyzer<'a> {
269308 }
270309 ClassElement :: AccessorProperty ( prop) => {
271310 for decorator in & prop. decorators {
272- Self :: collect_value_uses_from_expr ( & decorator. expression , other_value_uses) ;
311+ Self :: collect_uses_from_member_decorator (
312+ & decorator. expression ,
313+ member_decorator_uses,
314+ other_value_uses,
315+ ) ;
273316 }
274317 }
275318 _ => { }
276319 }
277320 }
278321 }
279322
323+ /// Collect uses from a member decorator expression.
324+ ///
325+ /// Angular member decorators like `@Input`, `@Output`, `@HostBinding`, `@HostListener`, etc.
326+ /// are compiled into the component/directive definition and can be elided.
327+ /// Non-Angular decorators are tracked as "other value uses" and are not elided.
328+ fn collect_uses_from_member_decorator (
329+ expr : & ' a Expression < ' a > ,
330+ member_decorator_uses : & mut FxHashSet < & ' a str > ,
331+ other_value_uses : & mut FxHashSet < & ' a str > ,
332+ ) {
333+ let decorator_name = match expr {
334+ // @HostBinding (without call)
335+ Expression :: Identifier ( id) => Some ( id. name . as_str ( ) ) ,
336+ // @HostBinding('class.active') or @Input()
337+ Expression :: CallExpression ( call) => {
338+ if let Expression :: Identifier ( id) = & call. callee {
339+ Some ( id. name . as_str ( ) )
340+ } else {
341+ None
342+ }
343+ }
344+ _ => None ,
345+ } ;
346+
347+ if let Some ( name) = decorator_name {
348+ // Check if this is a known Angular member decorator that can be elided
349+ if MEMBER_DECORATORS . contains ( & name) {
350+ member_decorator_uses. insert ( name) ;
351+ } else {
352+ // Non-Angular decorator - treat as value use (not elided)
353+ Self :: collect_value_uses_from_expr ( expr, other_value_uses) ;
354+ }
355+ } else {
356+ // Fallback: collect as value use
357+ Self :: collect_value_uses_from_expr ( expr, other_value_uses) ;
358+ }
359+ }
360+
280361 /// Collect uses from constructor parameters.
281362 ///
282363 /// This is the key function that identifies parameter decorators and their arguments.
@@ -667,6 +748,15 @@ pub fn filter_imports<'a>(
667748
668749 let mut result = source. to_string ( ) ;
669750 for ( start, end, replacement) in all_operations {
751+ // Validate that positions are within bounds and on UTF-8 character boundaries
752+ if start > result. len ( )
753+ || end > result. len ( )
754+ || !result. is_char_boundary ( start)
755+ || !result. is_char_boundary ( end)
756+ {
757+ // Skip this operation - it's invalid for the current source state
758+ continue ;
759+ }
670760 result. replace_range ( start..end, & replacement) ;
671761 }
672762
@@ -717,9 +807,10 @@ class MyComponent {
717807}
718808"# ;
719809 let type_only = analyze_source ( source) ;
720- // Component and Input are used in decorators (value position)
810+ // Component is used as class decorator - preserved
721811 assert ! ( !type_only. contains( "Component" ) ) ;
722- assert ! ( !type_only. contains( "Input" ) ) ;
812+ // Input is a member decorator - elided (compiled into definition)
813+ assert ! ( type_only. contains( "Input" ) , "Input should be elided (member decorator)" ) ;
723814 }
724815
725816 #[ test]
@@ -1001,9 +1092,11 @@ class MyComponent {
10011092 "Translation in type annotation should be elided"
10021093 ) ;
10031094
1004- // Component and Input are used as decorators - value references, preserved
1095+ // Component is the class decorator - preserved
10051096 assert ! ( !type_only. contains( "Component" ) ) ;
1006- assert ! ( !type_only. contains( "Input" ) ) ;
1097+
1098+ // Input is a member decorator - elided (compiled into definition)
1099+ assert ! ( type_only. contains( "Input" ) , "Input should be elided (member decorator)" ) ;
10071100 }
10081101
10091102 #[ test]
@@ -1418,11 +1511,14 @@ export class BitLabelComponent {
14181511 "FormControlComponent should be elided (type annotation)"
14191512 ) ;
14201513
1421- // Component, HostBinding, Input, input should be preserved (runtime decorators/values)
1514+ // Component and input (function call) should be preserved (runtime decorators/values)
1515+ // Note: Component is used as class decorator, input() is a function call
14221516 assert ! ( !type_only. contains( "Component" ) , "Component should be preserved" ) ;
1423- assert ! ( !type_only. contains( "HostBinding" ) , "HostBinding should be preserved" ) ;
1424- assert ! ( !type_only. contains( "Input" ) , "Input should be preserved" ) ;
14251517 assert ! ( !type_only. contains( "input" ) , "input function should be preserved" ) ;
1518+
1519+ // HostBinding and Input should be elided (member decorators are compiled away)
1520+ assert ! ( type_only. contains( "HostBinding" ) , "HostBinding should be elided (member decorator)" ) ;
1521+ assert ! ( type_only. contains( "Input" ) , "Input should be elided (member decorator)" ) ;
14261522 }
14271523
14281524 #[ test]
@@ -1459,12 +1555,14 @@ export class BitLabelComponent {
14591555 import_line
14601556 ) ;
14611557
1462- // Should still contain Component, HostBinding, Input, input
1558+ // Component and input (function call) should be in imports
14631559 assert ! ( import_line. contains( "Component" ) , "Component should be in imports" ) ;
1464- assert ! ( import_line. contains( "HostBinding" ) , "HostBinding should be in imports" ) ;
1465- assert ! ( import_line. contains( "Input" ) , "Input should be in imports" ) ;
14661560 assert ! ( import_line. contains( "input" ) , "input should be in imports" ) ;
14671561
1562+ // Member decorators should be removed (they're compiled into the definition)
1563+ assert ! ( !import_line. contains( "HostBinding" ) , "HostBinding should be removed from imports" ) ;
1564+ assert ! ( !import_line. contains( "Input" ) , "Input should be removed from imports" ) ;
1565+
14681566 // Should NOT contain ElementRef (type annotation)
14691567 assert ! (
14701568 !import_line. contains( "ElementRef" ) ,
0 commit comments