fix(parser): correct precedence handling for private-in expression (#18169)

## Summary

- Fix incorrect parsing of `1 + #a in b` which was parsed as `1 + (#a in b)` instead of `(1 + #a) in b`
- Add precedence check before parsing `#a in` as `PrivateInExpression`
- Report "Unexpected private identifier" error when precedence doesn't allow `in` operator

## Details

The parser was greedily parsing `#a in` as a `PrivateInExpression` without checking operator precedence. For example, when parsing the RHS of `+` in `1 + #a in b`, the `in` operator's precedence (`Compare = 13`) is lower than `+`'s precedence (`Add = 15`), so `#a in` should not be parsed as a unit.

The fix extracts private-in parsing into `parse_private_in_expression` and adds a check: if `lhs_precedence >= Precedence::Compare`, we reject the private identifier with a syntax error, matching V8 and Babel behavior.

## Test plan

- `cargo test -p oxc_parser` passes
- Conformance improved: Negative Passed 1648 → 1649 for Babel tests
- `invalid-private-followed-by-in-2` test case now correctly reports syntax error

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-01-18 07:51:38 +00:00
1 parent 4f928bf119
commit 2c6966dfe1
5 files changed
+52 -29

No files matched your search

+5
View File
@@ -730,6 +730,11 @@ pub fn private_in_private(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Unexpected right-hand side of private-in expression").with_label(span)
}
#[cold]
pub fn unexpected_private_identifier(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Unexpected private identifier").with_label(span)
}
#[cold]
pub fn import_arguments(span: Span) -> OxcDiagnostic {
OxcDiagnostic::error("Dynamic imports can only accept a module specifier and an optional set of attributes as arguments").with_label(span)
+22 -8
View File
@@ -157,6 +157,27 @@ impl<'a> ParserImpl<'a> {
self.ast.private_identifier(span, name)
}
/// [+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
fn parse_private_in_expression(
&mut self,
lhs_span: u32,
lhs_precedence: Precedence,
) -> Expression<'a> {
let left = self.parse_private_identifier();
// Check if `in` operator precedence is allowed at current level.
// For `1 + #a in b`, when parsing RHS of `+`, lhs_precedence is `Add` which is
// higher than `Compare` (the precedence of `in`), so `#a in` cannot be parsed here.
if lhs_precedence >= Precedence::Compare {
return self.fatal_error(diagnostics::unexpected_private_identifier(left.span));
}
self.expect(Kind::In);
let right = self.parse_binary_expression_or_higher(Precedence::Compare);
if let Expression::PrivateInExpression(private_in_expr) = right {
return self.fatal_error(diagnostics::private_in_private(private_in_expr.span));
}
self.ast.expression_private_in(self.end_span(lhs_span), left, right)
}
/// Section [Primary Expression](https://tc39.es/ecma262/#sec-primary-expression)
/// `PrimaryExpression`[Yield, Await] :
/// this
@@ -1143,14 +1164,7 @@ impl<'a> ParserImpl<'a> {
let lhs_parenthesized = self.at(Kind::LParen);
// [+In] PrivateIdentifier in ShiftExpression[?Yield, ?Await]
let lhs = if self.ctx.has_in() && self.at(Kind::PrivateIdentifier) {
let left = self.parse_private_identifier();
self.expect(Kind::In);
let right = self.parse_binary_expression_or_higher(Precedence::Compare);
if let Expression::PrivateInExpression(private_in_expr) = right {
let error = diagnostics::private_in_private(private_in_expr.span);
return self.fatal_error(error);
}
self.ast.expression_private_in(self.end_span(lhs_span), left, right)
self.parse_private_in_expression(lhs_span, lhs_precedence)
} else {
let has_pure_comment = self.lexer.trivia_builder.previous_token_has_pure_comment();
let mut expr = self.parse_unary_expression_or_higher(lhs_span);
+17 -13
View File
@@ -3,9 +3,7 @@ commit: fc58af40
parser_babel Summary:
AST Parsed : 2224/2230 (99.73%)
Positive Passed: 2207/2230 (98.97%)
Negative Passed: 1648/1697 (97.11%)
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2022/private-in/invalid-private-followed-by-in-2/input.js
Negative Passed: 1649/1697 (97.17%)
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/es2026/explicit-resource-management/invalid-for-using-of-no-initializer/input.js
Expect Syntax Error: tasks/coverage/babel/packages/babel-parser/test/fixtures/estree/class-private-property/typescript-invalid-abstract/input.ts
@@ -7370,12 +7368,11 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
· ────────────────
╰────
× Expected `in` but found `(`
╭─[babel/packages/babel-parser/test/fixtures/es2022/class-private-methods/asi-failure-generator/input.js:3:7]
× Unexpected private identifier
╭─[babel/packages/babel-parser/test/fixtures/es2022/class-private-methods/asi-failure-generator/input.js:3:4]
2 │ p = x
3 │ *#m () {}
· ┬
· ╰── `in` expected
· ──
4 │ }
╰────
@@ -8538,14 +8535,22 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
╰────
help: Did you mean `export { "吾道一以貫之。" as "忠恕。" } from 'some-module'`?
× Unexpected right-hand side of private-in expression
× Unexpected private identifier
╭─[babel/packages/babel-parser/test/fixtures/es2022/private-in/invalid-private-followed-by-in-1/input.js:5:11]
4 │ method() {
5 │ #a in #b in c
· ───────
· ──
6 │ }
╰────
× Unexpected private identifier
╭─[babel/packages/babel-parser/test/fixtures/es2022/private-in/invalid-private-followed-by-in-2/input.js:4:9]
3 │ method() {
4 │ 1 + #a in b
· ──
5 │ }
╰────
× Unexpected token
╭─[babel/packages/babel-parser/test/fixtures/es2022/private-in/invalid-private-followed-by-in-3/input.js:5:18]
4 │ method() {
@@ -8571,12 +8576,11 @@ Expect to Parse: tasks/coverage/babel/packages/babel-parser/test/fixtures/typesc
5 │ }
╰────
× Expected `in` but found `;`
╭─[babel/packages/babel-parser/test/fixtures/es2022/private-in/private-binary-expression-right/input.js:4:11]
× Unexpected private identifier
╭─[babel/packages/babel-parser/test/fixtures/es2022/private-in/private-binary-expression-right/input.js:4:9]
3 │ test() {
4 │ 1 + #x;
· ┬
· ╰── `in` expected
· ──
5 │ }
╰────
+2 -2
View File
@@ -18123,11 +18123,11 @@ Negative Passed: 4581/4581 (100.00%)
· ───────────
╰────
× Unexpected right-hand side of private-in expression
× Unexpected private identifier
╭─[test262/test/language/expressions/in/private-field-in-nested.js:26:15]
25 │ constructor() {
26 │ #field in #field in this;
· ──────────────
· ──────
27 │ }
╰────
@@ -14345,12 +14345,12 @@ Expect to Parse: tasks/coverage/typescript/tests/cases/conformance/statements/Va
26 │
╰────
× Cannot assign to this expression
╭─[typescript/tests/cases/conformance/classes/members/privateNames/privateNameInInExpressionTransform.ts:29:9]
28 │ invalidLHS(v: any) {
29 │ 'prop' in v = 10;
· ───────────
30 │ #field in v = 10;
× Unexpected private identifier
╭─[typescript/tests/cases/conformance/classes/members/privateNames/privateNameInInExpressionTransform.ts:20:14]
19 │
20 │ v << #field in v << v; // Good precedence (SyntaxError): (v << #field) in (v << v)
· ──────
21 │
╰────
× Unexpected token