Commit Graph

14525 Commits

Author SHA1 Message Date
camc314 c8e5fa7656 fix(parser): allow escaped type names in import and export specifiers (#26487)
Allow escaped value names such as `import { t\u0079pe as x } from "m"` and the equivalent named exports. Reject escapes only after resolving `type` as a type-only modifier.

Preserve the decoded identifier name in the ambiguous `type as as` form so its AST name remains `type`.
2026-09-09 13:22:34 +00:00
camc314 2dcee2f216 fix(parser): reject async modifiers on class fields (#26486)
`class C { async x; }` was accepted in JavaScript and TypeScript, silently discarding `async` from the resulting field. Reject the modifier after parsing a class field, once fields have been distinguished from methods.

Async methods, fields named `async`, and newline-separated fields remain valid.
2026-09-09 13:04:01 +00:00
camc314 9b1aff8c0c perf(linter/unicorn/no-useless-undefined): skip calls without trailing undefined (#26481)
Return early for calls whose last argument is not `undefined`, avoiding unnecessary callee and `bind` checks. Collect trailing undefined spans with pushes followed by one reversal instead of repeated front insertions, preserving diagnostic and fix order.
2026-09-09 12:48:10 +00:00
camc314 973d58e128 fix(parser): require comma after TypeScript this parameter (#26480)
Require a comma after a TypeScript `this` parameter when the parameter list continues. Previously, malformed syntax such as `function f(this: T x: T) {}` parsed without a diagnostic.
2026-09-09 12:26:51 +00:00
overlookmotel 88872b75ab refactor(lexer): add bm_clear function to clear a bitmap bit (#26478)
Pure refactor.

Add `bm_clear` function that clears a bit in a bitmap at specified index. This is the converse of `bm_set`, and single-bit version of `bm_clear_range`.

This operation is performed in various places across `classify`, `carve`, and `coalesce` stages. Replace the duplicated code with `bm_clear` calls. This makes the logic easier to read.

`bm_clear` is `#[inline(always)]` so this change makes zero difference to codegen. The changes in `carve_jsx` look more substantive, but compiling to ASM and diffing it before/after this PR shows 0 difference. The compiler is able to identify the repeated terms across the 4 calls - there's no need to perform that optimization manually.
2026-09-09 11:50:32 +00:00
overlookmotel 54aab0ce3d refactor(lexer): rename bm_set1 to bm_set (#26477)
Pure refactor. Just rename a function.

The `1` in `bm_set1` was unnecessary - "set" means set bit to 1, "clear" means unset bit to 0.
2026-09-09 11:50:31 +00:00
overlookmotel 3f0318739f refactor(lexer): deduplicate bitmap access code (#26461)
Code to read the nth bit of a bitmap was duplicated in several places across pipeline code:

- `wordbit` in `carve`
- `bit_at` in `regex_div`
- Various places inline in `carve`, `classify`, and `coalesce`

Add a function `bm_get` for this purpose to `bitmap` module (which feels like its natural home) and replace all usages with calls to this function.

This reduces repetition, and makes the call sites easier to read.

`bm_get` is marked `#[inline(always)]`, so extracting it into a function should have no effect on perf.
2026-09-09 08:12:06 +00:00
overlookmotel 2de2e0e578 docs(lexer): document bitmap functions (#26453)
Docs-only change. Add doc comments to all functions in `bitmap` module.
2026-09-09 08:12:06 +00:00
overlookmotel 639b7fe8eb refactor(lexer): re-order functions in bitmap module (#26452)
Pure refactor. No code changes, just moving code around.

Re-order the functions in `bitmap` module, moving `bm_next0` up to the top with the other getters.

Add line breaks between all the functions.
2026-09-09 08:12:06 +00:00
overlookmotel 6b242bae30 refactor(lexer): move write_sentinels into root of compress module (#26444)
Pure refactor. No code changes, just moving code around.

After #26443, main exported logic for `compress` stage is in `compress/mod.rs`.

`write_sentinels` is also exported from the module, so move it out of `common.rs` and into `mod.rs`, alongside `compress`. `common.rs` not only contains functions internal to the `compress` stage, shared between AVX2 and generic implementations.
2026-09-09 08:12:05 +00:00
overlookmotel 24bbc4fc1e refactor(lexer): move compress logic out of lex_raw (#26443)
Pure refactor. No code changes, just moving code around.

`compress` stage of pipeline involves several separate steps. Previously this compress-specific logic was in `lex_raw`, mixed with the logic for other stages.

This PR moves it into the `compress` module, so all that remains in `lex_raw` is a single call to `compress`.

The function that was previously called `compress` is renamed to `compress_blocks` to avoid duplicated names.
2026-09-09 08:12:05 +00:00
overlookmotel b1d2656f7d refactor(lexer): split compress pipeline stage into multiple files (#26437)
Pure refactor. No code changes, just moving code around.

Split `compress` pipeline stage into multiple files.

- `avx2.rs` / `generic.rs` - SIMD and generic impls.
- `common.rs` - Code shared between both impls.
- `mod.rs` - Minimal entry point, chooses which impl to load dependent on target.

Additionally, apply the same kinds of stylistic changes as #26390.
2026-09-09 08:12:04 +00:00
overlookmotel c2c2f2af5e refactor(lexer): group coalesce pipeline stage code into directory (#26422)
Pure refactor. No code changes, just moving code around.

Move all code for `coalesce` pipeline stage into a directory together.

`coalesce/mod.rs` contains same code as `coalesce.rs` did previously. `keyword.rs` moves into the same directory, since `kw_verify_batch` is only used in `coalesce` stage. `kw_flush` moves into `keyword.rs` as it's keyword-related.

Additionally, apply the same kinds of stylistic changes as #26390.
2026-09-09 08:12:04 +00:00
overlookmotel 6744ab8a3a refactor(lexer): move carve implementation selection out of lex_raw (#26417)
Pure refactor.

`carve` has 2 different implementations for JS and JSX. Move selecting which to use to within the `carve` module, to make `lex_raw` clearer to read.

`carve` function is marked `#[inline]` so this should make no difference whatsoever to codegen.
2026-09-09 08:12:03 +00:00
overlookmotel 9cc095c0f3 refactor(lexer): split carve pipeline stage into multiple files (#26416)
Pure refactor. No code changes, just moving code around.

Split `carve` pipeline stage into multiple files.

Unlike #26390, here there's little platform-specific code to split up. Aim is just to separate out code which is specific to JS / JSX, and split JSX code up into separate sub-systems.

Additionally, apply the same kinds of stylistic changes as #26390.
2026-09-09 08:12:03 +00:00
overlookmotel 3e8114a279 refactor(lexer): split classify pipeline stage into multiple files (#26390)
Pure refactor. No code changes, just moving code around.

Split `classify` pipeline stage into multiple files.

- `avx2.rs` / `generic.rs` - SIMD and generic impls.
- `common.rs` - Code shared between both impls.
- `mod.rs` - Minimal entry point, chooses which impl to load dependent on target.

Additionally, a few stylistic changes in Oxc "house style":

- Re-order code so it reads top-to-bottom - imports and consts first, then entry points, private fns/helpers lower down.
- Combine imports into a single `use` statements.
- Line breaks between functions.

The split of SIMD/generic into separate files is the main point. This is a precursor to switching over to runtime feature detection, instead of compile-time.
2026-09-09 08:12:02 +00:00
leaysgur 9dbddcaf48 fix(formatter): keep group flat for own-line comment in JSX (#26475)
Fixes #26466
2026-09-09 07:40:23 +00:00
leaysgur f579ec235c test(formatter_json): run json-test-suite snippets (#26473)
Follow up on https://github.com/oxc-project/oxc/pull/26469 for JSON.
2026-09-09 06:40:18 +00:00
leaysgur d226e6f43c fix(formatter_yaml): bump oxc-yaml-parser to 0.0.6 (#26472)
> https://github.com/oxc-project/oxc-yaml-parser/releases/tag/oxc-yaml-parser-v0.0.6

Fixed scanner bugs to parse more files.
2026-09-09 06:40:18 +00:00
leaysgur 8b41fc0904 fix(formatter_yaml): clamp tab_width: 0 to 1 to keep semantics (#26471)
`tabWidth: 0` is valid for many languages like:

```js
const a = {
k: "0 indent!"
}
```

But not for YAML.
2026-09-09 06:40:17 +00:00
leaysgur 4846f8d37d test(formatter_yaml): adjudicate the snippet-suite failures (#26470)
Follow up on #26469 for YAML.
2026-09-09 06:40:17 +00:00
leaysgur dc45cc3403 feat(formatter_test): test Prettier dynamic snippet tests (#26469)
Enable Prettier's dynamic snippet tests with `xxx-test-suite` module.

They do not have physical file, so need to slice during conformance run.
2026-09-09 06:40:17 +00:00
leaysgur dea2ba0f98 fix(formatter_core): do not panic with align(0) (#26468)
Prevent YAML formatter panic in certain case. And also supports `tabWidth: 0` as valid case.
2026-09-09 06:40:16 +00:00
leaysgur bf1d3f60bd fix(formatter): check this_param for simple parameters test (#26474)
Fixes #26465, also fixes test-call does not count `this` params as well.
2026-09-09 06:33:58 +00:00
Armano 80a76a0a2b feat(minifier): negate binary comparison for typeof x < 'u' (#26367)
Allow inversion of binary operation when comparing `'u'` string with
`typeof

`!(typeof a < "u")` => `typeof a > "u"`
`!(typeof a > "u")` => `typeof a < "u"`

this is needed to support negation of short version of `typeof x ===
"undefined"` -> `typeof a > "u"` generated by
`substitute_typeof_undefined`

fixes #25294
related #26460
2026-09-09 09:19:02 +08:00
camc314 d198982c08 perf(codegen): outline postfix source mapping work (#26450)
Keep the precedence and source-map availability checks in an inline wrapper, and move the remaining postfix mapping work into an `#[inline(never)]` helper. This keeps the call sites unchanged while separating the mapping work from the inline path.
2026-09-08 15:30:31 +00:00
camc314 1e5d5d7962 perf(linter/eslint/no-unused-vars): skip absent global declaration checks (#26457)
Skip the scope-ancestor walk in `is_in_declare_global` when the file contains no `TSGlobalDeclaration` nodes. Files containing global declarations retain the existing scope checks, using `nodes().kind()` directly when inspecting ambient module scopes.
2026-09-08 15:11:33 +00:00
camc314 403f899aba perf(linter/eslint/no-undef): check globals once per unresolved name (#26456)
Check configured globals once per unresolved name instead of once per reference. Names with only type references skip the global lookup, while undefined value references retain their individual `typeof` and function-scope `arguments` checks.
2026-09-08 15:03:04 +00:00
camc314 118adb0ea2 perf(linter/eslint/no-restricted-imports): avoid redundant import scans (#26455)
Skip the rule when neither paths nor patterns are configured. For side-effect import detection, build a set of import statement spans once instead of scanning every import entry for each request, and return early when the file has no requested modules.
2026-09-08 14:58:24 +00:00
camc314 dbe4fac500 perf(react-compiler): check component parameters before scanning bodies (#26451)
Check component parameter validity before scanning for hooks or JSX. Invalid parameter lists now short-circuit the body scan without changing component classification.
2026-09-08 13:44:15 +00:00
camc314 ab848f2f20 perf(linter/eslint/no-useless-assignment): skip tracking exported symbols (#26449)
Skip exported symbols before collecting their operations and assigning liveness bitset indices. These symbols were already excluded from diagnostics, so tracking them added reference scans and dataflow work without producing reports. Local symbols referenced inside exported declarations and assignments continue to be analyzed independently.
2026-09-08 13:38:08 +00:00
camc314 1b6e4098d8 perf(linter/unicorn/prefer-object-from-entries): avoid allocating configured paths (#26448)
Compare configured function paths directly through their split iterators instead of collecting a temporary vector for each candidate call. The shared matcher accepts iterators while preserving exact segment and length matching; its other caller, `no-array-method-this-argument`, is adapted to the new signature.
2026-09-08 13:32:14 +00:00
camc314 2206875def perf(linter/eslint/new-cap): avoid unnecessary callee name allocations (#26447)
Check the terminal callee name's capitalization before constructing its full property path, so ordinary calls and constructors return early. Borrow terminal names directly from the AST to avoid copying identifiers and computed property names; full paths are still constructed when exception matching requires them.
2026-09-08 13:26:11 +00:00
camc314 53f006e5fb perf(ecmascript): format small integer literals with itoa (#26446)
Format non-negative integers below 1000 directly with `itoa`, avoiding Dragonbox formatting and the intermediate `InlineString` copy. Larger numbers and fractions retain the existing shortest-literal logic.
2026-09-08 13:19:40 +00:00
camc314 8bfb8c0aad perf(codegen): avoid duplicate sourcemap name lookups (#26441)
Use `HashMap::entry` to reuse the lookup when inserting a new source-map name, avoiding the second hash and table search from `get` followed by `insert`. Existing name IDs and insertion order are preserved.
2026-09-08 12:31:30 +00:00
Ari Perkkiö b927e0a01f docs(linter/typescript/await-thenable): add missing lang for markdown doc comment (#26438)
Noticed that examples in
https://oxc.rs/docs/guide/usage/linter/rules/typescript/await-thenable.html#examples
don't have syntax highlighting.

Signed-off-by: Ari Perkkiö <ari.perkkio@gmail.com>
2026-09-08 09:45:26 +00:00
leaysgur a7f02a320c refactor(formatter): share the gap-segment walk and cursor helpers in Comments (#26433)
Pure refactoring;
2026-09-08 06:02:03 +00:00
leaysgur 44694ddc43 refactor(formatter): group Comments methods by cursor-based, position-based, and state (#26432)
Just reordering `impl` methods, and also update comments.
2026-09-08 06:02:02 +00:00
leaysgur 70918a3448 refactor(formatter): make is_cast_target position-based (#26431)
We use `preserve_parens: false`, need to detect typecast `()` from source, always.
2026-09-08 06:02:02 +00:00
leaysgur b26bf56cd5 refactor(formatter): route every assignment RHS shape rule through a cast-aware accessor (#26430)
Follow up on #26429, apply cast-aware behavior to assignment call sites.
2026-09-08 06:02:01 +00:00
leaysgur 33cdc841ac fix(formatter): keep cast-target assignment and commented parameter patterns (#26429)
Fixes #26425, #26426.

- Exclude cast target from never-break check
- Use position based comment check
2026-09-08 06:02:01 +00:00
camc314 32d00c55bc fix(codegen): preserve instantiation expression precedence (#26424)
Preserve parentheses around TypeScript instantiation expressions in postfix contexts. Previously, `(f<T>).x` and `(f<T>)[x]` were emitted as `f<T>.x` and `f<T>[x]`, producing invalid syntax or changing how the expression is parsed.

The printer now respects the surrounding precedence while retaining grouping for lower-precedence operands.
2026-09-07 18:20:11 +00:00
camc314 cfa47ab396 fix(parser): allow in expressions in class static blocks (#26423)
Class static blocks inherit the disabled `in` context of a surrounding `for` initializer, causing valid code such as `for (class { static { a in b; } };;);` to report a missing semicolon at `in`. Enable `Context::In` while parsing the static block, with the existing context helper restoring the surrounding flags afterward.
2026-09-07 18:06:57 +00:00
camc314 d61e3bf887 fix(parser): validate TS named tuple rest elements (#26419)
## Problem

Named tuple rest elements bypass the parser's existing checks for optional elements after an array rest and for multiple array rests. For example, both of these declarations currently parse successfully even though TypeScript rejects them:

```ts
type OptionalAfterRest = [...a: string[], b?: number];
type RestAfterRest = [...a: string[], ...b: number[]];
```

The equivalent unnamed forms already produce TS1266 and TS1265, respectively. Mixed named and unnamed array rests have the same gap when either rest is named.

## Cause and change

`parse_tuple_type` inspects `TSRestType.type_annotation` to decide whether an element is an array rest. For a named element, that annotation is a `TSNamedTupleMember`, and the actual type is stored in its `element_type`. Matching directly on the outer annotation therefore skips the array check and fails to track the rest element.

Unwrap the named member with `as_ts_type()` before applying the existing classification. Named and unnamed elements then share the same validation and existing diagnostics:

- An optional element after an array rest reports TS1266.
- A second array rest reports TS1265, including mixed named and unnamed forms.

The diagnostic spans continue to use the complete tuple elements, including their labels. The change borrows the underlying type without changing the AST representation or introducing allocations.

## Preserved behavior

A rest marker alone does not make a spread an unbounded array. Fixed-tuple and generic variadic spreads continue to be accepted, including:

```ts
type FixedBeforeOptional = [...a: [string], b?: number];
type RestBeforeFixed = [...a: string[], ...b: [number]];
type VariadicBeforeOptional<T extends unknown[]> = [...a: T, b?: number];
type RestBeforeVariadic<T extends unknown[]> = [...a: string[], ...b: T];
type MultipleVariadic<T extends unknown[], U extends unknown[]> = [...a: T, ...b: U];
```

This change preserves the existing type-reference classification; it does not add type resolution or change how shadowed `Array` references are handled.
2026-09-07 17:43:20 +00:00
camc314 ae6c386ed5 fix(codegen): preserve in restriction through yield arguments (#26413)
## Problem

A `yield` expression in a classic `for` loop initializer can lose the parentheses required around an `in` operator in its argument. This turns valid JavaScript into output that cannot be parsed, in both default and minified code generation.

For example, this input is valid:

```js
function *g(o) {
  for (yield (1 in o); false;);
}
```

Previously, codegen emitted:

```js
// Default output
function* g(o) {
  for (yield 1 in o; false;);
}

// Minified output
function*g(o){for(yield 1 in o;false;);}
```

The unparenthesized `in` is not allowed at this position in a classic `for` initializer. The same restriction applies to the argument of `yield*`, and can also be lost through nested yields or an assignment inside the yield argument.

## Change

`ForStatement` already prints its initializer with `Context::FORBID_IN`. However, `YieldExpression::gen_expr` ignored its incoming context and always printed its argument with `Context::empty()`.

Preserve `FORBID_IN` when printing the argument of an unwrapped yield expression. This lets the existing expression printers retain the required grouping:

```js
// Default output
function* g(o) {
  for (yield (1 in o); false;);
}

// Minified output
function*g(o){for(yield(1 in o);false;);}
```

Only the relevant `FORBID_IN` flag is forwarded; the argument's existing precedence remains `Precedence::Yield`. Both `yield` and `yield*` use this path.

## Parentheses around yield

When precedence requires parentheses around the entire yield expression, those parentheses already allow `in` in the argument. Clear the restriction in that case to avoid redundant inner grouping:

```js
// Input
function *g(o) {
  for ((yield (1 in o)) + 1; false;);
}

// Default output
function* g(o) {
  for ((yield 1 in o) + 1; false;);
}

// Minified output
function*g(o){for((yield 1 in o)+1;false;);}
```

Outside a restricted initializer, ordinary yield expressions continue to omit unnecessary parentheses: `yield (1 in o)` prints as `yield 1 in o`.
2026-09-07 17:16:18 +00:00
camc314 42ac916b21 fix(codegen): preserve private-in right operand precedence (#26411)
Private-in expressions can lose required parentheses around their right-hand operand in both default and minified code generation. When the operand is another relational expression, removing the grouping changes which value the private-field check receives and can change observable behavior.

For example:

```js
class D {
  #x;
  test(a, b) {
    return #x in (a instanceof b);
  }
}

new D().test({}, Object);
```

The original throws a `TypeError`: `a instanceof b` evaluates to `true`, and the private-field check requires an object as its right-hand operand. Previously, codegen emitted:

```js
return #x in a instanceof b;
```

Relational operators associate to the left, so this is parsed as `(#x in a) instanceof b`. The private-field check on `{}` produces `false`, and `false instanceof Object` then returns `false` instead of throwing.

The corrected output preserves the grouping in both modes:

```js
// Default output
return #x in (a instanceof b);

// Minified output
return#x in (a instanceof b)
```

`PrivateInExpression::gen_expr` now prints its right-hand operand with `Precedence::Compare` instead of `Precedence::Equals`. The printer wraps expressions whose precedence is equal to or lower than the requested precedence. Using `Compare` therefore preserves parentheses around relational operands, including `<`, `<=`, `>`, `>=`, `instanceof`, and nested private-in expressions. Equality and lower-precedence expressions continue to retain their required grouping, while shift, arithmetic, and exponentiation operands can still omit unnecessary parentheses.

This addresses the RHS grouping issue separately from #26383, which fixed the precedence passed to private-in expressions appearing as the left operand of another binary expression. The change is confined to the Rust `oxc_codegen` crate.
2026-09-07 17:10:19 +00:00
camc314 10521b288f fix(parser): allow escaped type default import bindings (#26409)
TypeScript allows a default import binding named `type` to contain Unicode escapes. Oxc currently reports `Keywords cannot contain escape characters` for these valid imports:

```ts
import t\u0079pe from "m";
import t\u0079pe, { x } from "m";
import t\u0079pe, * as ns from "m";
```

In each example, `t\u0079pe` is an identifier whose decoded name is `type`, and the declaration is a value import.

The parser initially recognizes the decoded `type` token before it has enough context to distinguish a default binding from a type-only import modifier. The escape check ran at that point, so it rejected the binding even when the following tokens established that this was a value import.

Move the escape check into the two branches that interpret `type` as a modifier: before a named or namespace import, and before another binding identifier after ruling out `import type from "m"`. The latter check also runs before the early return for a type-only import-equals declaration.

Escaped modifiers remain invalid, including these forms:

```ts
import t\u0079pe Foo = require("m");
import t\u0079pe { x } from "m";
import t\u0079pe * as ns from "m";
import { t\u0079pe Bar } from "m";
```

The change is limited to when the declaration-level escape diagnostic is emitted; import classification and specifier parsing retain their existing behavior.
2026-09-07 16:03:31 +00:00
camc314 72cb5e3539 fix(parser): reject rest parameters in getters (#26400)
## Summary

Getter accessors must have no formal parameters, but Oxc currently accepts a getter whose only parameter is a rest parameter. This affects class and object getters, as well as getter signatures in TypeScript interfaces and type literals.

```ts
class C { get x(...args) {} }
({ get x(...args) {} });
interface I { get x(...args: any[]): string }
type T = { get x(...args: any[]): string };
```

Each declaration above previously parsed without a diagnostic. It now reports the existing error: `A 'get' accessor must not have any formal parameters.`

## Cause and correction

`FormalParameters` stores ordinary parameters in `items` and the rest parameter separately in `rest`. Both getter checks inspected only whether `items` was empty, so a rest-only parameter list looked like a zero-parameter list.

Use `parameters_count() != 0` in both checks so the count includes the rest parameter. The shared `check_getter` path covers class and object getters; `parse_getter_setter_signature_member` covers interface and type-literal getter signatures.

The diagnostic still points at the full parameter list. Zero-parameter getters remain valid, ordinary getter parameters retain their existing diagnostic, and class/object accessors with type parameters retain the current type-parameter diagnostic precedence. Setter validation is unchanged.
2026-09-07 15:34:10 +00:00
camc314 4e76602fdd fix(parser): allow in in arrow block bodies within for initializers (#26395)
## Summary

Allow the `in` operator inside an arrow function’s block body when the arrow appears in a `for` initializer:

```js
for (() => { a in b; };;);
```

This is valid JavaScript, but the parser previously reported a missing semicolon at `in`. The block now parses successfully, including async arrows and arrows with a single unparenthesized parameter.

## Cause and fix

A `for` initializer disables `Context::In` so that an unparenthesized `in` can delimit a `for…in` statement. Arrow block bodies inherited that disabled context through `parse_arrow_function_expression_body`, which called `parse_function_body` directly.

The block-body branch now calls `parse_function_body` through `context_add(Context::In, ...)`. This enables `in` throughout the function body and restores the enclosing context afterward. It also permits nested concise arrows inside that block to use `in`.

The change is confined to block bodies because concise expression bodies inherit the surrounding grammar’s `In` parameter. For example, this remains invalid:

```js
for (var f = () => a in b;;);
```

Restoring the enclosing context also keeps this invalid, rather than accidentally treating `a in b` as a regular initializer expression after parsing the first arrow:

```js
for (var f = () => {}, value = a in b;;);
```
2026-09-07 15:07:31 +00:00
camc314 b20fc1950c 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.
2026-09-07 14:34:52 +00:00