Skip to content

fix(formatter): keep parens in chained export default type cast#23678

Closed
ammubhave wants to merge 1 commit into
oxc-project:mainfrom
ammubhave:claude/recursing-swartz-d5243b
Closed

fix(formatter): keep parens in chained export default type cast#23678
ammubhave wants to merge 1 commit into
oxc-project:mainfrom
ammubhave:claude/recursing-swartz-d5243b

Conversation

@ammubhave

@ammubhave ammubhave commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #23501

Problem

oxfmt drops the load-bearing outer parentheses in export default (fn as T) when the cast is chained:

// input
export default (function foo() {} as unknown as Foo);

// oxfmt output (wrong — parens lost)
export default function foo() {} as unknown as Foo;

Without the parens the leading function/class token makes export default parse the right-hand side as a declaration, so the trailing as … cast is no longer attached to anything. Downstream new defaultExport(...) then fails with TS7009.

Cause

The ExportDefaultDeclaration arm of ts_as_or_satisfies_needs_parens only inspected the direct inner expression:

AstNodes::ExportDefaultDeclaration(_) =>
    matches!(inner, Expression::FunctionExpression(_) | Expression::ClassExpression(_)),

A single as worked, but for a chained cast the outer as node's inner is another TSAsExpression, so the FunctionExpression/ClassExpression check failed and the parens were dropped.

Fix

Walk to the leftmost descendant via ExpressionLeftSide::leftmost before checking for FunctionExpression/ClassExpression — mirroring the existing CallExpression handler, which already does this for the same export default scenario. This matches the ECMAScript grammar: export default forbids a right-hand side whose leading token is function/async function/class, so parens are needed exactly when the leftmost leaf is a function/class expression.

Tests

  • New fixture tests/fixtures/ts/parenthesis/export-default-as.ts.
  • Full oxc_formatter fixture suite passes (290 tests).
  • Verified no over-parenthesization: identifiers, calls, object literals, and non-export-default contexts get no parens; function/class as leftmost (even chained or via member access) keep them.

🤖 Generated with Claude Code

`export default (fn as T)` was losing its outer parentheses when the
cast was chained, e.g. `export default (function foo() {} as unknown as
Foo)`. The parens are load-bearing: without them the leading `function`/
`class` token makes `export default` parse the right-hand side as a
declaration, detaching the `as` cast.

The `export default` case in `ts_as_or_satisfies_needs_parens` only
inspected the direct inner expression, so a chained cast (whose inner is
another `TSAsExpression`) failed the `FunctionExpression`/
`ClassExpression` check. Walk to the leftmost descendant instead,
mirroring the existing `CallExpression` handler.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ammubhave ammubhave requested a review from leaysgur as a code owner June 21, 2026 04:19
@codspeed-hq

codspeed-hq Bot commented Jun 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 52 untouched benchmarks
⏩ 19 skipped benchmarks1


Comparing ammubhave:claude/recursing-swartz-d5243b (902b1a2) with main (36009dd)

Open in CodSpeed

Footnotes

  1. 19 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-formatter Area - Formatter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

formatter: export default (fn as T) loses outer parens, breaking the type assertion

2 participants