Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions tsc/internal/printer/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions tsc/internal/printer/printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
9 changes: 9 additions & 0 deletions tsc/internal/transformers/tstransforms/typeeraser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ func TestTypeEraser(t *testing.T) {
{title: "CallExpression", input: "f<T>()", output: "f();"},
{title: "NewExpression1", input: "new f<T>()", output: "new f();"},
{title: "NewExpression2", input: "new f<T>", 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<T>``", output: "f ``;"},
{title: "NonNullExpression", input: "x!", output: "x;"},
{title: "TypeAssertionExpression#1", input: "<T>x", output: "x;"},
Expand Down