Skip to content

Commit 9feb2a8

Browse files
Brooooooklynclaude
andauthored
fix(angular): emit localRefs and templateRefExtractor for conditional create instructions (#35)
Angular's conditionalCreate and conditionalBranchCreate instructions support localRefs arguments (refs index + templateRefExtractor) when local template references are present. The Rust implementation was missing this, silently dropping the local_refs_index that the local_refs phase had already computed. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ddb8c9c commit 9feb2a8

2 files changed

Lines changed: 166 additions & 4 deletions

File tree

crates/oxc_angular_compiler/src/pipeline/phases/reify/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,7 @@ fn reify_create_op<'a>(
459459
cond.vars,
460460
cond.tag.as_ref(),
461461
cond.attributes,
462+
cond.local_refs_index,
462463
))
463464
}
464465
CreateOp::RepeaterCreate(repeater) => {
@@ -719,6 +720,7 @@ fn reify_create_op<'a>(
719720
branch.vars,
720721
branch.tag.as_ref(),
721722
branch.attributes,
723+
branch.local_refs_index,
722724
))
723725
}
724726
CreateOp::ControlCreate(_) => {

crates/oxc_angular_compiler/src/pipeline/phases/reify/statements/control_flow.rs

Lines changed: 164 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ use oxc_allocator::{Box, Vec as OxcVec};
44
use oxc_span::Atom;
55

66
use crate::output::ast::{
7-
DeclareVarStmt, LiteralExpr, LiteralValue, OutputExpression, OutputStatement, ReadVarExpr,
8-
StmtModifier,
7+
DeclareVarStmt, LiteralExpr, LiteralValue, OutputExpression, OutputStatement, ReadPropExpr,
8+
ReadVarExpr, StmtModifier,
99
};
1010
use crate::r3::Identifiers;
1111

@@ -20,7 +20,7 @@ use super::super::utils::create_instruction_call_stmt;
2020
/// - vars: Number of variable slots
2121
/// - tag: Optional tag name (null for control flow blocks)
2222
/// - constIndex: Optional const array index for attributes
23-
/// - localRefs: Optional local refs index (not implemented yet)
23+
/// - localRefs: Optional local refs index (if present, also adds templateRefExtractor)
2424
///
2525
/// Ported from Angular's `conditionalCreate()` in `instruction.ts`.
2626
/// Args are trimmed from the end if they are null values.
@@ -32,6 +32,7 @@ pub fn create_conditional_create_stmt<'a>(
3232
vars: Option<u32>,
3333
tag: Option<&Atom<'a>>,
3434
attributes: Option<u32>,
35+
local_refs_index: Option<u32>,
3536
) -> OutputStatement<'a> {
3637
let mut args = OxcVec::new_in(allocator);
3738

@@ -96,6 +97,31 @@ pub fn create_conditional_create_stmt<'a>(
9697
)));
9798
}
9899

100+
// Local refs index and templateRefExtractor
101+
// Ported from Angular's instruction.ts conditionalCreate(): when localRefs !== null,
102+
// push the refs const index and i0.ɵɵtemplateRefExtractor.
103+
if let Some(refs_idx) = local_refs_index {
104+
args.push(OutputExpression::Literal(Box::new_in(
105+
LiteralExpr { value: LiteralValue::Number(refs_idx as f64), source_span: None },
106+
allocator,
107+
)));
108+
args.push(OutputExpression::ReadProp(Box::new_in(
109+
ReadPropExpr {
110+
receiver: Box::new_in(
111+
OutputExpression::ReadVar(Box::new_in(
112+
ReadVarExpr { name: Atom::from("i0"), source_span: None },
113+
allocator,
114+
)),
115+
allocator,
116+
),
117+
name: Atom::from(Identifiers::TEMPLATE_REF_EXTRACTOR),
118+
optional: false,
119+
source_span: None,
120+
},
121+
allocator,
122+
)));
123+
}
124+
99125
// Trim trailing null arguments (matching Angular's behavior)
100126
while let Some(OutputExpression::Literal(lit)) = args.last() {
101127
if matches!(lit.value, LiteralValue::Null) {
@@ -135,7 +161,7 @@ pub fn create_conditional_update_stmt<'a>(
135161
/// - vars: Number of variable slots
136162
/// - tag: Optional tag name (null for control flow blocks)
137163
/// - constIndex: Optional const array index for attributes
138-
/// - localRefs: Optional local refs index (not implemented yet)
164+
/// - localRefs: Optional local refs index (if present, also adds templateRefExtractor)
139165
///
140166
/// Ported from Angular's `conditionalBranchCreate()` in `instruction.ts`.
141167
/// Args are trimmed from the end if they are null values.
@@ -147,6 +173,7 @@ pub fn create_conditional_branch_create_stmt<'a>(
147173
vars: Option<u32>,
148174
tag: Option<&Atom<'a>>,
149175
attributes: Option<u32>,
176+
local_refs_index: Option<u32>,
150177
) -> OutputStatement<'a> {
151178
let mut args = OxcVec::new_in(allocator);
152179

@@ -211,6 +238,31 @@ pub fn create_conditional_branch_create_stmt<'a>(
211238
)));
212239
}
213240

