From ed0bde2ef519c9b331cb1d8004bd072af2bb9d31 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Fri, 23 Oct 2026 17:33:51 +0800 Subject: [PATCH] fix(import-elision): count an aliased Angular param decorator as removed `@I(TOKEN)` for `import { Inject as I }` is removed like `@Inject(TOKEN)`, but the removed-reference set matched the local name, so a file with both kept an unused TOKEN import. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/component/import_elision.rs | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/crates/oxc_angular_compiler/src/component/import_elision.rs b/crates/oxc_angular_compiler/src/component/import_elision.rs index 7bb4ce954..3e6c1cf54 100644 --- a/crates/oxc_angular_compiler/src/component/import_elision.rs +++ b/crates/oxc_angular_compiler/src/component/import_elision.rs @@ -951,7 +951,13 @@ impl<'a> ImportElisionAnalyzer<'a> { { for param in &method.value.params.items { for decorator in ¶m.decorators { - if Self::is_removed_param_decorator(decorator, consts) { + // by what it resolves to: `@I(TOKEN)` for `import { Inject as I }` is removed too + if crate::directive::angular_param_decorator( + decorator, + Some(consts), + ) + .is_some() + { references.visit_decorator(decorator); } } @@ -969,24 +975,6 @@ impl<'a> ImportElisionAnalyzer<'a> { references.0 } - /// One of Angular's constructor parameter decorators (`@Inject(TOKEN)`, `@Optional()`, ...), which compilation - /// removes; another module's `@Inject` stays in the output (like ngtsc) - fn is_removed_param_decorator( - decorator: &'a oxc_ast::ast::Decorator<'a>, - consts: &crate::directive::StringConsts<'a>, - ) -> bool { - let name = match &decorator.expression { - Expression::Identifier(id) => id.name.as_str(), - Expression::CallExpression(call) => match &call.callee { - Expression::Identifier(id) => id.name.as_str(), - _ => return false, - }, - _ => return false, - }; - PARAM_DECORATORS.contains(&name) - && crate::directive::angular_param_decorator(decorator, Some(consts)).is_some() - } - /// The number of value references to an import binding, or `usize::MAX` /// when a value redeclaration shadows it (a reference can't be told apart). fn value_reference_count_of(id: &BindingIdentifier<'a>, semantic: &Semantic<'a>) -> usize { @@ -1978,6 +1966,27 @@ export class TestService {{ } } + #[test] + fn test_aliased_inject_counts_as_removed() { + // `@I(TOKEN)` is Angular's Inject under an alias, which compilation removes like `@Inject(TOKEN)` + let source = r#" +import { Injectable, Inject } from "@angular/core"; +import { Inject as I } from "@angular/core"; +import { TOKEN } from "./tokens"; + +@Injectable() +export class A { + constructor(@I(TOKEN) private value: unknown) {} +} + +@Injectable() +export class B { + constructor(@Inject(TOKEN) private value: unknown) {} +} +"#; + assert!(analyze_source(source).contains("TOKEN")); + } + #[test] fn test_inject_decorator_and_token_elided() { // @Inject(TOKEN) - both the decorator and the token should be elided