Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/oxc_angular_compiler/src/directive/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
32 changes: 22 additions & 10 deletions crates/oxc_angular_compiler/src/directive/property_decorators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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`]).
Expand Down
89 changes: 73 additions & 16 deletions crates/oxc_angular_compiler/src/injectable/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<UseClassMetadata<'a>> {
for prop in &config_obj.properties {
if let ObjectPropertyKind::ObjectProperty(prop) = prop
Expand All @@ -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 });
}
}
Expand All @@ -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<UseFactoryMetadata<'a>> {
for prop in &config_obj.properties {
if let ObjectPropertyKind::ObjectProperty(prop) = prop
Expand All @@ -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 });
}
}
Expand Down Expand Up @@ -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);

Expand All @@ -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);
}
}
Expand All @@ -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<DependencyMetadata<'a>> {
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.
Expand Down
126 changes: 126 additions & 0 deletions crates/oxc_angular_compiler/tests/injectable_provider_deps_test.rs
Original file line number Diff line number Diff line change
@@ -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<string>('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()))");
}