From cd95fe81994a2604b2c57edd6151202bf91a5e3f Mon Sep 17 00:00:00 2001 From: timurzvural <162848950+timurzvural@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:22:55 +0000 Subject: [PATCH] Fix escaped keyword ASI after class field initializers (#64883) Do not treat Unicode-escaped keywords as binary operators so `\u{69}n` / `\u{69}nstanceof` after a class field initializer become IdentifierNames (matching ECMA-262 and V8). Unescaped `in` still continues the initializer. AI assistance disclosure: this change was authored with Cursor Agent. --- tsc/internal/parser/parser.go | 16 +++++++++++++ tsc/internal/parser/parser_test.go | 38 ++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) 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 () {