Skip to content

fix(64231): avoid parsing async() calls as typed arrows in conditionals - #64233

Open
Oleksandr Tarasiuk (a-tarasyuk) wants to merge 12 commits into
microsoft:mainfrom
a-tarasyuk:fix/64231
Open

Oleksandr Tarasiuk (a-tarasyuk) wants to merge 12 commits into
microsoft:mainfrom
a-tarasyuk:fix/64231

Conversation

@a-tarasyuk

Copy link
Copy Markdown
Contributor

Fixes #64231

Copilot AI balanced review requested due to automatic review settings September 10, 2026 17:39
@github-project-automation github-project-automation Bot moved this to Not started in PR Backlog Sep 10, 2026
@typescript-automation typescript-automation Bot added the For Uncommitted Bug PR for untriaged, rejected, closed or missing bug label Sep 10, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The parser change is narrowly scoped and is backed by targeted regression tests plus updated baselines covering the reported ambiguity and key variants.

Pull request overview

Fixes a long-standing parsing ambiguity where async() inside conditional (?:) expressions could be misread as the start of an async arrow with a return type annotation, producing spurious syntax errors in valid code.

Changes:

  • Adjusts the parser’s parenthesized-arrow lookahead so async () : ... is treated as ambiguous (and therefore speculatively parsed) instead of being assumed to be an arrow.
  • Adds regression coverage for a ? async() : ... patterns in both TS and JS (allowJs/checkJs) test suites, including nested conditionals and mixed arrow branches.
  • Updates the corresponding reference baselines (types/symbols/errors and JS emit) to reflect the corrected parse.
File summaries
File Description
tsc/internal/parser/parser.go Makes async () : return TSUnknown in lookahead to avoid misclassifying ternary : as an arrow return-type :.
tsc/testdata/tests/cases/conformance/async/es2017/asyncArrowFunction/asyncArrowFunction_allowJs.ts Adds allowJs regression cases for conditional expressions involving async() calls.
tsc/testdata/tests/cases/compiler/parenthesizedAsyncArrowFunction.ts Adds compiler regression cases covering the reported conditional parsing scenarios and related edge cases.
tsc/testdata/baselines/reference/conformance/asyncArrowFunction_allowJs.types Baseline updates for the new allowJs conditional parsing cases.
tsc/testdata/baselines/reference/conformance/asyncArrowFunction_allowJs.symbols Symbol baseline updates for the new allowJs cases.
tsc/testdata/baselines/reference/conformance/asyncArrowFunction_allowJs.errors.txt Confirms the new allowJs cases introduce no additional diagnostics.
tsc/testdata/baselines/reference/compiler/parenthesizedAsyncArrowFunction.types Type baseline updates for the new compiler regression cases.
tsc/testdata/baselines/reference/compiler/parenthesizedAsyncArrowFunction.symbols Symbol baseline updates for the new compiler regression cases.
tsc/testdata/baselines/reference/compiler/parenthesizedAsyncArrowFunction.js Emit baseline updates demonstrating correct parsing/emit around the new conditional cases.
Review details
  • Files reviewed: 9/9 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@DanielRosenwasser

Copy link
Copy Markdown
Member

TypeScript Bot (@typescript-bot) test top800

@typescript-automation

typescript-automation Bot commented Sep 10, 2026 •

Copy link
Copy Markdown
Contributor

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top800 ✅ Started ✅ Results

@typescript-automation

Copy link
Copy Markdown
Contributor

Daniel Rosenwasser (@DanielRosenwasser) Here are the results of running the top 800 repos with tsc comparing main and refs/pull/64233/merge:

Everything looks good!

@typescript-automation typescript-automation Bot added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Sep 11, 2026
@jakebailey

Copy link
Copy Markdown
Member

I had copilot look at this and it caught some cases I missed:


