Skip to content
Merged
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
16 changes: 4 additions & 12 deletions crates/oxc_angular_compiler/src/partial/injectable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,18 +184,10 @@ fn clone_constructor_deps<'a>(
deps: Option<&[R3DependencyMetadata<'a>]>,
) -> R3FactoryDeps<'a> {
match deps {
// No constructor in source. Match upstream's behavior for
// non-inheriting parameterless classes (`deps: []` → no-arg
// `new Class()` factory). This is the common case.
//
// For a service that extends another class (uncommon enough to
// ignore), the optimal emit would be `R3FactoryDeps::None` so
// the linker uses `ɵɵgetInheritedFactory`. R3InjectableMetadata
// doesn't carry an inheritance flag, so we default to the
// safe-and-correct empty form. Inheriting services still get a
// working factory (plain `new`), just without the
// inherited-factory optimization.
None => R3FactoryDeps::Valid(Vec::new_in(&allocator)),
// No constructor, but a superclass: `deps: null`, so the linker
// uses `ɵɵgetInheritedFactory` (upstream `compileDependencies` in
// `packages/compiler/src/render3/partial/util.ts`).
None => R3FactoryDeps::None,
Some(deps) => {
let mut out = Vec::with_capacity_in(deps.len(), &allocator);
for dep in deps {
Expand Down
15 changes: 6 additions & 9 deletions crates/oxc_angular_compiler/src/partial/ng_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,11 @@ pub fn compile_declare_ng_module_from_metadata<'a>(

/// Builds the partial ɵfac factory paired with this NgModule.
///
/// `deps` is the module's extracted constructor dependencies (`None` when it
/// has no constructor). A class without a constructor gets `deps: []` to
/// match upstream's golden behavior (see compiler-cli/test/compliance/
/// test_cases/r3_view_compiler/hello_world/GOLDEN_PARTIAL.js — every ɵfac
/// for an NgModule carries `deps: []`). The linker then generates the
/// simple `new MyModule()` factory. OXC doesn't track base classes, so the
/// no-ctor `deps: null` upstream uses for the inherited-factory pattern
/// becomes `[]` here — a suboptimal-but-correct factory.
/// `deps` is the module's extracted constructor dependencies: empty for a
/// class with neither a constructor nor a superclass (`deps: []`, a plain
/// `new MyModule()` factory), and `None` for a class with no constructor
/// that extends another class (`deps: null`, so the linker uses
/// `ɵɵgetInheritedFactory`).
pub fn compile_declare_factory_for_ng_module<'a>(
allocator: &'a Allocator,
core_namespace: &'a str,
Expand All @@ -119,7 +116,7 @@ pub fn compile_declare_factory_for_ng_module<'a>(
}
R3FactoryDeps::Valid(factory_deps)
}
None => R3FactoryDeps::Valid(Vec::new_in(&allocator)),
None => R3FactoryDeps::None,
};
let factory_meta = R3FactoryMetadata::Constructor(R3ConstructorFactoryMetadata {
name: Ident::from("NgModuleFactory"),
Expand Down
20 changes: 5 additions & 15 deletions crates/oxc_angular_compiler/src/partial/pipe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,17 +139,10 @@ fn clone_constructor_deps<'a>(
deps: Option<&Vec<'a, crate::pipe::R3DependencyMetadata<'a>>>,
) -> R3FactoryDeps<'a> {
match deps {
// No constructor in source. Match upstream's behavior for
// non-inheriting parameterless classes (`deps: []` → no-arg
// `new Class()` factory). This is the common case.
//
// For a Pipe that DOES extend another class (rare), the optimal
// emit would be `R3FactoryDeps::None` so the linker calls
// `ɵɵgetInheritedFactory`. OXC's Pipe metadata doesn't track
// inheritance today, so we default to the safe-and-correct empty
// form. An inheriting pipe still gets a working factory (plain
// `new`), just without the inherited-factory optimization.
None => R3FactoryDeps::Valid(Vec::new_in(&allocator)),
// No constructor, but a superclass: `deps: null`, so the linker
// uses `ɵɵgetInheritedFactory` (upstream `compileDependencies` in
// `packages/compiler/src/render3/partial/util.ts`).
None => R3FactoryDeps::None,
Some(deps) => {
let mut out = Vec::with_capacity_in(deps.len(), &allocator);
for dep in deps {
Expand All @@ -165,10 +158,7 @@ fn clone_constructor_deps<'a>(
optional: dep.optional,
self_: dep.self_,
skip_self: dep.skip_self,
// Pipe's R3DependencyMetadata predates the type-only-invalid
// tracking (#288); the field doesn't exist on this struct,
// so we default to false.
type_only_invalid: false,
type_only_invalid: dep.type_only_invalid,
});
}
R3FactoryDeps::Valid(out)
Expand Down
29 changes: 27 additions & 2 deletions crates/oxc_angular_compiler/src/pipe/decorator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use oxc_ast::ast::{Argument, Class, Decorator, Expression, ObjectPropertyKind, P
use oxc_span::Span;
use oxc_str::Ident;

use super::metadata::R3PipeMetadata;
use super::metadata::{R3DependencyMetadata as PipeDependencyMetadata, R3PipeMetadata};
use crate::directive::StringConsts;
use crate::factory::R3DependencyMetadata;
use crate::output::ast::{OutputExpression, ReadVarExpr};
Expand Down Expand Up @@ -71,12 +71,37 @@ impl<'a> PipeMetadata<'a> {
&allocator,
));

// Constructor dependencies feed the pipe's factory. `None` (no
// constructor, but a superclass) stays `None` so the factory is
// inherited, like ngtsc's `ctorDeps` in `PipeDecoratorHandler.analyze`.
let deps = self.deps.as_ref().map(|deps| {
let mut out = Vec::with_capacity_in(deps.len(), &allocator);
for dep in deps {
out.push(PipeDependencyMetadata {
token: dep
.token
.as_ref()
.map(|t| Box::new_in(t.clone_in(allocator), &allocator)),
attribute_name_type: dep
.attribute_name_type
.as_ref()
.map(|a| Box::new_in(a.clone_in(allocator), &allocator)),
host: dep.host,
optional: dep.optional,
self_: dep.self_,
skip_self: dep.skip_self,
type_only_invalid: dep.type_only_invalid,
});
}
out
});

Some(R3PipeMetadata {
name: self.class_name.clone(),
pipe_name: self.pipe_name.clone(),
r#type: type_expr,
type_argument_count: 0,
deps: None,
deps,
pure: self.pure,
is_standalone: self.standalone,
})
Expand Down
10 changes: 9 additions & 1 deletion crates/oxc_angular_compiler/src/pipe/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ pub struct R3PipeMetadata<'a> {
pub type_argument_count: u32,

/// Constructor dependencies for dependency injection.
/// None means use default factory (no-arg constructor).
/// `None` means the class has no constructor but extends another class,
/// so its factory is inherited (`ɵɵgetInheritedFactory`).
pub deps: Option<Vec<'a, R3DependencyMetadata<'a>>>,

/// Whether the pipe is pure (default: true).
Expand Down Expand Up @@ -61,6 +62,11 @@ pub struct R3DependencyMetadata<'a> {

/// Whether `@SkipSelf()` decorator is present.
pub skip_self: bool,

/// Whether the token came from a type-only import, which is erased at
/// runtime. Any such dependency makes the factory `ɵɵinvalidFactory()`.
/// See issue #288.
pub type_only_invalid: bool,
}

