Skip to content
Open
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
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