Skip to content

Commit cf75ff7

Browse files
authored
fix: check compound expressions in needs_temporary_in_safe_access for safe navigation (#14)
1 parent 5fce15e commit cf75ff7

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,10 @@ pub fn expand_safe_reads(job: &mut ComponentCompilationJob<'_>) {
127127

128128
/// Checks if an IR expression requires a temporary variable to avoid re-evaluation.
129129
///
130-
/// Returns true for expressions with side effects:
131-
/// - Function calls/invocations
132-
/// - Pipe bindings
133-
/// - Safe function invocations
130+
/// Returns true for expressions with side effects (function calls, pipe bindings),
131+
/// or for compound expressions that may contain such sub-expressions.
132+
///
133+
/// Ported from Angular's `needsTemporaryInSafeAccess` in `expand_safe_reads.ts` lines 43-73.
134134
fn needs_temporary_in_safe_access(expr: &IrExpression<'_>) -> bool {
135135
match expr {
136136
// Function calls always need temporaries (they may have side effects)
@@ -140,6 +140,26 @@ fn needs_temporary_in_safe_access(expr: &IrExpression<'_>) -> bool {
140140
// Pipe bindings need temporaries (they may have side effects)
141141
IrExpression::PipeBinding(_) => true,
142142
IrExpression::PipeBindingVariadic(_) => true,
143+
// Array and object literals need temporaries
144+
IrExpression::LiteralArray(_) | IrExpression::DerivedLiteralArray(_) => true,
145+
IrExpression::LiteralMap(_) | IrExpression::DerivedLiteralMap(_) => true,
146+
// Unary operators need to check their operand
147+
IrExpression::Unary(u) => needs_temporary_in_safe_access(&u.expr),
148+
IrExpression::Not(n) => needs_temporary_in_safe_access(&n.expr),
149+
// Binary operators need to check both operands
150+
IrExpression::Binary(binary) => {
151+
needs_temporary_in_safe_access(&binary.lhs)
152+
|| needs_temporary_in_safe_access(&binary.rhs)
153+
}
154+
IrExpression::ResolvedBinary(rb) => {
155+
needs_temporary_in_safe_access(&rb.left) || needs_temporary_in_safe_access(&rb.right)
156+
}
157+
// Conditional expressions need to check all branches
158+
IrExpression::Ternary(t) => {
159+
needs_temporary_in_safe_access(&t.condition)
160+
|| needs_temporary_in_safe_access(&t.true_expr)
161+
|| needs_temporary_in_safe_access(&t.false_expr)
162+
}
143163
// AssignTemporary expressions need to check their inner expression
144164
IrExpression::AssignTemporary(assign) => needs_temporary_in_safe_access(&assign.expr),
145165
// Safe property reads need to check their receiver

crates/oxc_angular_compiler/tests/integration_test.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4374,3 +4374,67 @@ export class TestComponent {
43744374
assert!(code.contains("i18n_0"), "Should have i18n_0 variable. Output:\n{code}");
43754375
assert!(code.contains("i18n_1"), "Should have i18n_1 variable. Output:\n{code}");
43764376
}
4377+
4378+
/// Test that pipe inside binary expression with safe navigation generates a temporary variable.
4379+
///
4380+
/// When a pipe result is wrapped in a binary expression (e.g., `(data$ | async) || fallback`)
4381+
/// and then used with safe navigation (`?.`), the compiler should generate a temporary variable
4382+
/// to avoid calling the pipe twice.
4383+
///
4384+
/// This is a port of the Angular TS behavior where `needsTemporaryInSafeAccess` checks through
4385+
/// `BinaryOperatorExpr` to find nested pipe expressions that need temporaries.
4386+
///
4387+
/// Without the fix, the compiler generates:
4388+
/// `(pipeBind1(...) || fallback) == null ? null : (pipeBind1(...) || fallback).prop`
4389+
/// (pipe called twice, slot indices doubled)
4390+
///
4391+
/// With the fix:
4392+
/// `(tmp_0_0 = pipeBind1(...) || fallback) == null ? null : tmp_0_0.prop`
4393+
/// (pipe called once, stored in temp variable)
4394+
#[test]
4395+
fn test_pipe_in_binary_with_safe_nav_uses_temp_variable() {
4396+
let js = compile_template_to_js(
4397+
r#"<div [title]="((data$ | async) || defaultVal)?.name"></div>"#,
4398+
"TestComponent",
4399+
);
4400+
eprintln!("OUTPUT:\n{js}");
4401+
4402+
// Should use a temporary variable to avoid double pipe evaluation
4403+
assert!(
4404+
js.contains("tmp_0_0"),
4405+
"Should generate tmp_0_0 for pipe inside binary with safe nav. Output:\n{js}"
4406+
);
4407+
4408+
// The pipe should only appear ONCE in the expression (stored in tmp)
4409+
let pipe_count = js.matches("pipeBind1(").count();
4410+
assert_eq!(
4411+
pipe_count, 1,
4412+
"pipeBind1 should appear exactly once (not duplicated). Found {pipe_count} occurrences. Output:\n{js}"
4413+
);
4414+
}
4415+
4416+
/// Test pipe in binary with safe navigation and chained property access.
4417+
///
4418+
/// More complex case: `((data$ | async) || fallback)?.nested?.value`
4419+
/// The entire safe navigation chain should use the temp variable.
4420+
#[test]
4421+
fn test_pipe_in_binary_with_safe_nav_chain() {
4422+
let js = compile_template_to_js(
4423+
r#"<div [title]="((data$ | async) || defaultVal)?.nested?.value"></div>"#,
4424+
"TestComponent",
4425+
);
4426+
eprintln!("OUTPUT:\n{js}");
4427+
4428+
// Should use a temporary variable
4429+
assert!(
4430+
js.contains("tmp_0_0"),
4431+
"Should generate tmp_0_0 for pipe inside binary with safe nav chain. Output:\n{js}"
4432+
);
4433+
4434+
// The pipe should only appear ONCE
4435+
let pipe_count = js.matches("pipeBind1(").count();
4436+
assert_eq!(
4437+
pipe_count, 1,
4438+
"pipeBind1 should appear exactly once. Found {pipe_count}. Output:\n{js}"
4439+
);
4440+
}

0 commit comments

Comments
 (0)