diff --git a/tsc/internal/ast/precedence.go b/tsc/internal/ast/precedence.go index bfaaa679b63af..c159cbe365d77 100644 --- a/tsc/internal/ast/precedence.go +++ b/tsc/internal/ast/precedence.go @@ -392,6 +392,7 @@ func GetLeftmostExpression(node *Expression, stopAtCallExpressions bool) *Expres KindPropertyAccessExpression, KindNonNullExpression, KindPartiallyEmittedExpression, + KindParenthesizedExpression, KindSatisfiesExpression: node = node.Expression() continue diff --git a/tsc/internal/parser/parser.go b/tsc/internal/parser/parser.go index c670d64ad21c2..94a5925891e6b 100644 --- a/tsc/internal/parser/parser.go +++ b/tsc/internal/parser/parser.go @@ -4666,6 +4666,14 @@ func (p *Parser) parseBinaryExpressionRest(precedence ast.OperatorPrecedence, le // We either have a binary operator here, or we're finished. We call // reScanGreaterToken so that we merge token sequences like > and = into >= operator := p.reScanGreaterThanToken() + // A keyword written with Unicode escapes is an IdentifierName, not a keyword + // terminal (ECMA-262 §5.1.5). Do not treat escaped `in` / `instanceof` / `as` / + // `satisfies` as binary operators so ASI can end the previous expression + // (e.g. a class field initializer) and the escaped name can start a new member. + // See https://github.com/microsoft/TypeScript/issues/64883. + if p.isEscapedKeyword() { + break + } newPrecedence := ast.GetBinaryOperatorPrecedence(operator) // Check the precedence to see if we should "take" this operator // - For left associative operator (all operator but **), consume the operator, @@ -6358,10 +6366,18 @@ func (p *Parser) isImportAttributeName() bool { return tokenIsIdentifierOrKeyword(p.token) || p.token == ast.KindStringLiteral } +func (p *Parser) isEscapedKeyword() bool { + return ast.IsKeyword(p.token) && (p.scanner.HasUnicodeEscape() || p.scanner.HasExtendedUnicodeEscape()) +} + func (p *Parser) isBinaryOperator() bool { if p.inDisallowInContext() && p.token == ast.KindInKeyword { return false } + // Escaped keywords are IdentifierNames, not keyword operators. + if p.isEscapedKeyword() { + return false + } return ast.GetBinaryOperatorPrecedence(p.token) != ast.OperatorPrecedenceInvalid } diff --git a/tsc/internal/parser/parser_test.go b/tsc/internal/parser/parser_test.go index ffa96713eb444..4a58726b2e3de 100644 --- a/tsc/internal/parser/parser_test.go +++ b/tsc/internal/parser/parser_test.go @@ -365,6 +365,44 @@ func TestParseEscapedDynamicImportPhase(t *testing.T) { } } +func TestParseEscapedInInstanceofAfterClassField(t *testing.T) { + t.Parallel() + // Escaped `in` / `instanceof` after a class field initializer are IdentifierNames, + // not binary operators. ASI should end the previous field; the escaped names are + // additional fields. See https://github.com/microsoft/TypeScript/issues/64883. + source := "class C {\n a = 1\n \\u{69}n\n b = 2\n \\u{69}nstanceof\n}" + file := parsetestutil.ParseTypeScript(source, false) + parsetestutil.CheckDiagnostics(t, file) + assert.Equal(t, len(file.Statements.Nodes), 1) + classDecl := file.Statements.Nodes[0] + assert.Assert(t, ast.IsClassDeclaration(classDecl)) + members := classDecl.Members() + assert.Equal(t, len(members), 4) + assert.Equal(t, members[0].Name().Text(), "a") + assert.Equal(t, members[1].Name().Text(), "in") + assert.Equal(t, members[2].Name().Text(), "b") + assert.Equal(t, members[3].Name().Text(), "instanceof") +} + +func TestParseUnescapedInContinuesClassFieldInitializer(t *testing.T) { + t.Parallel() + // Without escapes, `in` on the next line correctly continues the initializer + // (test262 fields-asi-5.js). This must keep working after the escaped-keyword fix. + source := "class C {\n a = x\n in\n z\n}" + file := parsetestutil.ParseTypeScript(source, false) + parsetestutil.CheckDiagnostics(t, file) + assert.Equal(t, len(file.Statements.Nodes), 1) + classDecl := file.Statements.Nodes[0] + assert.Assert(t, ast.IsClassDeclaration(classDecl)) + members := classDecl.Members() + assert.Equal(t, len(members), 1) + prop := members[0] + assert.Equal(t, prop.Name().Text(), "a") + init := prop.Initializer() + assert.Assert(t, ast.IsBinaryExpression(init)) + assert.Equal(t, init.AsBinaryExpression().OperatorToken.Kind, ast.KindInKeyword) +} + func TestJSDocImportTypeParentChain(t *testing.T) { t.Parallel() sourceText := `test("", async function () { diff --git a/tsc/internal/printer/printer.go b/tsc/internal/printer/printer.go index 46bb47c25ebad..2b1c153a4353a 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. + expr := ast.SkipPartiallyEmittedExpressions(node.Expression) + if ast.GetLeftmostExpression(expr, true /*stopAtCallExpressions*/).Kind == ast.KindCallExpression { p.emitExpression(node.Expression, ast.OperatorPrecedenceParentheses) } else { p.emitExpression(node.Expression, ast.OperatorPrecedenceMember) 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;"},