This looks correct for the reported examples. Could we also cover spread arguments? The (... lookahead currently returns TSTrue even after async, although that can be an ordinary call:

const xs = [1, 2];
const spread1 = a ? async(...xs) : 0;
const spread2 = a ? async(...xs) : x => x;

Both still fail on this branch. It looks like the spread lookahead could similarly return TSUnknown when preceded by async. A typed-rest-arrow control would help ensure that remains recognized:

const restArrow = a
    ? async (...xs: number[]): Promise<number> => xs.length
    : () => 0;

There is also a remaining nested case worth covering specifically in the JavaScript test:

const nested1 = a ? a ? async() : x => x : 0;
const nested2 = a ? a ? async(1) : x => x : 0;

Node and esbuild's JS parser accept both, but this branch reports a spurious type annotation and ':' expected. The speculative inner arrow parse appears to use the outer conditional's colon as confirmation of a return-type annotation.

These are pre-existing failures, not regressions introduced here, so the nested-JS issue could reasonably be a follow-up.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Nested typed async arrows with empty parameter lists can still select the wrong conditional separator and change program behavior.

1 open issue

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Nested typed-arrow forms can still misparse conditional branches and change emitted program behavior.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Conditional restrictions are still lost inside unambiguous arrow bodies, causing valid JavaScript to produce a syntax error.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Shared parser disambiguation and rollback changes need human confirmation and compiler-suite validation, which was not run.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

An unresolved conditional-arrow parsing defect and ambiguity-sensitive backtracking require correction and human review.

1 open issue

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Nested conditionals can still consume an enclosing separator during speculative arrow parsing and reject valid code.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go

@jakebailey Jake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had Copilot do a bunch of cross checking, and there are some interesting edge cases to think about here.

  • No regressions: everything under testdata/tests plus node_modules was parsed with main and with this PR (~80k parses across TS/TSX/JS). ASTs and diagnostics were identical.
  • No backtracking blowup: deeply nested a ? async() : x => … chains (up to 40 levels) parse in the same time as on main.
  • A bonus fix: async(...a.b), async(...a()) and async(...[1]) fail to parse on main even outside conditionals, and this PR fixes them. That might be worth a test and a mention in the description.

Here's how other parsers handle the same inputs, with r = in front of each expression:

Input TS 6.0 main this PR esbuild swc oxc bun
a ? async() : 0 ❌ ❌ call call call call call
a ? async() : x => x ❌ ❌ call ❌ call call call
a ? async(...c) : x => x ❌ ❌ call ❌ call ❌ call*
a ? async(): number => 1 : 2 arrow arrow arrow arrow arrow arrow arrow
a ? (c): T => (d) : x => x arrow arrow arrow call arrow arrow call
a ? async(): T => (d) : x => x ❌ ❌ call ❌ arrow arrow call
a ? async (): T => async() : x => x ❌ ❌ call ❌ arrow arrow call
a ? async(): x => async() : y ❌ ❌ ❌ arrow arrow arrow arrow
async(...a.b) ❌ ❌ ok ok ok ❌ ok

(* Bun drops the spread here and emits async(c). That's a Bun bug.)

"call" means async(...) was parsed as a call in the true branch; "arrow" means a typed async arrow. Versions: TypeScript 6.0.3, main at 98add1bbed1, esbuild 0.28.2, swc 1.16.18, oxc-transform 0.155.0, bun 1.4.3.

The cases from the issue are fixed. The ambiguous typed cases have no consensus between tools, but a couple of them are inconsistent with how this PR treats the parenthesized form. See the inline comments.

We need to think about these edge cases and decide what behavior we actually want, ideally captured in tests, rather than leaving it to whatever falls out of the current heuristics.

Nit: could the description mention the async(...a.b) fix outside conditionals, and the jsdocDiagnostics/reparseList mark/rewind change? Both are behavior changes beyond the issue.

Comment thread tsc/internal/parser/parser.go Outdated
parameters = p.createMissingList()
} else {
if isAsync && typeParameters == nil && (p.token == ast.KindCloseParenToken || p.token == ast.KindDotDotDotToken) {
bodyFlags = flags & ExpressionParseFlagsAllowAmbiguousAsyncArrow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This removes DisallowArrowReturnType from the body of async()/async(...) arrows, so they resolve differently from the parenthesized equivalent:

a ? (c): T => (d) : x => x;      // arrow in true branch: a ? ((c): T => (d)) : (x => x)
a ? async(): T => (d) : x => x;  // call: a ? async() : (T => (d) => x)

Both readings are valid TS. Still, the existing rule for parenthesized arrows prefers the arrow in the true branch when a : follows, and swc and oxc pick the arrow for both. Is this difference intentional, perhaps to get f17 in the test to parse as a call?

Also, this assignment only has an effect when requiresColon is true, because the => branch below overwrites bodyFlags otherwise. Moving it into that branch would make that clearer.

Comment thread tsc/internal/parser/parser.go Outdated
return result
}

func (p *Parser) nextTokenIsStartOfArrowFunction() bool {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a lookahead heuristic, the result depends on how the return type is spelled:

a ? async (): Promise<number> => async() : x => x; // arrow (matches f27)
a ? async (): T => async() : x => x;               // call: a ? async() : (T => async(): x => x)

Both are valid TS, but changing the type name shouldn't change which reading we pick.

Comment thread tsc/internal/parser/parser.go Outdated
if lastToken == ast.KindEqualsGreaterThanToken || lastToken == ast.KindOpenBraceToken {
body = p.parseArrowFunctionExpressionBody(isAsync, allowReturnTypeInArrowFunction)
if requiresColon {
bodyFlags |= ExpressionParseFlagsAllowAmbiguousAsyncArrow

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This input still fails to parse, although it's valid TS:

a ? async(): x => async() : y;
// error TS1005: ';' expected

esbuild, swc, oxc and bun all parse it as a ? (async(): x => async()) : y. The parenthesized version a ? (c): x => (d) : y already works. The cause seems to be the flag on the outer body: it makes the inner async() : y count as a definite arrow, the outer arrow then finds no : after its body, and gets rejected. main fails on this too, so it's not a regression. It might be worth a test, even if it's only recorded as a known limitation.

Comment thread tsc/internal/parser/parser_test.go Outdated
assert.Equal(t, missingImplementsDecl.HeritageClauses.Nodes[0].AsHeritageClause().Types.Nodes[0].Kind, ast.KindExpressionWithTypeArguments)
}

func TestConditionalAsyncCall(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of these ~590 lines of unit tests repeat what parenthesizedAsyncArrowFunction{,Js}.ts already covers. Could some of them become baseline cases instead? That's the usual pattern here and easier to review. Keeping only the ones that check things the baselines can't show (e.g. YieldContext flags, JSDoc rewind) seems enough.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The unresolved exponential reparsing issue and broader rollback and caching changes require human review supported by performance validation.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread tsc/internal/parser/parser.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Failed speculative type-argument parsing can incorrectly reject and cache a valid async arrow head.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment on lines +347 to +348
if p.parsingFlags&ParsingFlagsTentative != 0 {
panic(tentativeParseFailure{})

This branch has not been deployed

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

Labels

For Milestone Bug PRs that fix a bug with a specific milestone

Projects

Status: Not started

Development

Successfully merging this pull request may close these issues.

Parser misinterprets async() calls in conditional expressions as async arrow functions

4 participants