mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
fix(parser): reject partially parenthesized mixed coalesce expressions (#26394)
The parser currently accepts mixed nullish and logical expressions when parentheses cover only the first operand of the logical expression: ```js (a) || b ?? c; a ?? (b) || c; (a) && b ?? c; a ?? (b) && c; ``` These expressions require a syntax error because `??` cannot be mixed directly with `||` or `&&`. Parentheses around `a` or `b` do not group the complete logical expression. This change reports the existing mixed-coalesce diagnostic for all four forms. Fully grouped operands remain valid: ```js (a || b) ?? c; a ?? (b || c); (a && b) ?? c; a ?? (b && c); ``` ### Why the existing check misses these expressions `parse_binary_expression_rest` used `lhs_parenthesized` and `rhs_parenthesized` flags captured before parsing an operand. Those flags describe whether the initial token is `(`, but the parser can subsequently accumulate a larger logical expression outside those parentheses. Applying the initial flag to that complete expression incorrectly suppresses the diagnostic. ### Complete-operand checks Check both operands independently for an `And` or `Or` logical expression whose span starts at the operand's starting position. A logical expression accumulated outside initial parentheses starts at that position and must be rejected. Checking both operands also prevents a grouped right operand from hiding an invalid left operand. This works with either `preserve_parens` setting. When parentheses are preserved, a fully grouped operand is a `ParenthesizedExpression`. When they are omitted, its inner logical expression retains a span beginning after the opening `(`, so its start differs from the operand start and it remains allowed. For `(a) || b`, the complete logical expression instead starts at the opening `(` and is correctly rejected.
This commit is contained in:
@@ -1391,7 +1391,7 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
}
|
||||
|
||||
self.bump_any(); // bump operator
|
||||
let rhs_parenthesized = self.at(Kind::LParen);
|
||||
let rhs_start = self.cur_start();
|
||||
let rhs = self.parse_binary_expression_or_higher(left_precedence);
|
||||
|
||||
lhs = if kind.is_logical_operator() {
|
||||
@@ -1399,18 +1399,13 @@ impl<'a, C: Config> ParserImpl<'a, C> {
|
||||
let op = map_logical_operator(kind);
|
||||
// check mixed coalesce
|
||||
if op == LogicalOperator::Coalesce {
|
||||
let mut maybe_mixed_coalesce_expr = None;
|
||||
if let Expression::LogicalExpression(rhs) = &rhs {
|
||||
if !rhs_parenthesized {
|
||||
maybe_mixed_coalesce_expr = Some(rhs);
|
||||
}
|
||||
} else if let Expression::LogicalExpression(lhs) = &lhs
|
||||
&& !lhs_parenthesized
|
||||
{
|
||||
maybe_mixed_coalesce_expr = Some(lhs);
|
||||
}
|
||||
if let Some(expr) = maybe_mixed_coalesce_expr
|
||||
&& matches!(expr.operator, LogicalOperator::And | LogicalOperator::Or)
|
||||
let is_unparenthesized_logical = |expr: &Expression<'a>, start: u32| {
|
||||
matches!(expr, Expression::LogicalExpression(expr)
|
||||
if expr.span.start == start
|
||||
&& matches!(expr.operator, LogicalOperator::And | LogicalOperator::Or))
|
||||
};
|
||||
if is_unparenthesized_logical(&lhs, lhs_start)
|
||||
|| is_unparenthesized_logical(&rhs, rhs_start)
|
||||
{
|
||||
self.error(diagnostics::mixed_coalesce(span));
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
a && b ?? (c);
|
||||
@@ -0,0 +1 @@
|
||||
a || b ?? (c);
|
||||
@@ -0,0 +1 @@
|
||||
a ?? b && (c);
|
||||
@@ -0,0 +1 @@
|
||||
a ?? b || (c);
|
||||
@@ -0,0 +1 @@
|
||||
(a) && b ?? c;
|
||||
@@ -0,0 +1 @@
|
||||
(a) || b ?? c;
|
||||
@@ -0,0 +1 @@
|
||||
a ?? (b) && c;
|
||||
@@ -0,0 +1 @@
|
||||
a ?? (b) || c;
|
||||
@@ -0,0 +1,2 @@
|
||||
a && b || (c);
|
||||
a || b && (c);
|
||||
@@ -1,3 +1,3 @@
|
||||
codegen_misc Summary:
|
||||
AST Parsed : 84/84 (100.00%)
|
||||
Positive Passed: 84/84 (100.00%)
|
||||
AST Parsed : 85/85 (100.00%)
|
||||
Positive Passed: 85/85 (100.00%)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
formatter_misc Summary:
|
||||
AST Parsed : 84/84 (100.00%)
|
||||
Positive Passed: 84/84 (100.00%)
|
||||
AST Parsed : 85/85 (100.00%)
|
||||
Positive Passed: 85/85 (100.00%)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
lexer_misc Summary:
|
||||
AST Parsed : 261/261 (100.00%)
|
||||
Positive Passed: 261/261 (100.00%)
|
||||
AST Parsed : 270/270 (100.00%)
|
||||
Positive Passed: 270/270 (100.00%)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
parser_misc Summary:
|
||||
AST Parsed : 84/84 (100.00%)
|
||||
Positive Passed: 84/84 (100.00%)
|
||||
Negative Passed: 177/177 (100.00%)
|
||||
AST Parsed : 85/85 (100.00%)
|
||||
Positive Passed: 85/85 (100.00%)
|
||||
Negative Passed: 185/185 (100.00%)
|
||||
|
||||
× Cannot assign to 'arguments' in strict mode
|
||||
╭─[misc/fail/arguments-eval-ambient.ts:2:13]
|
||||
@@ -201,6 +201,62 @@ Negative Passed: 177/177 (100.00%)
|
||||
· ╰── `;` expected
|
||||
╰────
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-final-left-and.js:1:1]
|
||||
1 │ a && b ?? (c);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-final-left-or.js:1:1]
|
||||
1 │ a || b ?? (c);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-final-right-and.js:1:1]
|
||||
1 │ a ?? b && (c);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-final-right-or.js:1:1]
|
||||
1 │ a ?? b || (c);
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-left-and.js:1:1]
|
||||
1 │ (a) && b ?? c;
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-left-or.js:1:1]
|
||||
1 │ (a) || b ?? c;
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-right-and.js:1:1]
|
||||
1 │ a ?? (b) && c;
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Logical expressions and coalesce expressions cannot be mixed
|
||||
╭─[misc/fail/coalesce-partial-parens-right-or.js:1:1]
|
||||
1 │ a ?? (b) || c;
|
||||
· ─────────────
|
||||
╰────
|
||||
help: Wrap either expression by parentheses
|
||||
|
||||
× Cannot use export statement outside a module
|
||||
╭─[misc/fail/commonjs-export-statement.cjs:2:1]
|
||||
1 │ // CommonJS does NOT allow export statements (use module.exports instead)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
semantic_misc Summary:
|
||||
AST Parsed : 84/84 (100.00%)
|
||||
Positive Passed: 80/84 (95.24%)
|
||||
AST Parsed : 85/85 (100.00%)
|
||||
Positive Passed: 81/85 (95.29%)
|
||||
semantic Error: tasks/coverage/misc/pass/declare-let-private.ts
|
||||
Bindings mismatch:
|
||||
after transform: ScopeId(0): ["private"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
transformer_misc Summary:
|
||||
AST Parsed : 84/84 (100.00%)
|
||||
Positive Passed: 84/84 (100.00%)
|
||||
AST Parsed : 85/85 (100.00%)
|
||||
Positive Passed: 85/85 (100.00%)
|
||||
|
||||
Reference in New Issue
Block a user