241+
// Local refs index and templateRefExtractor
242+
// Ported from Angular's instruction.ts conditionalBranchCreate(): when localRefs !== null,
243+
// push the refs const index and i0.ɵɵtemplateRefExtractor.
244+
if let Some(refs_idx) = local_refs_index {
245+
args.push(OutputExpression::Literal(Box::new_in(
246+
LiteralExpr { value: LiteralValue::Number(refs_idx as f64), source_span: None },
247+
allocator,
248+
)));
249+
args.push(OutputExpression::ReadProp(Box::new_in(
250+
ReadPropExpr {
251+
receiver: Box::new_in(
252+
OutputExpression::ReadVar(Box::new_in(
253+
ReadVarExpr { name: Atom::from("i0"), source_span: None },
254+
allocator,
255+
)),
256+
allocator,
257+
),
258+
name: Atom::from(Identifiers::TEMPLATE_REF_EXTRACTOR),
259+
optional: false,
260+
source_span: None,
261+
},
262+
allocator,
263+
)));
264+
}
265+
214266
// Trim trailing null arguments (matching Angular's behavior)
215267
while let Some(OutputExpression::Literal(lit)) = args.last() {
216268
if matches!(lit.value, LiteralValue::Null) {
@@ -456,3 +508,111 @@ pub fn create_declare_let_stmt<'a>(
456508
// StoreLet as an update op should have been converted to a StoreLet expression
457509
// during the store_let_optimization phase. If it reaches reify, it's a compiler bug.
458510
// This matches Angular's behavior which throws: "AssertionError: unexpected storeLet"
511+
512+
#[cfg(test)]
513+
mod tests {
514+
use super::*;
515+
use crate::output::emitter::JsEmitter;
516+
517+
#[test]
518+
fn conditional_create_emits_local_refs_and_template_ref_extractor() {
519+
let allocator = oxc_allocator::Allocator::default();
520+
let emitter = JsEmitter::new();
521+
522+
// With local_refs_index = Some(3), the instruction should include the refs index
523+
// and i0.ɵɵtemplateRefExtractor, matching Angular's instruction.ts conditionalCreate().
524+
let stmt = create_conditional_create_stmt(
525+
&allocator,
526+
0,
527+
Some(Atom::from("TestComponent_Conditional_0_Template")),
528+
Some(1),
529+
Some(0),
530+
None,
531+
None,
532+
Some(3),
533+
);
534+
let js = emitter.emit_statement(&stmt);
535+
assert!(
536+
js.contains("i0.ɵɵtemplateRefExtractor"),
537+
"conditionalCreate with localRefs should emit templateRefExtractor. Got: {js}"
538+
);
539+
// When tag and constIndex are null but localRefs is set, they remain as null placeholders
540+
// because localRefs+templateRefExtractor come after them, preventing null-trimming.
541+
assert!(
542+
js.contains("ɵɵconditionalCreate(0,TestComponent_Conditional_0_Template,1,0,null,null,3,i0.ɵɵtemplateRefExtractor)"),
543+
"conditionalCreate should emit: slot, fnRef, decls, vars, tag, constIndex, refsIdx, templateRefExtractor. Got: {js}"
544+
);
545+
}
546+
547+
#[test]
548+
fn conditional_create_without_local_refs_omits_template_ref_extractor() {
549+
let allocator = oxc_allocator::Allocator::default();
550+
let emitter = JsEmitter::new();
551+
552+
// Without local refs, templateRefExtractor should not appear
553+
let stmt = create_conditional_create_stmt(
554+
&allocator,
555+
0,
556+
Some(Atom::from("TestComponent_Conditional_0_Template")),
557+
Some(1),
558+
Some(0),
559+
None,
560+
None,
561+
None,
562+
);
563+
let js = emitter.emit_statement(&stmt);
564+
assert!(
565+
!js.contains("templateRefExtractor"),
566+
"conditionalCreate without localRefs should NOT emit templateRefExtractor. Got: {js}"
567+
);
568+
}
569+
570+
#[test]
571+
fn conditional_branch_create_emits_local_refs_and_template_ref_extractor() {
572+
let allocator = oxc_allocator::Allocator::default();
573+
let emitter = JsEmitter::new();
574+
575+
let stmt = create_conditional_branch_create_stmt(
576+
&allocator,
577+
1,
578+
Some(Atom::from("TestComponent_Conditional_1_Template")),
579+
Some(1),
580+
Some(0),
581+
None,
582+
None,
583+
Some(5),
584+
);
585+
let js = emitter.emit_statement(&stmt);
586+
assert!(
587+
js.contains("i0.ɵɵtemplateRefExtractor"),
588+
"conditionalBranchCreate with localRefs should emit templateRefExtractor. Got: {js}"
589+
);
590+
// When tag and constIndex are null but localRefs is set, they remain as null placeholders.
591+
assert!(
592+
js.contains("ɵɵconditionalBranchCreate(1,TestComponent_Conditional_1_Template,1,0,null,null,\n 5,i0.ɵɵtemplateRefExtractor)"),
593+
"conditionalBranchCreate should emit: slot, fnRef, decls, vars, tag, constIndex, refsIdx, templateRefExtractor. Got: {js}"
594+
);
595+
}
596+
597+
#[test]
598+
fn conditional_branch_create_without_local_refs_omits_template_ref_extractor() {
599+
let allocator = oxc_allocator::Allocator::default();
600+
let emitter = JsEmitter::new();
601+
602+
let stmt = create_conditional_branch_create_stmt(
603+
&allocator,
604+
1,
605+
Some(Atom::from("TestComponent_Conditional_1_Template")),
606+
Some(1),
607+
Some(0),
608+
None,
609+
None,
610+
None,
611+
);
612+
let js = emitter.emit_statement(&stmt);
613+
assert!(
614+
!js.contains("templateRefExtractor"),
615+
"conditionalBranchCreate without localRefs should NOT emit templateRefExtractor. Got: {js}"
616+
);
617+
}
618+
}

0 commit comments

Comments
 (0)