diff --git a/tsc/internal/printer/printer.go b/tsc/internal/printer/printer.go index 46bb47c25ebad..d2a212e7fd698 100644 --- a/tsc/internal/printer/printer.go +++ b/tsc/internal/printer/printer.go @@ -2593,8 +2593,13 @@ func (p *Printer) emitNewExpression(node *ast.NewExpression) { state := p.enterNode(node.AsNode()) p.emitToken(ast.KindNewKeyword, node.Pos(), WriteKindKeyword, node.AsNode()) p.writeSpace() - if ast.SkipPartiallyEmittedExpressions(node.Expression).Kind == ast.KindCallExpression { - // Parenthesize `C()` inside of a NewExpression so it is treated as `new (C())` and not `new C()` + // Parenthesize when the expression contains a CallExpression that would otherwise + // bind to `new`. `new C()` is fine, but `new C().D()` parses as `(new C()).D()`, so + // `new (C().D)()` needs parentheses around the whole expression — including after + // type assertions / satisfies / non-null are erased (e.g. `new (f() as T).C()`). + // See https://github.com/microsoft/TypeScript/issues/64981 and + // https://github.com/microsoft/TypeScript/pull/65015. + if newExpressionOperandNeedsParens(node.Expression) { p.emitExpression(node.Expression, ast.OperatorPrecedenceParentheses) } else { p.emitExpression(node.Expression, ast.OperatorPrecedenceMember) @@ -2604,6 +2609,34 @@ func (p *Printer) emitNewExpression(node *ast.NewExpression) { p.exitNode(node.AsNode(), state) } +// newExpressionOperandNeedsParens reports whether the operand of `new` must be +// parenthesized so a CallExpression is not parsed as the NewExpression callee. +// +// Parentheses are walked here only — not in ast.GetLeftmostExpression — so that +// emitConciseBody can still treat an already-parenthesized object literal +// `({ ... })` as parenthesized and avoid emitting a second pair. +func newExpressionOperandNeedsParens(expression *ast.Expression) bool { + node := ast.SkipPartiallyEmittedExpressions(expression) + for { + node = ast.SkipPartiallyEmittedExpressions(node) + switch node.Kind { + case ast.KindCallExpression: + return true + case ast.KindParenthesizedExpression, + ast.KindPropertyAccessExpression, + ast.KindElementAccessExpression, + ast.KindNonNullExpression, + ast.KindAsExpression, + ast.KindSatisfiesExpression, + ast.KindPartiallyEmittedExpression: + node = node.Expression() + continue + default: + return false + } + } +} + func (p *Printer) emitTemplateLiteral(node *ast.TemplateLiteral) { switch node.Kind { case ast.KindNoSubstitutionTemplateLiteral: diff --git a/tsc/internal/printer/printer_test.go b/tsc/internal/printer/printer_test.go index 867ef150d491c..5cc660b315103 100644 --- a/tsc/internal/printer/printer_test.go +++ b/tsc/internal/printer/printer_test.go @@ -1047,6 +1047,40 @@ func TestParenthesizeNew3(t *testing.T) { emittestutil.CheckEmit(t, nil, file.AsSourceFile(), "new C((a, b));") } +func TestParenthesizeNew4(t *testing.T) { + t.Parallel() + + // new (C().D)() — CallExpression is not the direct expression of `new`, but + // appears on the left of a property access. Without parentheses this would + // parse as (new C()).D(). + var factory ast.NodeFactory + file := factory.NewSourceFile(ast.SourceFileParseOptions{FileName: "/file.ts", PathKey: "/file.ts"}, "", factory.NewNodeList( + []*ast.Node{ + factory.NewExpressionStatement( + factory.NewNewExpression( + factory.NewPropertyAccessExpression( + factory.NewCallExpression( + factory.NewIdentifier("C"), + nil, /*questionDotToken*/ + nil, /*typeArguments*/ + factory.NewNodeList([]*ast.Node{}), + ast.NodeFlagsNone, + ), + nil, /*questionDotToken*/ + factory.NewIdentifier("D"), + ast.NodeFlagsNone, + ), + nil, /*typeArguments*/ + factory.NewNodeList([]*ast.Node{}), + ), + ), + }, + ), factory.NewToken(ast.KindEndOfFile)) + + parsetestutil.MarkSyntheticRecursive(file) + emittestutil.CheckEmit(t, nil, file.AsSourceFile(), "new (C().D)();") +} + func TestParenthesizeTaggedTemplate1(t *testing.T) { t.Parallel() diff --git a/tsc/internal/transformers/tstransforms/typeeraser_test.go b/tsc/internal/transformers/tstransforms/typeeraser_test.go index 56d8fce25a804..7e9b97f6e4bea 100644 --- a/tsc/internal/transformers/tstransforms/typeeraser_test.go +++ b/tsc/internal/transformers/tstransforms/typeeraser_test.go @@ -58,6 +58,15 @@ func TestTypeEraser(t *testing.T) { {title: "CallExpression", input: "f()", output: "f();"}, {title: "NewExpression1", input: "new f()", output: "new f();"}, {title: "NewExpression2", input: "new f", output: "new f;"}, + // After erasing `as T`, parentheses around the call must be restored so + // `new (f().C)()` is not emitted as `new f().C()` (== `(new f()).C()`). + // See https://github.com/microsoft/TypeScript/issues/64981. + {title: "NewExpression3", input: "new (f() as T).C()", output: "new (f().C)();"}, + {title: "NewExpression4", input: "new (f() as T).C", output: "new (f().C);"}, + {title: "NewExpression5", input: "new (f() satisfies T).C()", output: "new (f().C)();"}, + // Non-null doesn't elide the original parentheses the way `as`/`satisfies` do, + // so the call keeps its own grouping: `new ((f()).C)()`. + {title: "NewExpression6", input: "new (f()!).C()", output: "new ((f()).C)();"}, {title: "TaggedTemplateExpression", input: "f``", output: "f ``;"}, {title: "NonNullExpression", input: "x!", output: "x;"}, {title: "TypeAssertionExpression#1", input: "x", output: "x;"},