impl<'a> R3DependencyMetadata<'a> {
Expand All @@ -73,6 +79,7 @@ impl<'a> R3DependencyMetadata<'a> {
optional: false,
self_: false,
skip_self: false,
type_only_invalid: false,
}
}

Expand All @@ -85,6 +92,7 @@ impl<'a> R3DependencyMetadata<'a> {
optional: false,
self_: false,
skip_self: false,
type_only_invalid: false,
}
}
}
Expand Down
121 changes: 121 additions & 0 deletions crates/oxc_angular_compiler/tests/partial_e2e_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,124 @@ export class AbstractService {}
result.code
);
}

/// Transforms `source` in partial mode, then links the result. Returns the
/// partial output and the linked output, both with whitespace removed.
fn partial_and_linked(source: &str) -> (String, String) {
let allocator = Allocator::default();
let options =
TransformOptions { compilation_mode: CompilationMode::Partial, ..Default::default() };
let result = transform_angular_file(&allocator, "case.ts", source, Some(&options), None);
assert!(!result.has_errors(), "should not have errors, got: {:?}", result.diagnostics);
let linked = link(&allocator, &result.code, "case.ts");
assert!(linked.linked, "linker should accept the partial output:\n{}", result.code);
let strip = |s: &str| s.chars().filter(|c| !c.is_whitespace()).collect::<String>();
(strip(&result.code), strip(&linked.code))
}

