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
1 change: 1 addition & 0 deletions tsc/internal/ast/precedence.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ func GetLeftmostExpression(node *Expression, stopAtCallExpressions bool) *Expres
KindPropertyAccessExpression,
KindNonNullExpression,
KindPartiallyEmittedExpression,
KindParenthesizedExpression,
KindSatisfiesExpression:
node = node.Expression()
continue
Expand Down
16 changes: 16 additions & 0 deletions tsc/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down
38 changes: 38 additions & 0 deletions tsc/internal/parser/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
9 changes: 7 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.
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)
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