From e1b896d0a6759d9de54fd996ff2ce8904190fe50 Mon Sep 17 00:00:00 2001 From: LongYinan Date: Mon, 9 Feb 2026 20:12:07 +0800 Subject: [PATCH] fix: generate tmp variables for safe navigation on binary/ternary/unary expressions needs_temporary_in_safe_access() in expand_safe_reads was missing match arms for IrExpression::Binary, Ternary, Not, and Unary. When a safe property read like (pipeBind(...) || fallback)?.name had a Binary receiver, the function fell through to `_ => false` instead of recursing into operands to detect the pipe binding. This caused the pipe to be evaluated twice with inflated binding slot indices. Co-Authored-By: Claude Opus 4.6 --- .../src/pipeline/phases/expand_safe_reads.rs | 14 ++++++++++++++ .../tests/integration_test.rs | 14 ++++++++++++++ ...pipe_in_binary_with_safe_property_read.snap | 18 ++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 crates/oxc_angular_compiler/tests/snapshots/integration_test__pipe_in_binary_with_safe_property_read.snap diff --git a/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs b/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs index 9d103add1..66fadd186 100644 --- a/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs +++ b/crates/oxc_angular_compiler/src/pipeline/phases/expand_safe_reads.rs @@ -174,6 +174,20 @@ fn needs_temporary_in_safe_access(expr: &IrExpression<'_>) -> bool { needs_temporary_in_safe_access(&keyed.receiver) || needs_temporary_in_safe_access(&keyed.key) } + // Binary operators need to check both operands + IrExpression::Binary(bin) => { + needs_temporary_in_safe_access(&bin.lhs) || needs_temporary_in_safe_access(&bin.rhs) + } + // Ternary needs to check all branches + IrExpression::Ternary(ternary) => { + needs_temporary_in_safe_access(&ternary.condition) + || needs_temporary_in_safe_access(&ternary.true_expr) + || needs_temporary_in_safe_access(&ternary.false_expr) + } + // Not expression needs to check operand + IrExpression::Not(not) => needs_temporary_in_safe_access(¬.expr), + // Unary operator needs to check operand + IrExpression::Unary(unary) => needs_temporary_in_safe_access(&unary.expr), // Check AST expressions for function calls IrExpression::Ast(ast_expr) => needs_temporary_in_ast_expression(ast_expr), // Parenthesized expressions need to check their inner expression diff --git a/crates/oxc_angular_compiler/tests/integration_test.rs b/crates/oxc_angular_compiler/tests/integration_test.rs index 1953cb876..d9401dcd9 100644 --- a/crates/oxc_angular_compiler/tests/integration_test.rs +++ b/crates/oxc_angular_compiler/tests/integration_test.rs @@ -1156,6 +1156,20 @@ fn test_safe_call_in_listener_inside_conditional() { insta::assert_snapshot!("safe_call_in_listener_inside_conditional", js); } +#[test] +fn test_pipe_in_binary_with_safe_property_read() { + // Pattern from EmailCommentComponent: (comment$ | async || comment)?.new_mentioned_thread_count + // When a pipe binding is inside a binary expression that is the receiver of a safe property read, + // the compiler must generate a temporary variable to avoid evaluating the pipe twice. + // TypeScript Angular compiler produces: (tmp = pipeBind(...) || fallback) == null ? null : tmp.prop + // Without the fix, OXC duplicates the pipe call in both the guard and the access expression. + let js = compile_template_to_js( + r#"
{{ ((data$ | async) || fallback)?.name }}
"#, + "TestComponent", + ); + insta::assert_snapshot!("pipe_in_binary_with_safe_property_read", js); +} + // ============================================================================ // Event Modifier Tests // ============================================================================ diff --git a/crates/oxc_angular_compiler/tests/snapshots/integration_test__pipe_in_binary_with_safe_property_read.snap b/crates/oxc_angular_compiler/tests/snapshots/integration_test__pipe_in_binary_with_safe_property_read.snap new file mode 100644 index 000000000..5517184d7 --- /dev/null +++ b/crates/oxc_angular_compiler/tests/snapshots/integration_test__pipe_in_binary_with_safe_property_read.snap @@ -0,0 +1,18 @@ +--- +source: crates/oxc_angular_compiler/tests/integration_test.rs +assertion_line: 1170 +expression: js +--- +function TestComponent_Template(rf,ctx) { + if ((rf & 1)) { + i0.ɵɵelementStart(0,"div"); + i0.ɵɵtext(1); + i0.ɵɵpipe(2,"async"); + i0.ɵɵelementEnd(); + } + if ((rf & 2)) { + let tmp_0_0; + i0.ɵɵadvance(); + i0.ɵɵtextInterpolate((((tmp_0_0 = (i0.ɵɵpipeBind1(2,1,ctx.data$) || ctx.fallback)) == null)? null: tmp_0_0.name)); + } +}