You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Native printer: new (C?.().D) emits as new (C?.()).D (optional call trips hasUnparenthesizedCall) #65148
hasUnparenthesizedCallAtStartOfMemberChain, optional chain, new expression, parenthesize, OperatorPrecedenceOptionalChain, printer
🕗 Version & Regression Information
Present on current main native printer (7.1.0-dev)
Related to #65133 (NonNull in the same helper), but a different gap: optional calls are skipped because OperatorPrecedenceOptionalChain < OperatorPrecedenceMember
💻 Code
Synthetic AST (factory / transform output without an outer ParenthesizedExpression), equivalent to intending:
hasUnparenthesizedCallAtStartOfMemberChain returns false for C?.().D, so emitNewExpression does not force parentheses around the whole member chain. The optional call still gets local parens from ordinary precedence (OptionalChain < Member), producing new (C?.()).D().
🙂 Expected behavior
new(C?.().D)();
Root cause sketch
In hasUnparenthesizedCallAtStartOfMemberChain (printer/utilities.go):
Optional-chain calls report OperatorPrecedenceOptionalChain, which is less thanOperatorPrecedenceMember, so the walk bails out when it reaches C?.() even though that call is exactly the “unparenthesized call at the start of the member chain” the helper exists to detect.
Control that still works: new (C.D?.())() (optional call is the entire callee) — local precedence parenthesization is enough.
Notes
Same helper already fails to walk NonNullExpression (#65133). Fixing the optional-chain precedence bail should be considered alongside that.
Same precedence bail also affects as / satisfies in the member chain (Relational < Member), e.g. synthetic
new((C()asD).E)();
currently prints as
new(C()asD).E();
(still a NewExpression of the property access, so runtime-equivalent without further wrapping, but the helper fails to treat the leading call as “unparenthesized at the start of the chain” for the same reason as optional calls).
I investigated this issue and opened #65329 to fix it, but have since closed that PR after deeper analysis. Summary of what I found:
On current main, the synthetic AST from this issue already emits a syntactically valid result. The unflagged outer access .D takes the Member-precedence branch of emitExpression, and hasUnparenthesizedCallAtStartOfMemberChain supplies the outer parens, so the output is new (C?.()).D() — no SyntaxError.
The chain-interrupted form appears to be intentional, TS5-compatible behavior.TestParenthesizePropertyAccess1/2 in internal/printer/printer_test.go assert that a non-chain access wrapping an optional chain root prints as (a?.b).c.
Semantically, printing (C?.()).D for an AST where .D has no optional-chain flag is faithful: short-circuiting ends at the parenthesized call. The desired output in this issue, new (C?.().D)(), would fold .D into the optional chain and change runtime behavior — for a nullish C, C?.().D evaluates to undefined, while (C?.()).D throws a TypeError. So making the printer produce the minimal form would be a semantic regression unless the AST itself is changed.
The underlying gap seems to be that transforms can construct member chains without propagating the optional-chain flags to the outer accesses, which makes the printed output differ from what a parsed new (C?.().D)() would round-trip to (the parser sets flags on every link, and those round-trip cleanly). If the intent is for the printer to reconstruct the chain in that case, it would require deciding that unflagged accesses over an optional chain root should be treated as part of the chain — a semantic change beyond a printer tweak.
Closing my PR for now; happy to revisit if maintainers decide the printer should treat such synthetic chains as one optional chain.
Activity
timurzvural commentedon Nov 3, 2026
Same precedence bail also affects
as/satisfiesin the member chain (Relational < Member), e.g. syntheticcurrently prints as
(still a
NewExpressionof the property access, so runtime-equivalent without further wrapping, but the helper fails to treat the leading call as “unparenthesized at the start of the chain” for the same reason as optional calls).JemmyIndex commentedon Nov 5, 2026
I investigated this issue and opened #65329 to fix it, but have since closed that PR after deeper analysis. Summary of what I found:
On current
main, the synthetic AST from this issue already emits a syntactically valid result. The unflagged outer access.Dtakes theMember-precedence branch ofemitExpression, andhasUnparenthesizedCallAtStartOfMemberChainsupplies the outer parens, so the output isnew (C?.()).D()— no SyntaxError.The chain-interrupted form appears to be intentional, TS5-compatible behavior.
TestParenthesizePropertyAccess1/2ininternal/printer/printer_test.goassert that a non-chain access wrapping an optional chain root prints as(a?.b).c.Semantically, printing
(C?.()).Dfor an AST where.Dhas no optional-chain flag is faithful: short-circuiting ends at the parenthesized call. The desired output in this issue,new (C?.().D)(), would fold.Dinto the optional chain and change runtime behavior — for a nullishC,C?.().Devaluates toundefined, while(C?.()).Dthrows a TypeError. So making the printer produce the minimal form would be a semantic regression unless the AST itself is changed.The underlying gap seems to be that transforms can construct member chains without propagating the optional-chain flags to the outer accesses, which makes the printed output differ from what a parsed
new (C?.().D)()would round-trip to (the parser sets flags on every link, and those round-trip cleanly). If the intent is for the printer to reconstruct the chain in that case, it would require deciding that unflagged accesses over an optional chain root should be treated as part of the chain — a semantic change beyond a printer tweak.Closing my PR for now; happy to revisit if maintainers decide the printer should treat such synthetic chains as one optional chain.