diff --git a/crates/oxc_angular_compiler/src/component/import_elision.rs b/crates/oxc_angular_compiler/src/component/import_elision.rs index 3e6c1cf54..07f7e7566 100644 --- a/crates/oxc_angular_compiler/src/component/import_elision.rs +++ b/crates/oxc_angular_compiler/src/component/import_elision.rs @@ -52,12 +52,6 @@ use rustc_hash::{FxHashMap, FxHashSet}; use crate::optimizer::Edit; -/// Angular constructor parameter decorators that are removed during compilation. -/// These decorators are downleveled to factory metadata and their imports can be elided. -/// -/// Reference: packages/compiler-cli/src/ngtsc/annotations/common/src/di.ts -const PARAM_DECORATORS: &[&str] = &["Inject", "Optional", "Self", "SkipSelf", "Host", "Attribute"]; - /// Analyzer for determining which imports are type-only and can be elided. pub struct ImportElisionAnalyzer<'a> { /// Set of import specifier local names that should be removed (type-only). @@ -106,19 +100,10 @@ impl<'a> ImportElisionAnalyzer<'a> { let mut declaration_specifiers = Vec::new(); let mut declaration_of = FxHashMap::default(); - // First, collect all symbols that are used ONLY in constructor parameter decorators. - // These should be elided because Angular removes these decorators during compilation. - // When class metadata is emitted, `ctorParameters`/`propDecorators` reference the - // same names, so nothing collected here is elidable. + // The references inside decorators compilation removes: an import is decorator-only when every value + // reference to it is one of them. When class metadata is emitted, `ctorParameters`/`propDecorators` + // name the same identifiers, so none is removed. let consts = crate::directive::StringConsts::declarations_of(program); - let ctor_param_decorator_only = if emit_metadata { - FxHashSet::default() - } else { - Self::collect_ctor_param_decorator_only_imports(program, &consts) - }; - // The references inside decorators compilation removes: a name is decorator-only when every value - // reference to it is one of them, wherever else in the file the walk above didn't look (an exported const, - // a function, ...) let removed_references = if emit_metadata { FxHashSet::default() } else { @@ -167,8 +152,8 @@ impl<'a> ImportElisionAnalyzer<'a> { if Self::is_type_only_import(&spec.local, semantic) { type_only_specifiers.insert(name.clone()); } - // Check if this import is only used in constructor parameter decorators - else if ctor_param_decorator_only.contains(name.as_str()) + // Check if this import is only used in decorators compilation removes + else if !removed_references.is_empty() && Self::value_references_only_among( &spec.local, semantic, @@ -532,355 +517,6 @@ impl<'a> ImportElisionAnalyzer<'a> { } } - /// Collect import names that are used ONLY in compiler-handled positions. - /// - /// This includes: - /// 1. Constructor parameter decorators (`@Inject`, `@Optional`, etc.) - removed by Angular - /// 2. `@Inject(TOKEN)` arguments - only used in ctor param decorators - /// 3. `declare` property decorator arguments - `declare` properties are not emitted by - /// TypeScript, so their decorator arguments have no runtime value references - /// - /// Reference: packages/compiler-cli/src/ngtsc/transform/jit/src/downlevel_decorators_transform.ts - fn collect_ctor_param_decorator_only_imports( - program: &'a Program<'a>, - consts: &crate::directive::StringConsts<'a>, - ) -> FxHashSet<&'a str> { - let mut result = FxHashSet::default(); - - // Track: - // 1. Symbols used ONLY in ctor param decorator position (the decorator itself) - // 2. Symbols used ONLY as @Inject() arguments - // 3. Symbols used ONLY in `declare` property decorator arguments - let mut ctor_param_decorator_uses: FxHashSet<&'a str> = FxHashSet::default(); - let mut inject_arg_uses: FxHashSet<&'a str> = FxHashSet::default(); - let mut declare_prop_decorator_uses: FxHashSet<&'a str> = FxHashSet::default(); - let mut other_value_uses: FxHashSet<&'a str> = FxHashSet::default(); - - // Walk the AST to find constructor parameters and their decorators - for stmt in &program.body { - Self::collect_uses_from_statement( - stmt, - consts, - &mut ctor_param_decorator_uses, - &mut inject_arg_uses, - &mut declare_prop_decorator_uses, - &mut other_value_uses, - ); - } - - // A symbol can be elided if it appears ONLY in compiler-handled positions - // and NOT in other value positions - for name in ctor_param_decorator_uses { - if !other_value_uses.contains(name) { - result.insert(name); - } - } - for name in inject_arg_uses { - if !other_value_uses.contains(name) { - result.insert(name); - } - } - for name in declare_prop_decorator_uses { - if !other_value_uses.contains(name) { - result.insert(name); - } - } - - result - } - - /// Recursively collect uses from a statement. - fn collect_uses_from_statement( - stmt: &'a Statement<'a>, - consts: &crate::directive::StringConsts<'a>, - ctor_param_decorator_uses: &mut FxHashSet<&'a str>, - inject_arg_uses: &mut FxHashSet<&'a str>, - declare_prop_decorator_uses: &mut FxHashSet<&'a str>, - other_value_uses: &mut FxHashSet<&'a str>, - ) { - match stmt { - Statement::ClassDeclaration(class) => { - Self::collect_uses_from_class( - class, - consts, - ctor_param_decorator_uses, - inject_arg_uses, - declare_prop_decorator_uses, - other_value_uses, - ); - } - Statement::ExportDefaultDeclaration(export) => { - if let oxc_ast::ast::ExportDefaultDeclarationKind::ClassDeclaration(class) = - &export.declaration - { - Self::collect_uses_from_class( - class, - consts, - ctor_param_decorator_uses, - inject_arg_uses, - declare_prop_decorator_uses, - other_value_uses, - ); - } - } - Statement::ExportDeclaration(export) => { - if let oxc_ast::ast::Declaration::ClassDeclaration(class) = &export.declaration { - Self::collect_uses_from_class( - class, - consts, - ctor_param_decorator_uses, - inject_arg_uses, - declare_prop_decorator_uses, - other_value_uses, - ); - } - } - // Collect value uses from variable declarations - Statement::VariableDeclaration(var_decl) => { - for decl in &var_decl.declarations { - if let Some(init) = &decl.init { - Self::collect_value_uses_from_expr(init, other_value_uses); - } - } - } - // Collect value uses from expression statements - Statement::ExpressionStatement(expr_stmt) => { - Self::collect_value_uses_from_expr(&expr_stmt.expression, other_value_uses); - } - _ => {} - } - } - - /// Collect uses from a class declaration. - fn collect_uses_from_class( - class: &'a oxc_ast::ast::Class<'a>, - consts: &crate::directive::StringConsts<'a>, - ctor_param_decorator_uses: &mut FxHashSet<&'a str>, - inject_arg_uses: &mut FxHashSet<&'a str>, - declare_prop_decorator_uses: &mut FxHashSet<&'a str>, - other_value_uses: &mut FxHashSet<&'a str>, - ) { - // Process class decorators - these are NOT elided (they run at runtime) - for decorator in &class.decorators { - Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses); - } - - // Process class members - for element in &class.body.body { - match element { - ClassElement::MethodDefinition(method) => { - // Process method decorators (e.g., @HostListener) - NOT elided - for decorator in &method.decorators { - Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses); - } - - if method.kind == MethodDefinitionKind::Constructor { - // This is the constructor - process parameter decorators specially - Self::collect_uses_from_constructor_params( - &method.value.params, - consts, - ctor_param_decorator_uses, - inject_arg_uses, - ); - } - } - ClassElement::PropertyDefinition(prop) => { - if prop.declare { - // `declare` properties are not emitted by TypeScript, so their - // decorators and arguments have no runtime value references. - // Track them separately for elision. - for decorator in &prop.decorators { - Self::collect_value_uses_from_expr( - &decorator.expression, - declare_prop_decorator_uses, - ); - } - } else { - // Process property decorators (e.g., @Input, @ViewChild) - NOT elided - for decorator in &prop.decorators { - Self::collect_value_uses_from_expr( - &decorator.expression, - other_value_uses, - ); - } - } - // Process property initializers (e.g., doc = DOCUMENT) - if let Some(init) = &prop.value { - Self::collect_value_uses_from_expr(init, other_value_uses); - } - } - ClassElement::AccessorProperty(prop) => { - for decorator in &prop.decorators { - Self::collect_value_uses_from_expr(&decorator.expression, other_value_uses); - } - } - _ => {} - } - } - } - - /// Collect uses from constructor parameters. - /// - /// This is the key function that identifies parameter decorators and their arguments. - fn collect_uses_from_constructor_params( - params: &'a oxc_ast::ast::FormalParameters<'a>, - consts: &crate::directive::StringConsts<'a>, - ctor_param_decorator_uses: &mut FxHashSet<&'a str>, - inject_arg_uses: &mut FxHashSet<&'a str>, - ) { - for param in ¶ms.items { - for decorator in ¶m.decorators { - // Get the decorator name - let (decorator_name, call_args) = match &decorator.expression { - // @Optional (without call) - Expression::Identifier(id) => (Some(id.name.as_str()), None), - // @Optional() or @Inject(TOKEN) - Expression::CallExpression(call) => { - if let Expression::Identifier(id) = &call.callee { - (Some(id.name.as_str()), Some(&call.arguments)) - } else { - (None, None) - } - } - _ => (None, None), - }; - - if let Some(name) = decorator_name { - // Check if this is one of Angular's parameter decorators, - // which compilation removes. Another module's `@Inject` - // stays in the output (like ngtsc), so its import must too. - if PARAM_DECORATORS.contains(&name) - && crate::directive::angular_param_decorator(decorator, Some(consts)) - .is_some() - { - ctor_param_decorator_uses.insert(name); - - // If this is @Inject(TOKEN), collect the TOKEN argument - if name == "Inject" { - if let Some(args) = call_args { - for arg in args { - // Handle spread elements by extracting their inner expression - if let oxc_ast::ast::Argument::SpreadElement(spread) = arg { - Self::collect_inject_arg_uses( - &spread.argument, - inject_arg_uses, - ); - } else if let Some(expr) = arg.as_expression() { - Self::collect_inject_arg_uses(expr, inject_arg_uses); - } - } - } - } - } - } - } - } - } - - /// Collect identifier uses from an @Inject() argument. - fn collect_inject_arg_uses(expr: &'a Expression<'a>, inject_arg_uses: &mut FxHashSet<&'a str>) { - match expr { - Expression::Identifier(id) => { - inject_arg_uses.insert(id.name.as_str()); - } - // Handle property access like SomeModule.TOKEN - Expression::StaticMemberExpression(member) => { - if let Expression::Identifier(id) = &member.object { - inject_arg_uses.insert(id.name.as_str()); - } - } - _ => {} - } - } - - /// Collect all identifier uses from an expression (for "other" non-elided uses). - fn collect_value_uses_from_expr( - expr: &'a Expression<'a>, - other_value_uses: &mut FxHashSet<&'a str>, - ) { - match expr { - Expression::Identifier(id) => { - other_value_uses.insert(id.name.as_str()); - } - Expression::CallExpression(call) => { - Self::collect_value_uses_from_expr(&call.callee, other_value_uses); - for arg in &call.arguments { - // Handle spread elements by extracting their inner expression - if let oxc_ast::ast::Argument::SpreadElement(spread) = arg { - Self::collect_value_uses_from_expr(&spread.argument, other_value_uses); - } else if let Some(expr) = arg.as_expression() { - Self::collect_value_uses_from_expr(expr, other_value_uses); - } - } - } - Expression::StaticMemberExpression(member) => { - Self::collect_value_uses_from_expr(&member.object, other_value_uses); - } - Expression::ComputedMemberExpression(member) => { - Self::collect_value_uses_from_expr(&member.object, other_value_uses); - Self::collect_value_uses_from_expr(&member.expression, other_value_uses); - } - Expression::ObjectExpression(obj) => { - for prop in &obj.properties { - if let oxc_ast::ast::ObjectPropertyKind::ObjectProperty(prop) = prop { - Self::collect_value_uses_from_expr(&prop.value, other_value_uses); - } - } - } - Expression::ArrayExpression(arr) => { - for elem in &arr.elements { - match elem { - oxc_ast::ast::ArrayExpressionElement::SpreadElement(spread) => { - Self::collect_value_uses_from_expr(&spread.argument, other_value_uses); - } - oxc_ast::ast::ArrayExpressionElement::Elision(_) => {} - _ => { - if let Some(expr) = elem.as_expression() { - Self::collect_value_uses_from_expr(expr, other_value_uses); - } - } - } - } - } - Expression::NewExpression(new_expr) => { - Self::collect_value_uses_from_expr(&new_expr.callee, other_value_uses); - for arg in &new_expr.arguments { - // Handle spread elements by extracting their inner expression - if let oxc_ast::ast::Argument::SpreadElement(spread) = arg { - Self::collect_value_uses_from_expr(&spread.argument, other_value_uses); - } else if let Some(expr) = arg.as_expression() { - Self::collect_value_uses_from_expr(expr, other_value_uses); - } - } - } - Expression::ArrowFunctionExpression(arrow) => { - // Arrow function bodies may contain value references, e.g., - // `forwardRef(() => TagPickerComponent)` in Component imports. - if let Some(expr) = arrow.get_expression() { - Self::collect_value_uses_from_expr(expr, other_value_uses); - } else if let Some(body) = arrow.get_function_body() { - for stmt in &body.statements { - match stmt { - Statement::ExpressionStatement(expr_stmt) => { - Self::collect_value_uses_from_expr( - &expr_stmt.expression, - other_value_uses, - ); - } - Statement::ReturnStatement(ret) => { - if let Some(arg) = &ret.argument { - Self::collect_value_uses_from_expr(arg, other_value_uses); - } - } - _ => {} - } - } - } - } - _ => {} - } - } - /// Check if an import binding is only used in type positions. fn is_type_only_import(id: &BindingIdentifier<'a>, semantic: &Semantic<'a>) -> bool { let symbol_id = id.symbol_id(); @@ -914,9 +550,20 @@ impl<'a> ImportElisionAnalyzer<'a> { }) } - /// The references inside the decorators compilation removes: Angular's constructor parameter decorators of the - /// classes [`Self::collect_ctor_param_decorator_only_imports`] looks at, and the decorators of `declare` - /// properties + /// The references the compiled output no longer has, because the transform strips the decorator that held + /// them and compiles nothing that names them. Only on a class the transform compiles (one with an Angular + /// class decorator, see [`crate::directive::angular_class_decorator`]): + /// + /// - an Angular constructor parameter decorator of the first constructor (the one + /// `collect_constructor_decorator_spans` strips and the factory reads), with the argument of `@Inject(X)` + /// when it is a bare identifier: the factory names `X` through a namespace import (`i1.X`), while any other + /// argument (`TOKEN.sub`, `forwardRef(() => TOKEN)`) is copied as written + /// - the Angular member decorator itself on a `declare` property, not its arguments: a query predicate + /// (`viewQuery(Foo)`) or an input `transform` is compiled into the definition by its local name + /// + /// A decorator that stays in the output (not Angular's, or on a class that isn't compiled) keeps all of its + /// references. `transform` also keeps the imports of decorators it didn't strip after all (a component whose + /// template is missing). fn removed_decorator_references( program: &'a Program<'a>, consts: &crate::directive::StringConsts<'a>, @@ -930,6 +577,15 @@ impl<'a> ImportElisionAnalyzer<'a> { } } } + impl References { + /// The decorator's callee: `Inject` in `@Inject(TOKEN)`, `core` in `@core.Optional()` + fn visit_callee<'a>(&mut self, decorator: &oxc_ast::ast::Decorator<'a>) { + match &decorator.expression { + Expression::CallExpression(call) => self.visit_expression(&call.callee), + expression => self.visit_expression(expression), + } + } + } let mut references = References::default(); for stmt in &program.body { let class = match stmt { @@ -944,31 +600,48 @@ impl<'a> ImportElisionAnalyzer<'a> { }, _ => continue, }; - for element in &class.body.body { - match element { - ClassElement::MethodDefinition(method) - if method.kind == MethodDefinitionKind::Constructor => + if !class.decorators.iter().any(|decorator| { + crate::directive::angular_class_decorator(decorator, consts).is_some() + }) { + continue; + } + let constructor = class.body.body.iter().find_map(|element| match element { + ClassElement::MethodDefinition(method) + if method.kind == MethodDefinitionKind::Constructor => + { + Some(method) + } + _ => None, + }); + for param in constructor.into_iter().flat_map(|ctor| &ctor.value.params.items) { + for decorator in ¶m.decorators { + // by what it resolves to: `@I(TOKEN)` for `import { Inject as I }` is removed too + let Some(name) = + crate::directive::angular_param_decorator(decorator, Some(consts)) + else { + continue; + }; + references.visit_callee(decorator); + if name == "Inject" + && let Expression::CallExpression(call) = &decorator.expression + && let Some(oxc_ast::ast::Argument::Identifier(token)) = + call.arguments.first() { - for param in &method.value.params.items { - for decorator in ¶m.decorators { - // 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); - } - } - } + references.visit_identifier_reference(token); } - ClassElement::PropertyDefinition(prop) if prop.declare => { - for decorator in &prop.decorators { - references.visit_decorator(decorator); + } + } + for element in &class.body.body { + if let ClassElement::PropertyDefinition(prop) = element + && prop.declare + { + for decorator in &prop.decorators { + if crate::directive::angular_member_decorator(decorator, Some(consts)) + .is_some() + { + references.visit_callee(decorator); } } - _ => {} } } } @@ -2290,10 +1963,10 @@ type AddEditFieldForm = FormGroup<{ name: FormControl }>; } #[test] - fn test_declare_property_viewchild_arg_elided() { + fn test_declare_property_viewchild_arg_kept() { // Case 2: `@ViewChild(GridstackComponent) declare gridstack: GridstackComponent;` - // When a property is `declare`, TypeScript does not emit it, so the decorator - // and its arguments have no runtime value references. Angular strips it. + // The decorator is stripped, but the query is compiled into `ɵɵviewQuery(GridstackComponent, 5)`, which + // names the predicate by its local name (ngtsc keeps the import too). let source = r#" import { Component, ViewChild } from '@angular/core'; import { GridstackComponent, GridstackModule } from 'gridstack/dist/angular'; @@ -2306,14 +1979,12 @@ class MyComponent { "#; let type_only = analyze_source(source); - // GridstackComponent is only used on a `declare` property — must be elided assert!( - type_only.contains("GridstackComponent"), - "GridstackComponent on declare property should be elided" + !type_only.contains("GridstackComponent"), + "GridstackComponent is the query predicate the definition names" ); - // ViewChild is only used as decorator on a `declare` property — must also be elided. - // TypeScript does not emit `declare` properties, so the decorator has no runtime effect. + // ViewChild is only used as decorator on a `declare` property, which compilation strips assert!( type_only.contains("ViewChild"), "ViewChild decorator on declare property should be elided" diff --git a/crates/oxc_angular_compiler/src/component/transform.rs b/crates/oxc_angular_compiler/src/component/transform.rs index 9bca18f5e..4ad59ba8d 100644 --- a/crates/oxc_angular_compiler/src/component/transform.rs +++ b/crates/oxc_angular_compiler/src/component/transform.rs @@ -4549,6 +4549,71 @@ pub fn transform_angular_file( } } + // The analyzer counts an Angular constructor parameter decorator, and the Angular decorator of a `declare` + // property, as removed on every class with an Angular class decorator. One the walk above left in place (a + // component whose template is missing, a constructor after the first) keeps what it references. + { + let stripped: rustc_hash::FxHashSet = + decorator_spans_to_remove.iter().copied().collect(); + let mut kept = DecoratorIdentifiers::default(); + for stmt in &parser_ret.program.body { + let class = match stmt { + Statement::ClassDeclaration(class) => class.as_ref(), + Statement::ExportDefaultDeclaration(export) => match &export.declaration { + ExportDefaultDeclarationKind::ClassDeclaration(class) => class.as_ref(), + _ => continue, + }, + Statement::ExportDeclaration(export) => match &export.declaration { + Declaration::ClassDeclaration(class) => class.as_ref(), + _ => continue, + }, + _ => continue, + }; + for element in &class.body.body { + let decorators: std::vec::Vec<_> = match element { + oxc_ast::ast::ClassElement::MethodDefinition(method) + if method.kind == oxc_ast::ast::MethodDefinitionKind::Constructor => + { + method + .value + .params + .items + .iter() + .flat_map(|param| ¶m.decorators) + .filter(|decorator| { + crate::directive::angular_param_decorator( + decorator, + Some(&string_consts), + ) + .is_some() + }) + .collect() + } + oxc_ast::ast::ClassElement::PropertyDefinition(prop) if prop.declare => prop + .decorators + .iter() + .filter(|decorator| { + crate::directive::angular_member_decorator( + decorator, + Some(&string_consts), + ) + .is_some() + }) + .collect(), + _ => continue, + }; + for decorator in decorators { + if !stripped.contains(&decorator.span) { + oxc_ast_visit::Visit::visit_decorator(&mut kept, decorator); + } + } + } + } + for name in kept.0 { + import_elision_mut.preserve(name); + } + } + // 5. Generate output code using span-based edits from the original AST. // All edits reference positions in the original source and are applied in one pass. diff --git a/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs b/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs new file mode 100644 index 000000000..ac8ae8955 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/import_elision_output_references_test.rs @@ -0,0 +1,281 @@ +//! Without class metadata (`advanced_optimizations`), import elision drops an +//! import used only in decorators compilation removes. It must keep every +//! import the output still references: decorators the transform leaves in +//! place, and decorator arguments it copies into the factory or definition by +//! their local name. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +fn compile(source: &str) -> String { + compile_checked(source, true) +} + +fn compile_checked(source: &str, no_errors: bool) -> String { + let allocator = Allocator::default(); + let options = TransformOptions { + advanced_optimizations: true, + emit_class_metadata: false, + ..TransformOptions::default() + }; + let result = transform_angular_file(&allocator, "test.ts", source, Some(&options), None); + assert!(!no_errors || !result.has_errors(), "unexpected errors: {:?}", result.diagnostics); + result.code +} + +/// Whether a named import binds `name` in `code` (namespace imports aside). +fn imports(code: &str, name: &str) -> bool { + code.lines().filter(|l| l.starts_with("import") && !l.contains("* as")).any(|l| { + l.split(|c: char| !(c.is_alphanumeric() || c == '_' || c == '$')).any(|w| w == name) + }) +} + +/// Whether `code` names `name` outside its import lines (not as a member, like `i1.name`). +fn uses(code: &str, name: &str) -> bool { + code.lines().filter(|l| !l.starts_with("import")).any(|line| { + line.match_indices(name).any(|(at, _)| { + let before = line[..at].chars().next_back(); + let after = line[at + name.len()..].chars().next(); + let ident = |c: char| c.is_alphanumeric() || c == '_' || c == '$'; + !before.is_some_and(|c| ident(c) || c == '.') && !after.is_some_and(ident) + }) + }) +} + +/// Each name stays imported, and the output still names it: no import is kept for nothing. +fn assert_kept(source: &str, names: &[&str]) { + let code = compile(source); + for name in names { + assert!(imports(&code, name), "`{name}` must stay imported:\n{code}"); + assert!(uses(&code, name), "`{name}` is kept but the output doesn't name it:\n{code}"); + } +} + +#[test] +fn plain_inject_token_and_decorator_are_still_elided() { + let code = compile( + r#" +import { Injectable, Inject, Optional } from "@angular/core"; +import { TOKEN } from "./tokens"; +@Injectable() +export class B { constructor(@Optional() @Inject(TOKEN) v: unknown) {} } +"#, + ); + assert!(!imports(&code, "TOKEN"), "{code}"); + assert!(!imports(&code, "Inject"), "{code}"); + assert!(!imports(&code, "Optional"), "{code}"); + assert!(code.contains("i1.TOKEN"), "{code}"); +} + +#[test] +fn inject_forward_ref_keeps_token() { + // The factory copies `forwardRef(() => TOKEN)` as written. + assert_kept( + r#" +import { Injectable, Inject, forwardRef } from "@angular/core"; +import { TOKEN } from "./tokens"; +@Injectable() +export class A { constructor(@Inject(forwardRef(() => TOKEN)) v: unknown) {} } +@Injectable() +export class B { constructor(@Inject(TOKEN) v: unknown) {} } +"#, + &["TOKEN", "forwardRef"], + ); +} + +#[test] +fn inject_member_expression_keeps_object() { + // The factory emits `ɵɵinject(TOKEN.sub)` with the local name. + assert_kept( + r#" +import { Injectable, Inject } from "@angular/core"; +import { TOKEN } from "./tokens"; +@Injectable() export class S { constructor(@Inject(TOKEN.sub) private v: unknown) {} } +"#, + &["TOKEN"], + ); +} + +#[test] +fn non_angular_decorator_on_declare_field_keeps_its_references() { + assert_kept( + r#" +import { Injectable, Inject } from "@angular/core"; +import { TOKEN } from "./tokens"; +@Injectable() +export class A { constructor(@Inject(TOKEN) v: unknown) {} } +export class O { @Dec(TOKEN) declare p: string; } +function Dec(..._a: any[]): any { return () => {}; } +"#, + &["TOKEN"], + ); + assert_kept( + r#" +import { Injectable } from "@angular/core"; +import { TOKEN } from "./tokens"; +@Injectable() export class A { @Dec(TOKEN) declare p: string; } +function Dec(..._a: any[]): any { return () => {}; } +"#, + &["TOKEN"], + ); +} + +#[test] +fn param_decorators_of_a_class_that_is_not_compiled_stay() { + for class in [ + "export class P { constructor(@Inject(TOKEN) private v: unknown) {} }", + "@Foo() export class P { constructor(@Inject(TOKEN) private v: unknown) {} }", + ] { + assert_kept( + &format!( + r#" +import {{ Injectable, Inject }} from "@angular/core"; +import {{ TOKEN }} from "./tokens"; +declare function Foo(): any; +@Injectable() export class S {{ constructor(@Inject(TOKEN) private v: unknown) {{}} }} +{class} +"# + ), + &["Inject", "TOKEN"], + ); + } +} + +#[test] +fn param_decorators_of_a_component_whose_template_is_unresolved_stay() { + // The template is not resolved, so the component is emitted as written, without its definition (an error + // is reported too). + let code = compile_checked( + r#" +import { Component, Inject, LOCALE_ID } from "@angular/core"; +@Component({ selector: "app-root", templateUrl: "./app.component.html" }) +export class AppComponent { constructor(@Inject(LOCALE_ID) public locale: string) {} } +"#, + false, + ); + assert!(code.contains("@Inject(LOCALE_ID)"), "{code}"); + for name in ["Inject", "LOCALE_ID"] { + assert!(imports(&code, name), "`{name}` must stay imported:\n{code}"); + } +} + +#[test] +fn ctor_overload_keeps_implementation_param_decorators() { + // Angular compliance case r3_view_compiler_di/di/ctor_overload.ts + assert_kept( + r#" +import {Injectable, Optional} from '@angular/core'; +class MyDependency {} +class MyOptionalDependency {} +@Injectable() +export class MyService { + constructor(dep: MyDependency); + constructor(dep: MyDependency, @Optional() optionalDep?: MyOptionalDependency) {} +} +"#, + &["Optional"], + ); +} + +#[test] +fn decorators_on_declare_fields_that_stay_keep_their_imports() { + // A non-Angular decorator on a compiled class. + assert_kept( + r#" +import { Component } from "@angular/core"; +import { TOKEN } from "./tokens"; +declare function Foo(x: any): any; +@Component({selector:"a",template:""}) export class C { @Foo(TOKEN) declare x: string; } +"#, + &["TOKEN"], + ); + // An Angular decorator on a class that is not compiled. + assert_kept( + r#" +import { Inject } from "@angular/core"; +import { TOKEN } from "./tokens"; +export class Q { @Inject(TOKEN) declare z: string; } +"#, + &["Inject", "TOKEN"], + ); +} + +#[test] +fn declare_query_predicate_keeps_import() { + // Compiled into `ɵɵviewQuery(Foo, 5)`. + let source = r#" +import { Component, ViewChild } from "@angular/core"; +import { Foo } from "./foo"; +@Component({ selector: "x", template: "" }) +export class C { + @ViewChild(Foo, { static: true }) declare q: Foo; +} +"#; + assert_kept(source, &["Foo"]); + let code = compile(source); + assert!(code.contains("viewQuery(Foo"), "{code}"); + // The decorator itself is gone. + assert!(!imports(&code, "ViewChild"), "{code}"); +} + +#[test] +fn abstract_base_param_decorators_stay() { + assert_kept( + r#" +import { Injectable, Inject } from "@angular/core"; +import { TOKEN } from "./tokens"; +export abstract class Base { + constructor(@Inject(TOKEN) protected value: unknown) {} +} +@Injectable() +export class TestService { + constructor(@Inject(TOKEN) private value: unknown) {} +} +"#, + &["Inject", "TOKEN"], + ); +} + +#[test] +fn declare_input_transform_keeps_import() { + // Compiled into `inputs: {disabled: [2, "disabled", "disabled", booleanAttribute]}`. + assert_kept( + r#" +import { Component, Input, booleanAttribute } from "@angular/core"; +@Component({selector:'x', template:''}) +export class A { @Input({ transform: booleanAttribute }) declare disabled: boolean; } +"#, + &["booleanAttribute"], + ); + assert_kept( + r#" +import { Component, Input } from "@angular/core"; +import { toNum } from "./t"; +@Component({selector:'x', template:''}) +export class A { @Input({ transform: toNum }) declare v: number; } +"#, + &["toNum"], + ); +} + +#[test] +fn declare_query_read_option_keeps_import() { + // `read` is compiled into the query call by its local name, like the predicate. + assert_kept( + r#" +import { Component, ContentChildren, QueryList } from "@angular/core"; +import { Foo, Bar } from "./foo"; +@Component({ selector: "x", template: "" }) +export class C { @ContentChildren(Foo, { read: Bar }) declare items: QueryList; } +"#, + &["Foo", "Bar"], + ); + assert_kept( + r#" +import { Component, ViewChild, ViewContainerRef } from "@angular/core"; +@Component({ selector: "x", template: "" }) +export class C { @ViewChild("ref", { read: ViewContainerRef }) declare ref: ViewContainerRef; } +"#, + &["ViewContainerRef"], + ); +}