#[test]
fn partial_pipe_factory_keeps_constructor_deps() {
// ngtsc: ɵɵngDeclareFactory({..., type: P, deps: [{ token: TOK },
// { token: Ok, optional: true }], target: i0.ɵɵFactoryTarget.Pipe })
let (partial, linked) = partial_and_linked(
"import { Pipe, Inject, Optional, InjectionToken } from '@angular/core';
const TOK = new InjectionToken<string>('t');
class Ok {}
@Pipe({ name: 'p' })
export class P {
constructor(@Inject(TOK) t: string, @Optional() o: Ok) {}
transform(v: any) { return v; }
}
",
);
assert!(
partial.contains("type:P,deps:[{token:TOK")
&& partial.contains("},{token:Ok,optional:true}],target:i0.ɵɵFactoryTarget.Pipe"),
"pipe factory should keep its constructor deps, got:\n{partial}"
);
assert!(
linked.contains(
"new(__ngFactoryType__||P)(i0.ɵɵdirectiveInject(TOK,16),i0.ɵɵdirectiveInject(Ok,24))"
),
"linked pipe factory should inject TOK and Ok, got:\n{linked}"
);
}

#[test]
fn partial_pipe_factory_without_constructor() {
// ngtsc: a pipe with no constructor and no superclass gets `deps: []`;
// one that extends a base class gets `deps: null` (inherited factory).
let (partial, _) = partial_and_linked(
"import { Pipe } from '@angular/core';
@Pipe({ name: 'p' })
export class P { transform(v: any) { return v; } }
",
);
assert!(
partial.contains("type:P,deps:[],target:i0.ɵɵFactoryTarget.Pipe"),
"pipe without a constructor should get deps:[], got:\n{partial}"
);

let (partial, linked) = partial_and_linked(
"import { Pipe } from '@angular/core';
class Ok {}
class Base { constructor(o: Ok) {} }
@Pipe({ name: 'p' })
export class P extends Base { transform(v: any) { return v; } }
",
);
assert!(
partial.contains("type:P,deps:null,target:i0.ɵɵFactoryTarget.Pipe"),
"pipe inheriting its constructor should get deps:null, got:\n{partial}"
);
assert!(
linked.contains("i0.ɵɵgetInheritedFactory(P)"),
"linked pipe factory should be inherited, got:\n{linked}"
);
}

#[test]
fn partial_injectable_factory_inherits_constructor() {
// ngtsc: ɵɵngDeclareFactory({..., type: S, deps: null,
// target: i0.ɵɵFactoryTarget.Injectable })
let (partial, linked) = partial_and_linked(
"import { Injectable } from '@angular/core';
class Ok {}
@Injectable() export class B { constructor(o: Ok) {} }
@Injectable() export class S extends B {}
",
);
assert!(
partial.contains("type:S,deps:null,target:i0.ɵɵFactoryTarget.Injectable"),
"injectable inheriting its constructor should get deps:null, got:\n{partial}"
);
assert!(
linked.contains("new(__ngFactoryType__||B)(i0.ɵɵinject(Ok))"),
"linked base injectable factory should inject Ok, got:\n{linked}"
);
assert!(
linked.contains("i0.ɵɵgetInheritedFactory(S)"),
"linked injectable factory should be inherited, got:\n{linked}"
);
}

#[test]
fn partial_ng_module_factory_inherits_constructor() {
// ngtsc: ɵɵngDeclareFactory({..., type: M, deps: null,
// target: i0.ɵɵFactoryTarget.NgModule })
let (partial, linked) = partial_and_linked(
"import { NgModule } from '@angular/core';
class Ok {}
export class B { constructor(o: Ok) {} }
@NgModule({}) export class M extends B {}
",
);
assert!(
partial.contains("type:M,deps:null,target:i0.ɵɵFactoryTarget.NgModule"),
"NgModule inheriting its constructor should get deps:null, got:\n{partial}"
);
assert!(
linked.contains("i0.ɵɵgetInheritedFactory(M)"),
"linked NgModule factory should be inherited, got:\n{linked}"
);
}
Loading