diff --git a/crates/oxc_angular_compiler/src/directive/mod.rs b/crates/oxc_angular_compiler/src/directive/mod.rs index 7cdb5466e..8703600ab 100644 --- a/crates/oxc_angular_compiler/src/directive/mod.rs +++ b/crates/oxc_angular_compiler/src/directive/mod.rs @@ -52,10 +52,10 @@ pub use metadata::{ R3HostMetadata, R3InputMetadata, R3QueryMetadata, }; pub(crate) use property_decorators::{ - angular_class_decorator, angular_member_decorator, angular_param_decorator, - decorator_written_name, is_angular_core_decorator, parse_decorator_queries, - try_parse_signal_input, try_parse_signal_model, try_parse_signal_output, - unwrap_initializer_api_expr, + angular_class_decorator, angular_core_identifier, angular_member_decorator, + angular_param_decorator, decorator_written_name, is_angular_core_decorator, + parse_decorator_queries, try_parse_signal_input, try_parse_signal_model, + try_parse_signal_output, unwrap_initializer_api_expr, }; pub use property_decorators::{ extract_class_queries, extract_content_queries, extract_host_bindings, extract_host_listeners, diff --git a/crates/oxc_angular_compiler/src/directive/property_decorators.rs b/crates/oxc_angular_compiler/src/directive/property_decorators.rs index e376181d6..8ad28a703 100644 --- a/crates/oxc_angular_compiler/src/directive/property_decorators.rs +++ b/crates/oxc_angular_compiler/src/directive/property_decorators.rs @@ -76,16 +76,7 @@ pub(crate) fn angular_core_decorator( expr => expr, }; let name = match callee { - Expression::Identifier(id) => match consts { - Some(consts) => { - let import = consts.scope().import(&id.name)?; - if import.module != CORE { - return None; - } - import.imported? - } - None => id.name.as_str(), - }, + Expression::Identifier(id) => return angular_core_identifier(&id.name, consts, names), Expression::StaticMemberExpression(m) => match &m.object { Expression::Identifier(ns) if is_core_namespace(consts, &ns.name) => { m.property.name.as_str() @@ -97,6 +88,27 @@ pub(crate) fn angular_core_decorator( names.iter().copied().find(|n| *n == name) } +/// Which of Angular's `names` the identifier `name` is: with the file `consts` +/// was collected from, only one imported from `@angular/core` (under any +/// alias), like ngtsc's `reflector.getImportOfIdentifier`; without it, by name. +pub(crate) fn angular_core_identifier( + name: &str, + consts: Option<&super::StringConsts<'_>>, + names: &[&'static str], +) -> Option<&'static str> { + let name = match consts { + Some(consts) => { + let import = consts.scope().import(name)?; + if import.module != CORE { + return None; + } + import.imported? + } + None => name, + }; + names.iter().copied().find(|n| *n == name) +} + /// Whether `decorator` comes from `@angular/core`: `@X`/`@X()` with `X` /// imported from it (by any alias or default), or `@ns.Y()` with /// `import * as ns from '@angular/core'` (see [`is_core_namespace`]). diff --git a/crates/oxc_angular_compiler/src/injectable/decorator.rs b/crates/oxc_angular_compiler/src/injectable/decorator.rs index d4ee39076..ced97f191 100644 --- a/crates/oxc_angular_compiler/src/injectable/decorator.rs +++ b/crates/oxc_angular_compiler/src/injectable/decorator.rs @@ -292,10 +292,10 @@ pub(crate) fn extract_injectable_metadata_in<'a>( let provided_in = extract_provided_in(allocator, config_obj, source_text); // Extract useClass - let use_class = extract_use_class(allocator, config_obj, source_text); + let use_class = extract_use_class(allocator, config_obj, source_text, consts); // Extract useFactory - let use_factory = extract_use_factory(allocator, config_obj, source_text); + let use_factory = extract_use_factory(allocator, config_obj, source_text, consts); // Extract useValue let use_value = extract_use_value(allocator, config_obj, source_text); @@ -382,6 +382,7 @@ fn extract_use_class<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, source_text: Option<&'a str>, + consts: Option<&StringConsts<'_>>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop @@ -391,7 +392,7 @@ fn extract_use_class<'a>( if key_name.as_str() == "useClass" { let (class_expr, is_forward_ref) = extract_forward_ref_or_expression(allocator, &prop.value, source_text)?; - let deps = extract_deps_from_config(allocator, config_obj, source_text); + let deps = extract_deps_from_config(allocator, config_obj, source_text, consts); return Some(UseClassMetadata { class_expr, is_forward_ref, deps }); } } @@ -404,6 +405,7 @@ fn extract_use_factory<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, source_text: Option<&'a str>, + consts: Option<&StringConsts<'_>>, ) -> Option> { for prop in &config_obj.properties { if let ObjectPropertyKind::ObjectProperty(prop) = prop @@ -412,7 +414,7 @@ fn extract_use_factory<'a>( if let Some(key_name) = get_property_key_name(&prop.key) { if key_name.as_str() == "useFactory" { let factory = convert_oxc_expression(allocator, &prop.value, source_text)?; - let deps = extract_deps_from_config(allocator, config_obj, source_text); + let deps = extract_deps_from_config(allocator, config_obj, source_text, consts); return Some(UseFactoryMetadata { factory, deps }); } } @@ -492,6 +494,7 @@ fn extract_deps_from_config<'a>( allocator: &'a Allocator, config_obj: &'a oxc_ast::ast::ObjectExpression<'a>, source_text: Option<&'a str>, + consts: Option<&StringConsts<'_>>, ) -> Vec<'a, DependencyMetadata<'a>> { let mut deps = Vec::new_in(&allocator); @@ -503,7 +506,9 @@ fn extract_deps_from_config<'a>( if key_name.as_str() == "deps" { if let Expression::ArrayExpression(arr) = &prop.value { for element in &arr.elements { - if let Some(dep) = extract_dependency(allocator, element, source_text) { + if let Some(dep) = + extract_dependency(allocator, element, source_text, consts) + { deps.push(dep); } } @@ -517,24 +522,76 @@ fn extract_deps_from_config<'a>( deps } +/// Angular's DI flags a `deps` array entry can hold, see [`extract_dependency`]. +const DEP_FLAGS: &[&str] = &["Inject", "Optional", "SkipSelf", "Self"]; + +/// One `deps` entry, like ngtsc's `getDep` +/// (`packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts`): an array +/// entry (`[new Optional(), new Self(), Dep]`) is a token plus flags, where +/// `Inject`, `Optional`, `SkipSelf` and `Self` from `@angular/core` (bare or +/// `new X()`) set the flags, `new Inject(T)` the token `T`, and any other +/// element (the last one wins) the token. fn extract_dependency<'a>( allocator: &'a Allocator, element: &'a ArrayExpressionElement<'a>, source_text: Option<&'a str>, + consts: Option<&StringConsts<'_>>, ) -> Option> { - match element { - ArrayExpressionElement::SpreadElement(_) | ArrayExpressionElement::Elision(_) => None, - _ => { - let expr = element.to_expression(); - convert_oxc_expression(allocator, expr, source_text).map(|t| DependencyMetadata { - token: t, - optional: false, - self_: false, - skip_self: false, - host: false, - }) + if matches!( + element, + ArrayExpressionElement::SpreadElement(_) | ArrayExpressionElement::Elision(_) + ) { + return None; + } + let expr = element.to_expression(); + let mut dep = DependencyMetadata { + token: convert_oxc_expression(allocator, expr, source_text)?, + optional: false, + self_: false, + skip_self: false, + host: false, + }; + let Expression::ArrayExpression(arr) = expr else { return Some(dep) }; + for el in &arr.elements { + if matches!( + el, + ArrayExpressionElement::SpreadElement(_) | ArrayExpressionElement::Elision(_) + ) { + continue; + } + let el = el.to_expression(); + let (flag, inject_token) = match el { + Expression::Identifier(id) => { + (crate::directive::angular_core_identifier(&id.name, consts, DEP_FLAGS), None) + } + Expression::NewExpression(new) => match &new.callee { + Expression::Identifier(id) => ( + crate::directive::angular_core_identifier(&id.name, consts, DEP_FLAGS), + new.arguments.first().and_then(Argument::as_expression), + ), + _ => (None, None), + }, + _ => (None, None), + }; + match flag { + Some("Inject") => { + if let Some(token) = inject_token + .and_then(|token| convert_oxc_expression(allocator, token, source_text)) + { + dep.token = token; + } + } + Some("Optional") => dep.optional = true, + Some("SkipSelf") => dep.skip_self = true, + Some("Self") => dep.self_ = true, + _ => { + if let Some(token) = convert_oxc_expression(allocator, el, source_text) { + dep.token = token; + } + } } } + Some(dep) } /// Extract constructor dependencies from a class. diff --git a/crates/oxc_angular_compiler/tests/injectable_provider_deps_test.rs b/crates/oxc_angular_compiler/tests/injectable_provider_deps_test.rs new file mode 100644 index 000000000..f9ee5ee50 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/injectable_provider_deps_test.rs @@ -0,0 +1,126 @@ +//! An `@Injectable({ useFactory | useClass, deps })` entry written as an array +//! (`[new Optional(), new Self(), Dep]`) is a token plus flags, as ngtsc's +//! `getDep` (`annotations/src/injectable.ts`) reads it: `Inject`, `Optional`, +//! `Self` and `SkipSelf` from `@angular/core`, `new X()` or bare `X`, set the +//! flags (or the token, for `new Inject(T)`); any other element is the token. + +use oxc_allocator::Allocator; +use oxc_angular_compiler::{TransformOptions, transform_angular_file}; + +/// The compiled code for `source`, without whitespace and with `'` as `"`. +fn compile(source: &str) -> String { + let allocator = Allocator::default(); + let result = transform_angular_file( + &allocator, + "test.ts", + source, + Some(&TransformOptions::default()), + None, + ); + assert!(!result.has_errors(), "Should not have errors: {:?}", result.diagnostics); + result + .code + .chars() + .filter(|c| !c.is_whitespace()) + .map(|c| if c == '\'' { '"' } else { c }) + .collect() +} + +fn assert_contains(code: &str, expected: &str) { + assert!(code.contains(expected), "Expected `{expected}` in:\n{code}"); +} + +/// ngtsc_spec.ts: "should compile Injectables with providedIn and factory with +/// deps with array literal tokens". +#[test] +fn use_factory_deps_with_array_literal_tokens() { + let code = compile( + r" +import {Injectable, Optional, Self} from '@angular/core'; + +@Injectable() +export class Dep {} + +@Injectable({ + providedIn: 'root', + useFactory: (dep: Dep) => new Service(dep), + deps: [[new Optional(), new Self(), Dep]], +}) +export class Service { + constructor(dep: Dep) {} +} +", + ); + assert_contains(&code, "returnnew(__ngFactoryType__||Service)(i0.ɵɵinject(Dep));"); + assert_contains( + &code, + "__ngConditionalFactory__=((dep)=>newService(dep))(i0.ɵɵinject(Dep,10))", + ); +} + +#[test] +fn use_class_deps_with_bare_flag_identifiers() { + let code = compile( + r" +import {Injectable, Optional, SkipSelf} from '@angular/core'; + +@Injectable() +export class Dep {} + +@Injectable() +export class Impl { + constructor(dep: Dep) {} +} + +@Injectable({ + providedIn: 'root', + useClass: Impl, + deps: [[Optional, SkipSelf, Dep]], +}) +export class Service {} +", + ); + assert_contains(&code, "__ngConditionalFactory__=newImpl(i0.ɵɵinject(Dep,12))"); +} + +#[test] +fn inject_entry_sets_the_token() { + let code = compile( + r" +import {Inject, Injectable, InjectionToken, Optional} from '@angular/core'; + +export const TOKEN = new InjectionToken('TOKEN'); + +@Injectable({ + providedIn: 'root', + useFactory: (a: string, b: string) => new Service(), + deps: [[new Inject(TOKEN), new Optional()], TOKEN], +}) +export class Service {} +", + ); + assert_contains(&code, "(i0.ɵɵinject(TOKEN,8),i0.ɵɵinject(TOKEN))"); +} + +#[test] +fn flags_not_imported_from_angular_core_are_tokens() { + let code = compile( + r" +import {Injectable} from '@angular/core'; +import {Optional} from './other'; + +@Injectable() +export class Dep {} + +@Injectable({ + providedIn: 'root', + useFactory: (dep: Dep) => new Service(dep), + deps: [[Dep, new Optional()]], +}) +export class Service { + constructor(dep: Dep) {} +} +", + ); + assert_contains(&code, "(i0.ɵɵinject(newOptional()))"); +}