Skip to content

fix(compiler): keep parentheses that JavaScript requires (arrow bodies, unary ** bases) - #557

Merged
Brooooooklyn merged 1 commit into
mainfrom
fix/issue-510-arrow-paren
Oct 7, 2026
Merged

Brooooooklyn merged 1 commit into
mainfrom
fix/issue-510-arrow-paren

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Summary

Fixes #510 — the emitter dropped source parentheses, so an arrow body whose leftmost token is { printed invalid JS:

@Input({transform: (v: any) => ({value: v}).value}) a: any;
// was: (v) => {value:v}.value   (invalid / wrong meaning)
// now: (v) => ({value:v}).value

Upstream ngtsc never hits this: it emits source expressions verbatim through WrappedNodeExpr. Our converter rebuilds the AST (Expression::ParenthesizedExpression → OutputExpression::Parenthesized), and the emitter dropped the parens unconditionally. That also masked other required parens — (-1) ** 2, (1).toString(), (obj?.m)(), (() => f)() all lost theirs.

Changes

src/output/emitter.rs:

  • Parenthesized now keeps its parens — they're source parens ngtsc would emit verbatim, or required parens the strip phase kept. Skipped only when the inner expression already self-parenthesizes (BinaryOperator, Conditional, Comma, UnaryOperator with parens), so ((a + b)) sources don't emit double parens.
  • New emits_leading_brace helper walks the leftmost emitted token through ReadProp/ReadKey receivers, InvokeFunction callees and TaggedTemplateLiteral tags; the ArrowFunction body check uses it, replacing the LiteralMap/raw-source whole-body check. () => ({...}) still emits exactly once.

src/pipeline/phases/strip_nonrequired_parentheses.rs:

  • The strip phase now handles IrExpression::Parenthesized — our pipe-aware ingest produces IR-level parens (e.g. (a | async) || b) that were invisible to it before; the emitter's drop-parens behavior masked that. Non-required IR parens are stripped like upstream's single-tree pass.
  • check_ir_expression_for_required_parens marks Parenthesized operands of IR Binary (**, ??, &&/||) as required, mirroring upstream checkExponentiationParens/checkNullishCoalescingParens/checkAndOrParens.
  • The **-base check covers !/typeof/void unary bases too, and classifiers look through nested Parenthesized (((-1)) ** 2 keeps the required wrapper). Upstream needs only UnaryOperatorExpr because its emit goes through the TypeScript printer, which re-adds parens — we emit raw JS.

Tests

New tests/arrow_paren_emit_test.rs (9 tests): both issue repros in @Input transform and useFactory metadata (full + partial mode), () => ({...}) (issue #43), call/member chains on parenthesized objects, verbatim source parens, (-1) ** 2 / (1).toString() / (o?.m)() in metadata, and template-pipeline coverage for all four unary ** bases, nested parens, paren stripping, and ??/&&/?: mixes.

Verification

  • cargo test --all-features: 3077 pass — including the pipe_in_binary_with_safe_property_read snapshot, which now round-trips through the corrected strip phase
  • cargo check + --all-features, cargo fmt --check: clean
  • cargo run -p oxc_angular_conformance: 1264/1264

The emitter dropped every OutputExpression::Parenthesized, so
`(v) => ({value: v}).value` emitted as `(v) => {value:v}.value` — invalid
JS. ngtsc emits such expressions verbatim via WrappedNodeExpr; we rebuild
the AST, so parens that survive the strip phase must print.

- emitter.rs: `Parenthesized` keeps its parens unless the inner
  expression already self-parenthesizes (BinaryOperator, Conditional,
  Comma, UnaryOperator with `parens`). Arrow expression bodies use a new
  `emits_leading_brace` helper that walks the leftmost token through
  member access, calls and tagged templates, replacing the whole-body
  LiteralMap check.
- strip_nonrequired_parentheses.rs: handle `IrExpression::Parenthesized`
  (our pipe-aware ingest produces them; upstream's single-tree strip
  covers them) and mark required parens on IR `Binary` operands for
  `**`/`??`/`&&`/`||`. The unary `**`-base check covers `!`/`typeof`/`void`
  too — upstream only needs `UnaryOperatorExpr` because the TS printer
  re-adds parens; we emit raw JS. Nested `((-1)) ** 2` marks the outer
  wrapper so `(-1) ** 2` survives.

Fixes #510
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-07T09:00:24.416093Z 0bb0d97 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Brooooooklyn
Brooooooklyn merged commit 3a05105 into main Oct 7, 2026
10 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/issue-510-arrow-paren branch October 7, 2026 09:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(emit): (v) => ({value: v}).value is printed as invalid (v) =>{value:v}.value

1 participant