Skip to content

Commit e4c6212

Browse files
Brooooooklynclaude
andauthored
fix(emitter): preserve parentheses around arrow function object literal body (#47)
When user code like `() => ({ key: val })` was parsed by OXC, the object literal was wrapped in a ParenthesizedExpression, converting to `Parenthesized(LiteralMap(...))` in the output AST. The emitter's arrow function body check only matched direct `LiteralMap`, missing the wrapped case and emitting `() => { key: val }` (a block with a label) instead. Fix by unwrapping one level of `Parenthesized` before checking for `LiteralMap` in the arrow function body emission. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ffc7934 commit e4c6212

1 file changed

Lines changed: 149 additions & 3 deletions

File tree

crates/oxc_angular_compiler/src/output/emitter.rs

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -764,9 +764,14 @@ impl JsEmitter {
764764
ctx.print(") =>");
765765
match &e.body {
766766
ArrowFunctionBody::Expression(body_expr) => {
767-
// Check if the body is an object literal (needs parens)
768-
let is_object_literal =
769-
matches!(body_expr.as_ref(), OutputExpression::LiteralMap(_));
767+
// Check if the body is an object literal (needs parens).
768+
// Also unwrap Parenthesized wrapper, which comes from converting
769+
// OXC's ParenthesizedExpression (e.g. `() => ({ key: val })`).
770+
let inner = match body_expr.as_ref() {
771+
OutputExpression::Parenthesized(p) => p.expr.as_ref(),
772+
other => other,
773+
};
774+
let is_object_literal = matches!(inner, OutputExpression::LiteralMap(_));
770775
if is_object_literal {
771776
ctx.print("(");
772777
}
@@ -2616,4 +2621,145 @@ mod tests {
26162621
let output = emitter.emit_statement(&stmt);
26172622
assert_eq!(output, "function foo() {\n}");
26182623
}
2624+
2625+
// ========================================================================
2626+
// Arrow Function Object Literal Paren Tests (issue #43)
2627+
// ========================================================================
2628+
2629+
#[test]
2630+
fn test_emit_arrow_function_direct_object_literal_body() {
2631+
use super::super::ast::{
2632+
ArrowFunctionBody, ArrowFunctionExpr, LiteralMapEntry, LiteralMapExpr,
2633+
};
2634+
2635+
let emitter = JsEmitter::new();
2636+
let alloc = Allocator::default();
2637+
2638+
// Build: () =>({showMenu:signal(true)})
2639+
let signal_call = OutputExpression::InvokeFunction(Box::new_in(
2640+
super::super::ast::InvokeFunctionExpr {
2641+
fn_expr: Box::new_in(
2642+
OutputExpression::ReadVar(Box::new_in(
2643+
ReadVarExpr { name: Atom::from("signal"), source_span: None },
2644+
&alloc,
2645+
)),
2646+
&alloc,
2647+
),
2648+
args: {
2649+
let mut args = oxc_allocator::Vec::new_in(&alloc);
2650+
args.push(OutputExpression::Literal(Box::new_in(
2651+
LiteralExpr { value: LiteralValue::Boolean(true), source_span: None },
2652+
&alloc,
2653+
)));
2654+
args
2655+
},
2656+
pure: false,
2657+
optional: false,
2658+
source_span: None,
2659+
},
2660+
&alloc,
2661+
));
2662+
2663+
let mut entries = oxc_allocator::Vec::new_in(&alloc);
2664+
entries.push(LiteralMapEntry {
2665+
key: Atom::from("showMenu"),
2666+
value: signal_call,
2667+
quoted: false,
2668+
});
2669+
2670+
let obj_literal = OutputExpression::LiteralMap(Box::new_in(
2671+
LiteralMapExpr { entries, source_span: None },
2672+
&alloc,
2673+
));
2674+
2675+
let params = oxc_allocator::Vec::new_in(&alloc);
2676+
2677+
let expr = OutputExpression::ArrowFunction(Box::new_in(
2678+
ArrowFunctionExpr {
2679+
params,
2680+
body: ArrowFunctionBody::Expression(Box::new_in(obj_literal, &alloc)),
2681+
source_span: None,
2682+
},
2683+
&alloc,
2684+
));
2685+
2686+
let output = emitter.emit_expression(&expr);
2687+
// Object literal body must be wrapped in parens to avoid ambiguity with block
2688+
assert!(output.contains("=>({"), "Expected parens around object literal, got: {output}");
2689+
assert!(output.ends_with(")"), "Expected closing paren, got: {output}");
2690+
}
2691+
2692+
#[test]
2693+
fn test_emit_arrow_function_parenthesized_object_literal_body() {
2694+
use super::super::ast::{
2695+
ArrowFunctionBody, ArrowFunctionExpr, LiteralMapEntry, LiteralMapExpr,
2696+
ParenthesizedExpr,
2697+
};
2698+
2699+
let emitter = JsEmitter::new();
2700+
let alloc = Allocator::default();
2701+
2702+
// Build: () =>({showMenu:signal(true)})
2703+
// But the body is Parenthesized(LiteralMap(...)) as would come from OXC AST conversion
2704+
let signal_call = OutputExpression::InvokeFunction(Box::new_in(
2705+
super::super::ast::InvokeFunctionExpr {
2706+
fn_expr: Box::new_in(
2707+
OutputExpression::ReadVar(Box::new_in(
2708+
ReadVarExpr { name: Atom::from("signal"), source_span: None },
2709+
&alloc,
2710+
)),
2711+
&alloc,
2712+
),
2713+
args: {
2714+
let mut args = oxc_allocator::Vec::new_in(&alloc);
2715+
args.push(OutputExpression::Literal(Box::new_in(
2716+
LiteralExpr { value: LiteralValue::Boolean(true), source_span: None },
2717+
&alloc,
2718+
)));
2719+
args
2720+
},
2721+
pure: false,
2722+
optional: false,
2723+
source_span: None,
2724+
},
2725+
&alloc,
2726+
));
2727+
2728+
let mut entries = oxc_allocator::Vec::new_in(&alloc);
2729+
entries.push(LiteralMapEntry {
2730+
key: Atom::from("showMenu"),
2731+
value: signal_call,
2732+
quoted: false,
2733+
});
2734+
2735+
let obj_literal = OutputExpression::LiteralMap(Box::new_in(
2736+
LiteralMapExpr { entries, source_span: None },
2737+
&alloc,
2738+
));
2739+
2740+
// Wrap in Parenthesized (this is what convert_oxc_expression produces)
2741+
let parenthesized = OutputExpression::Parenthesized(Box::new_in(
2742+
ParenthesizedExpr { expr: Box::new_in(obj_literal, &alloc), source_span: None },
2743+
&alloc,
2744+
));
2745+
2746+
let params = oxc_allocator::Vec::new_in(&alloc);
2747+
2748+
let expr = OutputExpression::ArrowFunction(Box::new_in(
2749+
ArrowFunctionExpr {
2750+
params,
2751+
body: ArrowFunctionBody::Expression(Box::new_in(parenthesized, &alloc)),
2752+
source_span: None,
2753+
},
2754+
&alloc,
2755+
));
2756+
2757+
let output = emitter.emit_expression(&expr);
2758+
// Even when wrapped in Parenthesized, the object literal body must have parens
2759+
assert!(
2760+
output.contains("=>({"),
2761+
"Expected parens around parenthesized object literal, got: {output}"
2762+
);
2763+
assert!(output.ends_with(")"), "Expected closing paren, got: {output}");
2764+
}
26192765
}

0 commit comments

Comments
 (0)