mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/parser-grouped-expression-allocation
192 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3205cac297 |
refactor(react_compiler): use Scoping directly, drop the ScopeInfo conversion layer (#24095)
## Summary `oxc_react_compiler` materialized a full copy of semantic data per compiled file: `convert_scope_info` built a `ScopeInfo` of String-cloned names, four position-keyed hash maps (using `span.start` as a "node id"), and eagerly-precomputed Babel-style metadata. This was a Babel-bridge leftover — `ScopeInfo` turned out to be a 1:1, index-aligned re-encoding of `oxc_semantic`'s `Scoping`, read-only after construction, with every lookup replaceable by the semantic id cells (`reference_id` / `symbol_id` / `scope_id`) that `SemanticBuilder` already stamps on the AST the compiler walks. This PR deletes `convert_scope.rs` and the `ScopeInfo` tables and has the lowering pipeline read through `Semantic`/`Scoping` directly via a thin `ScopeResolver` view. It removes the per-file O(symbols + scopes + refs) copy (which ran in lint mode too), drops span-start-as-identity, and eliminates the per-function whole-program `reference_node_ids` copy (now built once per program). ## Approach Staged into four independently-verified commits: 1. **Remove dead `ScopeInfo` surface** — never-called API, the redundant `declaration_node_id` field (value-identical to `declaration_start`), a duplicated export-default block, and an ignored parameter. 2. **Add `ScopeResolver` + migrate the outer ring** — the resolver plus discovery, UID-name seeding, and `reference_node_ids`, while the old tables still fed lowering. 3. **Lower directly onto `Scoping`** — `HirBuilder` / `build_hir` / `find_context_identifiers` resolve identifiers through the AST id cells; binding identity becomes `SymbolId`, scope identity becomes oxc `ScopeId`; delete `convert_scope.rs`. 4. **Delete unreachable synthetic-node fallbacks** — the position-0 / zero-width paths that only existed for Babel-bridge inputs (every node from the pristine parse now has its cells populated). Reachable fallbacks (function redeclaration, duplicate var / sloppy params) are kept. This is a behavior-freeze refactor: output is **byte-identical** over the full 1805-fixture compiler corpus at every commit, and load-bearing quirks are preserved deliberately — the object-method alias windows for range checks, the first-declaration-only binding-identifier resolution gate, and the overwritten-shadow symbol reading as program scope. Net −829 LOC. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
b316c7ccf9 |
chore(clippy): enable more restriction lints (#24148)
## What
Enable six additional clippy `restriction` lints. All are at **zero** violations across the workspace today (except `unnecessary_self_imports`, whose two hits are fixed inline), so they're pure regression-prevention.
| Lint | Rationale |
|---|---|
| `empty_enum_variants_with_brackets` | Consistency — mirrors the already-enabled `empty_structs_with_brackets` |
| `format_push_string` | Perf: forbids the throwaway `String` alloc of `push_str(&format!(..))` |
| `fn_to_numeric_cast_any` | Correctness: casting a fn item/pointer to a number is almost always a missing call |
| `unnecessary_self_imports` | Drops redundant `use path::{self}` (2 fixes in `oxc_react_compiler`) |
| `try_err` | `return Err(x)?` obscures a plain `return Err(x.into())` |
| `error_impl_error` | Don't name a type `Error` when it implements `std::error::Error` |
Verified green with `cargo lint -- -D warnings` across the full workspace.
## Considered but skipped
A full sweep of the `restriction` group surfaced three higher-count lints that were evaluated and intentionally left off, because enabling them would require ~400 `#[expect]` annotations or degrade code rather than improve it:
- `missing_asserts_for_indexing` — fires on code already guarded by `&&` short-circuits; the suggested `assert!` is redundant and converts graceful `false` returns into panics.
- `iter_over_hash_type` — 242 sites; oxc uses `FxHashMap` (fixed-seed, deterministic iteration), so most are order-independent and would only collect `#[expect]`.
- `let_underscore_must_use` — majority are `let _ = write!/writeln!(..)` (the standard idiom for infallible `String` writes) plus deliberate result-ignores.
|
||
|
|
f13df97da3 |
fix(react_compiler): drop stray empty statement from catch bindings (#24133)
## Root cause
A `try`/`catch` with a catch parameter lowers the catch binding to a
`DeclareLocal` (and a `StoreLocal` in the handler) with
`InstructionKind::Catch`. In `emit_store` these codegen to an **empty
statement** — a faithful port of the fork's `emitStore` (`case
InstructionKind.Catch: return t.emptyStatement()`) — because the catch
parameter itself is emitted separately, from the `Try` terminal's
`handler_binding`, so the declaration has no textual form.
oxc's other instruction paths drop such empty statements
(`ox_codegen_instruction_nullable` filters `EmptyStatement` -> `None`),
but the `StoreLocal`/`DeclareLocal`/`DeclareContext`/`Destructure` path
returned the result of `ox_codegen_store_or_declare` **directly**, so
the catch declaration's empty statement was pushed into the block. It
surfaced as a stray `;`:
```js
bb0: {
x = [];
; // <- stray, from the catch `DeclareLocal`
try { ... } catch (t1) { ... }
}
```
and, when the `try` was optimized away entirely, as a bare `;` in the
function body (`let x; ; return 42;`). The reference goldens
(`*.expect.md`) never contain such a `;`.
## Fix
`crates/oxc_react_compiler/src/react_compiler_reactive_scopes/codegen_reactive_function.rs`:
1. **Drop the empty statement** from the store/declare path in
`ox_codegen_instruction_nullable`, consistent with the other instruction
paths.
2. Removing it turned a labeled block that wrapped only a `try` into a
single-statement block, which tripped the labeled-block single-statement
**unwrap** and dropped the braces (`bb0: try {}` instead of `bb0: { try
{} catch {} }`). Restrict the unwrap so a labeled `try` keeps its braces
— matching the reference output, which always brace-wraps a labeled
`try` (the catch declaration originally padded the block to two
statements). Other single statements (`if`/`for`/`switch`/`while`,
nested labels such as `bb1: bb2: switch`) still unwrap as before.
## Fixtures changed
20 snapshots updated, each removing exactly one stray `;` (braces and
everything else preserved). ~29 `try`/`catch` fixtures now match the
fork golden at the compiled-component level. The change also cleaned two
non-`try-catch`-named fixtures that embed a `try`
(`repro-unreachable-code-early-return-in-useMemo`,
`error.bug-invariant-local-or-context-references`). The whole-suite diff
vs `main` is 20 files, all `-1` line; no snapshot regressed.
## Remaining work (out of scope, pre-existing)
The still-diverging `try`/`catch` fixtures differ only by codegen
formatting / error handling unrelated to the stray `;`:
- Prettier-style formatting: inline vs multi-line object/array literals
and `FIXTURE_ENTRYPOINT` params, multi-line vs inline JSX
(`try-catch-multiple-value-blocks`, `try-catch-within-object-method*`),
directive-comment placement (`propagate-scope-deps-hir-fork__*`,
`repro-nested-try-catch-in-usememo`).
- `finally` clauses and invalid-JSX-in-`catch` fixtures (`error.todo*`,
`invalid-jsx-*`, `fault-tolerance__error.try-finally-*`) where oxc's
error/`## Error` behavior diverges from the fork.
|
||
|
|
cb2a5051af |
fix(react_compiler): codegen destructuring reassignment targets (#24131)
## Root cause
The reactive-function backend (`codegen_reactive_function.rs`) only knew
how to turn a plain `BindingIdentifier` lvalue into an
`AssignmentTarget`. Any **destructuring reassignment** to an existing
binding — `[x] = arr`, `({a} = obj)`, `({a} = {a:{}}), ([x] = [[]])` —
hit an unported `invariant_err("Destructuring reassignment targets are
not yet ported to oxc")`.
`codegen_function` deliberately swallows any emission error and falls
back to an **empty body** (a shim from the de-Babel batches). So every
function containing such an assignment silently compiled to `function
f() {}` — params and body both dropped — with **no diagnostic**. This is
a hard miscompile, not a whitespace diff.
## Fix
Port `ox_binding_pattern_to_assignment_target` to recursively reshape
`BindingPattern` → `AssignmentTarget`, since oxc (unlike Babel) models
declaration patterns and assignment targets as distinct node families.
Adds handling for:
- array patterns → `ArrayAssignmentTarget` (elements + holes + rest)
- object patterns → `ObjectAssignmentTarget`, distinguishing shorthand
(`{a}` / `{a = 1}` → `AssignmentTargetPropertyIdentifier`) from keyed
(`{a: b}` → `AssignmentTargetPropertyProperty`)
- rest elements (`...x`)
- nested patterns and defaults (`AssignmentTargetWithDefault`)
Mirrors Babel, which reuses the same pattern nodes as the assignment
LHS.
## Impact
23 corpus snapshots changed — **all move toward the fork golden, none
regress**; 14 now match the fork exactly. Verified old-vs-new token
distance to each fork golden.
Within this batch (`reduce-reactive-deps` /
`propagate-scope-deps-hir-fork`):
- `ssa-renaming-via-destructuring-with-mutation` — was empty function,
now matches the fork exactly.
- `ssa-renaming-ternary-destruction` /
`ssa-renaming-ternary-destruction-with-mutation` — were empty functions,
now compile with **matching dependency sets, cache keys, scope
boundaries, and dead-code elimination**; the only residual is
codegen-only (sequence-expression parenthesisation `([x]=[[]], …)` vs
`(([x]=[[]]), …)` and object-literal whitespace).
Other improved fixtures across the corpus (also previously
empty-bodied): `destructuring`, `destructuring-assignment`,
`destructuring-assignment-array-default`,
`destructure-direct-reassignment`, `destructure-in-branch-ssa`,
`destructuring-object-pattern-within-rest`,
`escape-analysis-destructured-rest-element`,
`call-args-destructuring-assignment`,
`array-pattern-spread-creates-array`, the `nonmutated-spread-*` set,
`repro-reassign-props`, `switch-with-only-default`,
`new-mutability__ssa-renaming-ternary-destruction`.
## Remaining work (out of scope here)
The rest of the batch's diffs are pure codegen and live in
`oxc_codegen`, not the dependency passes:
- sequence-expression parenthesisation of assignment operands (`(a=[],
b)` vs `((a=[]), b)`)
- JSX multi-line wrapping, TS type printing, object/array-literal
whitespace
The two `try-catch-*` fixtures (stray `;`) belong to a sibling
`fix/rc-try-catch` branch and are left untouched.
|
||
|
|
b82c394e2f |
fix(react_compiler): propagate codegen invariants instead of emitting empty bodies (#24128)
## Summary
This batch targets 5 fixtures that **compiled without error in oxc but
throw an internal `Invariant` upstream** — cases where oxc's output was
*silently wrong* (worse than upstream's loud failure).
### Root cause (shared by 4 of the 5)
`codegen_function` wrapped `ox_codegen_reactive_function` in a
scaffolding shim that fell back to an **empty function body on ANY
codegen error**. That turned genuine "this input cannot be compiled"
invariants into silent miscompiles: the whole body was replaced with
`{}` and applied to the program with no diagnostic.
The shim still had a legitimate job, though — a couple of codegen
sub-emitters aren't ported yet (`Destructuring reassignment targets`,
hook-guard wrapping), and upstream *does* compile those. Blindly
propagating every error would regress ~26 fixtures that currently emit
an (imperfect) empty body but would now spuriously error.
**Fix:** split the two error classes. A new
`CompilerError::unimplemented` flag (set via `unimplemented_err`) marks
the genuinely-unported sub-emitters; the shim keeps falling back to an
empty body only for those. Every other codegen error now propagates as a
diagnostic — matching upstream's `Invariant` — instead of emitting wrong
code. Net diff: only the 5 genuine-invariant fixtures change; the ~26
not-yet-ported fixtures keep their prior output (no regressions).
## Per-fixture status
| Fixture | Status | Notes |
| --- | --- | --- |
| `error.bug-invariant-unnamed-temporary` | **fixed → now-errors** |
matches upstream |
| `error.bug-invariant-codegen-methodcall` | **fixed → now-errors** |
matches upstream |
| `error.call-args-destructuring-asignment-complex` | **fixed →
now-errors** | matches upstream |
| `error.todo-nested-method-calls-lower-property-load-into-temporary` |
**fixed → now-errors** | matches upstream |
| `error.bug-invariant-local-or-context-references` | **fixed → correct
output** | oxc handles a case upstream crashes on |
Bonus: `error.todo-kitchensink` also flipped from a silent empty body
(masked behind 8 accumulated warnings) to the single upstream invariant
`Expected a variable declaration`.
### Details
**`error.bug-invariant-unnamed-temporary`** — `renderer={(...props) =>
<span {...props}>{displayValue}</span>}`
Was silently compiled with the renderer arrow dropped to `function
_temp() {}` (empty body). The codegen path hit `Expected temporaries to
be promoted to named identifiers in an earlier pass` (an unnamed
identifier reaching `convert_identifier`), which the shim swallowed. Now
the error propagates and the function is left unchanged — identical to
upstream's `Invariant`.
**`error.bug-invariant-codegen-methodcall`** — `Math.floor(diff.bar())`
Was silently compiled to `const YearsAndMonthsSince = () => {};` (entire
body dropped). `diff.bar`'s property had been promoted/lowered into an
`Identifier` so codegen's `MethodCall::property must be an unpromoted +
unmemoized MemberExpression` invariant fired (`Got: 'Identifier'`) and
was swallowed. Now propagates — matches upstream.
**`error.todo-nested-method-calls-lower-property-load-into-temporary`**
— `Math.max(2, items.push(5), ...other)`
Same `MethodCall::property` invariant as above (`items.push` property
lowered into a temporary), same silent empty-body drop. Now propagates —
matches upstream.
**`error.call-args-destructuring-asignment-complex`** — `x.foo(([[x]] =
makeObject()))`
Was silently compiled to `function Component() {}` (body dropped). The
complex destructuring assignment produced a const-declaration
instruction that is *also referenced as an expression*, tripping
codegen's `Const declaration cannot be referenced as an expression`
invariant (`this is Const`), which was swallowed. Now propagates —
matches upstream.
**`error.bug-invariant-local-or-context-references`** — `catch (err) {
setState(_prev => ({..., error: err})); }`
Upstream throws `Expected all references to a variable to be
consistently local or context references` (it mis-classifies a catch
binding captured by a lambda as a context variable even though `err` is
never reassigned). **oxc already lowers this correctly**: it renames the
catch param and aliases it — `catch (t0) { const err = t0; setState(...)
}` — so `err` stays a plain read-only local and no inconsistency arises.
Verified semantically equivalent to the source on both the try-success
and catch paths (identical `setState` call sequences, including error
propagation). No code change needed — oxc legitimately handles a case
upstream crashes on.
## Notes / still-open
- The 4 now-erroring fixtures are cases upstream *also* fails on (its
own passes emit these invariants). Producing *correct* memoized output
for them would require fixing the earlier promotion/SSA passes so
temporaries are named/unpromoted before codegen — upstream doesn't do
this either, so erroring cleanly is the correct, achievable resolution
here.
- `todo-kitchensink`'s oxc message is `Invariant: Expected a variable
declaration` (upstream adds a `Got ExpressionStatement.` description);
the reason matches, the description is a cosmetic gap.
|
||
|
|
e0e652cd6e |
test(react_compiler): render code for lint output-mode fixtures (#24132)
## Summary
Resolves the `effect-derived-computations/` fidelity batch (21 fixtures)
where oxc emitted `No changes.` while the fork produces `## Code`.
## Root cause
The batch header is `// @validateNoDerivedComputationsInEffects_exp
@loggerTestOnly @outputMode:"lint"`. The original hypothesis was that
oxc *aborts* on the `EffectDerivationsOfState` Error-severity diagnostic
(`panic_threshold: "all_errors"`). That is **not** what happens —
instrumenting `compile()` shows all 21 fixtures return
`CompileResult::Success { ast: None }`, never `Error`:
- `validate_no_derived_computations_in_effects_exp` returns its
findings, which the pipeline hands to `log_errors_as_events`
(non-fatal), not `handle_error`.
- In lint output mode `process_fn` returns `Ok(None)` (lint never
rewrites), so `transform` returns `program: None` / `changed: false`.
The real divergence is in the **snapshot harness**: it rendered any
no-rewrite result as `No changes.`, so *every* `@outputMode:"lint"`
fixture looked like a bail-out even though the compiler ran to
completion and reported its findings. Upstream's `snap` runner instead
emits the (unmodified) code in lint mode alongside the findings.
## Fix
- `tests/snapshot.rs`: in lint output mode, render the validated
program's code instead of `No changes.` (the program is unchanged,
matching the fork's lint `## Code`).
- `src/lib.rs`: re-export `CompilerOutputMode` so the harness detects
lint mode canonically via `CompilerOutputMode::from_opts`.
No compiler logic changes — hence the `test(...)` scope; the compiler
was already correct, the harness was misrepresenting lint output.
## Fixtures changed (46 lint-mode snapshots)
Verified each changed snapshot's code against the fork's `.expect.md`
with a whitespace/trailing-comma-insensitive comparison.
**effect-derived-computations batch (21/21 — this PR's target):** all
now match the fork's `## Code` and still emit the
`EffectDerivationsOfState` diagnostic. 18 match modulo whitespace; 3
differ only by JSX-return parenthesization (`return <div>` vs `return
(<div>)`), a pure oxc-codegen-vs-babel formatting difference.
**Other `@outputMode:"lint"` fixtures (25) — all improvements toward the
fork:**
- 21 (setState, static-components, jsx-in-try-with-catch, `valid-*`,
`use-memo-noemit`, `dynamic-gating-noemit`, `fn-name-no-leak`): now
match the fork's `## Code`.
- 2 (`valid-setState-in-effect-from-ref-function-call`,
`valid-setState-in-useEffect-controlled-by-ref-value`): match the fork's
`## Code` except for compiler variable-rename suffixes (`ref_0`,
`data_0`) that the fork applies to its lint output but oxc's harness
does not — still a clear improvement over `No changes.`.
- 2 (`error.todo-invalid-jsx-in-try-with-finally`,
`error.todo-invalid-jsx-in-catch-in-outer-try-with-finally`): the fork
throws (`## Error`) on `try`/`finally` without a `catch`; oxc handles
it, emitting a `Todo` **warning** and now showing the code. Pre-existing
behavior divergence (oxc is more lenient here), surfaced by this change.
The `git diff --stat` is confined to these 46 lint-mode snapshots plus
the two source files.
## Notes / follow-ups (out of this batch)
- `invalid-jsx-in-try-with-catch` and
`invalid-jsx-in-catch-in-outer-try-with-catch` internally still
`CompileResult::Error` (a separate fatal-in-lint-mode path in the
JSX-in-try lowering). Because lint mode never rewrites, their emitted
code + diagnostic still match the fork's `## Code`, but the latent abort
is worth fixing in the JSX-in-try batch.
- Applying lint-mode variable renames in the harness (the
`ref_0`/`data_0` gap) and oxc's `try`/`finally` handling vs the fork are
separate items.
|
||
|
|
4e9194fe57 |
fix(react_compiler): lower delete obj.prop to Property/ComputedDelete (#24123)
Closes Group C of the "oxc compiles where the fork errors" gap — deleting a property of a frozen or global value.
## Root cause
`build_hir` lowered `delete` on a member expression to a `Primitive(undefined)` no-op behind a leftover `TODO(stage1a-arms)`. So `delete obj.prop` was **silently dropped** — the deletion never reached the output, and it never produced a mutation effect. The downstream machinery was already in place: mutation inference emits `Mutate { object }` for `PropertyDelete`/`ComputedDelete` (`infer_mutation_aliasing_effects.rs:1953`) and codegen emits `delete ...` for them (`codegen_reactive_function.rs:2034`). Only the lowering was missing.
## Fix
Lower `delete obj.prop` → `PropertyDelete` and `delete obj[key]` → `ComputedDelete`, reusing the existing `lower_member_expression` helper (matches TS `BuildHIR`). Non-member `delete` keeps the prior no-op.
## Result (9 snapshots, all improvements)
- **3 error fixtures now error + decline** with `This value cannot be modified`, matching the fork: `error.invalid-delete-property-of-frozen-value`, `error.invalid-delete-computed-property-of-frozen-value`, `error.mutate-property-from-global`.
- **6 fixtures that compile with a `delete`** now emit `delete ...` instead of the silently-dropped `undefined` (e.g. `delete-property` now matches the fork exactly; `round3_effect_read_vs_capture_v2` — previously a mis-compile — now emits `delete profile[key]`).
Verified: full `cargo test -p oxc_react_compiler` (snapshot + 15 unit tests) green, `cargo clippy --all-features -D warnings` and `cargo fmt --check` clean.
|
||
|
|
8dd81f2e65 |
refactor(react_compiler): remove unused js options (#24065)
Removes unused upstream JS option fields from Oxc's React Compiler options and deletes the dead filename plumbing they carried through the compile environment. Stacked on #24063. AI-assisted. |
||
|
|
1fff15c499 |
refactor(react_compiler): borrow semantic in compile pipeline (#24063)
Borrow existing semantic data and caller allocator in React Compiler transform/lint paths to avoid hidden rebuild/allocation work. Transform now returns the compiled program so callers can install it after the semantic borrow ends. AI-assisted. |
||
|
|
53aaa0cab8 |
refactor(react_compiler): emit diagnostics via oxc_diagnostics, drop logger events (#24054)
Consolidates the React Compiler's error/warning reporting onto `oxc_diagnostics` and removes the JS-shim `LoggerEvent` machinery it no longer needs in the Rust-native integration. ### Before Compiler passes recorded internal `CompilerError`/`CompilerDiagnostic`, which were converted to serializable `LoggerEvent`s (`CompileError`, `CompileSuccess`, `CompileSkip`, `CompileUnexpectedThrow`, `PipelineError`) + a `CompilerErrorInfo` for the JS logger callback, then re-converted to `oxc_diagnostics` at the boundary. Nothing in the workspace read those events (`TransformResult::events` had no consumer), and `CompileSuccess`/`CompileSkip` never produced a diagnostic at all. ### After `ProgramContext` accumulates `OxcDiagnostic`s directly during compilation. Diagnostics are built the way the parser does it — `OxcDiagnostic::error(msg).with_label(span)` — with a single shared `#[cold] detail_to_diagnostic` converter (used by both the per-function error path and the lint/validation path); one-off messages are inlined at their call sites. ### Removed - `LoggerEvent` and the whole JS-serialization layer (`CompilerErrorInfo`, `CompilerErrorDetailInfo`, `LoggerSourceLocation`, `LoggerPosition`, `LoggerSuggestion*`, `CompilerErrorItemInfo`), `formatted_message`/`raw_message`. - `react_compiler_diagnostics/code_frame.rs` (~420 lines) — the JS error-string formatter, now redundant with `oxc_diagnostics` rendering. - `TransformResult::events` (dead public field); `FnSourceLoc`/`FnSourcePos` collapsed to `Option<Span>`. Net **−539 lines**. ### Behavior Output is unchanged for all exercised paths — the `oxc_linter` `react_compiler` snapshot is **byte-identical**. Two deliberate changes on untested paths: the redundant fatal-error *summary* diagnostic is dropped (per-detail diagnostics already cover it), and the lint path now attaches an `ErrorDetail`'s own span (snapshot confirms no observable change). ### Verified - Builds clean in both feature modes; clippy (`dev-no-debug-assertions`, incl. `--features debug`/`react_compiler`) and `cargo shear` clean. - `oxc_react_compiler` 15/15; `oxc_transformer` react_compiler integration 2/2; `oxc_linter` `react_compiler` snapshot unchanged. Draft: opening for review of the approach before finalizing. |
||
|
|
3cad48d67b |
refactor(react_compiler): gate HIR debug printing behind a debug feature (#24048)
Puts the HIR debug printer (`react_compiler::debug_print`, ~600 lines)
behind a new off-by-default `debug` cargo feature, so it isn't compiled
into normal builds.
### How
- `Cargo.toml`: add `[features]` with `debug = []`.
- `react_compiler/mod.rs`: `#[cfg(feature = "debug")]` on `pub mod
debug_print;`, plus a `#[cfg(not(feature = "debug"))]` no-op stub module
exposing the two entry points the pipeline calls (`debug_hir`,
`format_hir_function_into`).
The printer is threaded through the vendored `pipeline.rs` at ~70 `if
context.debug_enabled { … }` sites (plus a closure capture and a local
formatter fn that are required pass arguments). Gating each site would
scatter ~75 `#[cfg]` attributes through vendored code and fight
re-syncability, so gating the module with a stub keeps
`pipeline.rs`/`imports.rs` untouched while still compiling the printer
out by default.
### Behavior
No change to default builds: nothing in the workspace sets
`PluginOptions::debug`, so the printer was never triggered. To get HIR
dumps, build with `--features oxc_react_compiler/debug` and set
`PluginOptions::debug = true`.
### Verified
- Default build (stub) and `--features debug` (real printer) both
compile clean.
- `oxc_react_compiler` tests pass in both modes (15/15); clippy clean in
both.
- `oxc`, `oxc_linter`, `oxc_transformer` build with default features.
|
||
|
|
579f025c7c |
refactor(react_compiler): trim PluginOptions and remove timing instrumentation (#24042)
Slims down `oxc_react_compiler`'s `PluginOptions` and removes the now-dead timing infrastructure. Four related cleanups: ### Remove `source_code` from `PluginOptions` It was redundant — `compile` in `lib.rs` always overwrote it with `program.source_text`, and no caller set it. The one consumer (`ProgramContext::new` in `program.rs`) now reads `program.source_text` directly (the oxc `Program` is already in scope there), and the threading block in `lib.rs` is gone. ### `default_plugin_options()` → `impl Default for PluginOptions` Converted the free function into a manual `Default` impl (the defaults are non-trivial: `should_compile: true`, `target: "19"`, `flow_suppressions: true`, …). All callers now use `PluginOptions::default()`. ### Remove `profiling` The field was always `false` in practice (no caller set it). Its sole consumer now passes `TimingData::new(false)` — which leads into… ### Remove the timing instrumentation entirely With `profiling` gone, timing was pure, always-disabled instrumentation: every `context.timing.start/stop` was a no-op and the collected `Vec<TimingEntry>` was always empty and never read (`lib.rs` destructures `CompileResult` with `..`). Removed: - `timing.rs` (the `TimingData`/`TimingEntry` module) and its `mod` declaration - ~218 `context.timing.start/stop(...)` calls across `pipeline.rs` - the `timing` field on `ProgramContext` and on both `CompileResult` variants, plus all initializers ### Verification - `oxc_react_compiler`: builds, 15/15 tests pass. - `oxc_linter`: `react_compiler` rule snapshot test passes — **snapshot unchanged** (diagnostics identical). - `oxc_transformer` react_compiler integration: 2/2 pass; `oxc_benchmark` bench compiles. - rustc warnings, clippy (`dev-no-debug-assertions`), and `cargo shear` all clean. |
||
|
|
64683a7784 |
refactor(react_compiler): drop the oxc_parser dependency (#24038)
Removes `oxc_parser` from `oxc_react_compiler`'s library dependencies. The compiler is handed an already-parsed `Program` (via `transform`/`lint`), so the library itself no longer needs to parse anything. The only remaining `src/` use was in `codegen_reactive_function.rs`, when re-emitting a stored TS type annotation (on an `as`/`satisfies` cast) whose inner identifiers had binding renames applied. The old code sliced the original source, applied the renames as text edits, wrapped the result in `let __oxc_t = null as …;`, and **re-parsed** it. That path is replaced with a `clone_in` of the stored `TSType` followed by an in-place identifier rename via `VisitMut`. `clone_in` preserves spans, so the renames stay keyed by source offset — the exact same reference set that drove the text edits before. Both approaches rename the same identifiers (both are driven by the `visit_ts_type` walk plus the `reference_node_ids`/`renames` filter) and yield structurally identical ASTs, so codegen output is unchanged. It's also cheaper (no source slicing, no re-parse) and can no longer fail, so the helper's return type drops from `Option<TSType>` to `TSType` and the dead `None` fallback at the two call sites is removed. `oxc_parser` moves to `[dev-dependencies]` (the example and integration tests still parse source). Depends on the API in #24028 (already merged). |
||
|
|
f27bbc7760 |
refactor(react_compiler): remove transform_source and lint_source (#24028)
Remove the `transform_source` and `lint_source` convenience wrappers from `oxc_react_compiler`'s public API.
- `lint_source` had no callers anywhere in the workspace.
- `transform_source` (parse-then-`transform`) was only used by the crate's own integration tests, so it moves into `tests/transform.rs` as a private helper — the 17 call sites are unchanged.
- Dropped the now-unused `oxc_parser::Parser` import in `lib.rs` and narrowed `oxc_span::{GetSpan, SourceType}` to `GetSpan`.
`transform`, `lint`, and `LintResult` are kept — they remain in use by the benchmark/example (`transform`) and the `react/react_compiler` linter rule (`lint`).
|
||
|
|
3c2a0f0e39 |
refactor(react_compiler): drop the old AstBuilder compat shim (#24027)
## Summary Follow-up cleanup after the de-Babel (#24009). The ported codegen called deprecated inherent `AstBuilder` methods (`vec` / `alloc` / `expression_*` / `statement_*` / …), which the crate's `disable_old_builder` dev-dep flags for migration; a compat trait `OldBuilderExt` (`old_builder_ext.rs`) mirrored them on the new API to ease the port. This removes that shim: all ~300 call sites (`codegen_reactive_function.rs` + `program.rs`) now use the new `AstBuilder` API directly (`ArenaVec::*_in` / `ArenaBox::new_in` / `Type::new_*`), and `old_builder_ext.rs` (~1.7k lines) is deleted. The now-unused `oxc_str` dependency is dropped. ## Verification - No deprecated builder methods remain (`cargo check` reports 0 `use of deprecated`). - clippy `--all-features --all-targets -D warnings`: clean. - Differential over the 1,805 upstream React Compiler fixtures: byte-identical (`same=1805 diff=0`). - 15 integration tests pass; `cargo shear` / `cargo fmt --check` / `typos` clean. --- Produced with AI assistance (Claude Code); verified byte-identical against the 1,805-fixture differential and the crate's tests. |
||
|
|
269ce46cdb |
test(react_compiler): move end-to-end tests to tests/, drop internal unit tests (#24022)
## Summary Follow-up to #24009 (de-Babel). Now that `oxc_react_compiler` speaks the oxc AST natively, this tidies its test + dependency layout: - **Move the 15 end-to-end tests to `tests/transform.rs` as integration tests** — they exercise the crate through its public `transform_source` API instead of `#[cfg(test)]` modules inside `lib.rs`. - **Remove the internal unit tests** (function discovery, `JsString`, HIR `Environment`, codegen hashing, prefilter) — the crate is covered through its public API. - **Move `oxc_codegen` to `[dev-dependencies]`** — the library builds the oxc AST but never codegens it; only the example and integration tests print the program. Net −397 lines. ## Verification - `cargo test -p oxc_react_compiler`: 15 integration tests pass, 0 unit. - Differential over the 1,805 upstream React Compiler fixtures: byte-identical (`same=1805 diff=0`). - clippy `-D warnings`, `cargo fmt --check`, `typos`, `cargo shear`: clean. Example builds. --- Produced with AI assistance (Claude Code); verified byte-identical against the 1,805-fixture differential and the crate's tests. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> |
||
|
|
36088087f8 |
refactor(react_compiler): replace the vendored Babel AST with the oxc AST (#24009)
## Summary
`oxc_react_compiler` (the Rust port of the React Compiler) worked internally on a **vendored Babel-shaped AST**: the pipeline converted the oxc `Program` into that AST, compiled against it, then converted back to oxc for codegen. That cost ~6k lines of conversion glue plus a whole Babel-AST module, and it couldn't carry TypeScript type nodes through the IR (type annotations were serialized to JSON and re-parsed from source).
This PR removes the Babel AST entirely. The compiler now consumes the **oxc AST natively**: it lowers the oxc AST directly to the HIR, and codegen builds oxc via `AstBuilder` and splices the compiled functions into the `&mut Program` in place. Since oxc already models every TS type-annotation node, the HIR carries borrowed `&'a TSType<'a>` nodes directly instead of re-parsing.
```
oxc Program → convert_scope → discovery (oxc_ast_visit::Visit)
→ build_hir (oxc AST → HIR) → passes
→ codegen (oxc AstBuilder) → splice into &mut Program
```
## Commits
1. `RawNode` JSON blob → structured metadata; drop serde / serde_json
2. Front-end: lower the oxc AST → HIR directly (transitional span-bridge)
3. Back-end: codegen emits oxc via `AstBuilder` + splices in place; delete `convert_ast_reverse`
4. Delete the vendored `react_compiler_ast` + `convert_ast`; discovery walks the oxc `Program`
5. Thread `'a` through the HIR to carry `&'a TSType` / `&'a Statement` (no source-reparse)
Net **−13k lines** (71 files).
## Verification
Every commit is **byte-identical** to the pre-change compiler over the 1,805 upstream React Compiler fixtures (`same=1805 diff=0`), and `cargo test -p oxc_react_compiler` (38 tests) passes throughout. The public API (`transform(&mut Program)` / `lint` / `TransformResult`) is unchanged.
## Follow-ups (not in this PR)
- The codegen still calls deprecated `AstBuilder` methods (surfaced by `disable_old_builder`); they need migrating to the new builder interface (#23043) and the transitional `old_builder_ext.rs` shim removed. **CI `-D warnings` will flag these until then.**
- Optional ecosystem-fidelity fixes not covered by the fixture suite.
---
Produced with AI assistance (Claude Code); all output verified byte-identical against the 1,805-fixture differential and the crate's tests.
|
||
|
|
0c2dbe44b3 |
refactor(react_compiler): remove unnecessary AST enum conversions (#24003)
Follow-on after `AstBuilder` changes. Shorten code by removing unnecessary conversions. Instead of creating an `AssignmentTargetPattern` or `SimpleAssignmentTarget` and then converting to `AssignmentTarget` with `.into()`, just create an `AssignmentTarget` directly. |
||
|
|
987ce8ef14 |
refactor(react_compiler): move all imports to top, trim module allows (#23998)
Readability refactor of `oxc_react_compiler` — pure import relocation, no behavior change. **1. Trim module allows** — the 12 vendored-module `#[allow(clippy::all, dead_code, unused)]` become `#[allow(clippy::all)]`. `dead_code`/`unused` suppressed nothing: the modules are `pub`, and the few genuinely-dead private items keep their own targeted `#[allow(dead_code)]`. **2. Hoist function-local imports** — function-local `use` statements moved to the module top across the crate. **3. Shorten inline module paths** — ~1230 inline `crate::`/extern-crate module-path qualifiers in the code body replaced with top-level `use` imports + short names, so the code reads without long paths. Collisions are aliased, not shadowed: - AST vs HIR types under the `react_compiler_hir::*` glob (e.g. `BinaryOperator as AstBinaryOperator`) - `std::fmt::Result as FmtResult` — bare `Result` would shadow the prelude `Result<T, E>` and leak through the hir glob into sibling modules - `SourceType as AstSourceType` vs `oxc_span::SourceType` Doc-comment intra-doc links and a few paths disambiguating a same-named local function are intentionally left fully-qualified. |
||
|
|
81101e7749 |
refactor(react_compiler): transform takes &mut Program, lint takes &Program (#23995)
## Summary `transform` now rewrites the program **in place** instead of returning a new one, and `lint` stays **read-only** — aligning with how the rest of the oxc transform pipeline mutates the AST. - `transform(program: &mut Program, allocator, options)` writes the memoized AST back into `program`. `TransformResult` drops its `program: Option<Program>` field (and lifetime) and gains `changed: bool`. - `lint(program: &Program, options)` is unchanged — borrowed, read-only. - Both share a private `compile(&Program) -> (Option<Program>, Diagnostics, Vec<LoggerEvent>)` pipeline. Keeping the semantic borrow contained in that call is what lets `transform` do `*program = compiled` afterwards — `SemanticBuilder::build` takes `&'a Program<'a>`, so an inlined build would pin the borrow for the whole arena and block the write. - `transform_source` now returns `(Program, TransformResult)`. ## Callers updated - `oxc_transformer::run_react_compiler` — the `*program = compiled` it used to do now lives inside `transform`; it just checks `result.changed` before rebuilding scoping. - `examples/react_compiler.rs`, `tasks/benchmark/benches/react_compiler.rs` — pass `&mut program`, gate output on `result.changed`. - lib.rs smoke tests — `transform_source(...).program.expect(...)` → destructure `(program, result)` + `assert!(result.changed, ...)`. ## Verification - `cargo test -p oxc_react_compiler` (38) and `oxc_transformer --features react_compiler` (9) pass - `cargo ck` (workspace, all-features/all-targets, `--locked`), clippy (`dev-no-debug-assertions`), `cargo fmt --check` — clean - `oxc_linter` (uses `lint`), benchmark target, and example all build --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) — AI-assisted, reviewed before submission (per the repo AI usage policy). |
||
|
|
74fb16871f |
refactor(react_compiler): vendor forked-react-compiler crates as in-crate modules (#23993)
## Summary Vendors the React Compiler *core* — the 12 `react_compiler*` crates from the MIT fork [oxc-project/forked-react-compiler](https://github.com/oxc-project/forked-react-compiler) @ `58d13c6` — directly into `crates/oxc_react_compiler`, one module per upstream crate, **replacing the three crates.io `forked_react_compiler*` dependencies**. This lets us edit and re-sync the compiler core in-tree instead of consuming it as published crates. The oxc↔Babel conversion boundary (`convert_ast`, `convert_ast_reverse`, `convert_scope`, `prefilter`, …) is unchanged. ## How Two commits, kept separate for reviewability: 1. **Verbatim copy** — the 12 crates' `src/` trees copied unmodified (114 files, `lib.rs` → `mod.rs`). Diff this commit against upstream to confirm fidelity. 2. **Wire-up** — a scripted, compiler-verified path rewrite: - each module's own `crate::` → `crate::<module>::`; sibling refs `react_compiler_x::` → `crate::react_compiler_x::` - `lib.rs` declares the 12 modules — `pub` (so the public API can name types that originate in them) and `#[allow(clippy::all, dead_code, unused)]` (kept near byte-for-byte with upstream) - `Cargo.toml`: drop the 3 fork deps; add the externals they need (`serde`, `serde-transcode`, `hmac-sha256`) and extend `serde_json` features - add `hmac-sha256` + `serde-transcode` to the workspace dependencies; allowlist 4 vendored local-variable abbreviations in `.typos.toml` ## Verification - `cargo test -p oxc_react_compiler`: **38 pass** (boundary smoke tests + vendored inline tests); codegen output byte-identical - `cargo ck` (workspace, all-features/targets, `--locked`), clippy (default + `dev-no-debug-assertions`), `cargo doc -D warnings`, `fmt`, `typos`, `cargo shear`, `cargo deny` — all clean - dependents build: `oxc_transformer --features react_compiler`, `oxc_linter` Behavior is unchanged — same compiler code, only module paths differ. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) — AI-assisted, reviewed before submission (per the repo AI usage policy). |
||
|
|
35c71e3e3b |
refactor(ast): declare enum inheritance with INHERIT (#23953)
Change the syntax used to declare that one enum inherits the variants from another.
Before:
```rust
enum Expression<'a> {
// ... own variants ...
@inherit MemberExpression
}
```
After:
```rust
enum Expression<'a> {
// ... own variants ...
INHERIT(MemberExpression<'a>),
}
```
Reason for this change is that the latter is valid Rust code, and can be parsed normally by Rust compiler and `syn`.
This removes the need for custom parsing logic in `ast_tools` - now we can just use `syn`'s standard parser for enums.
The downside it that this complicates the `inherit_variants!` macro - this PR reimplements it as a TT-muncher. But this macro is removed entirely in a later PR (#23960).
No change to behavior - only affects _how_ inheritance is declared, not how it works.
|
||
|
|
5762638c8e |
refactor(react_compiler): shorten AST builder code (#23860)
Follow-up to the `AstBuilder` migration. Now everything's on the new builder, a load of call sites were left more verbose than they need to be. This collapses them to the shorthand constructors, e.g.: ```rs Statement::VariableDeclaration(VariableDeclaration::boxed(span, kind, decls, declare, ctx)) // -> Statement::new_variable_declaration(span, kind, decls, declare, ctx) ``` Three shapes get shortened: - `ArenaBox::new_in(Function::new(...), x)` → `Function::boxed(...)` - `Statement::FunctionDeclaration(Function::boxed(...))` → `E::new_function_declaration(...)` - `Statement::from(Declaration::new_function_declaration(...))` → `Statement::new_function_declaration(...)` Done with an [ast-grep codemod](https://github.com/oxc-project/oxc/tree/overlookmotel/ast-builder-codemod), so it's purely mechanical - no behaviour change. |
||
|
|
3ea9304bd4 |
perf(react_compiler): use faster API to arena allocate strings (#23849)
Small perf optimization to React Compiler. Replace `ArenaStringBuilder::from_str_in(s, self.allocator).into_str()` with `Str::from_str_in(s, self)`. The latter is faster - `ArenaStringBuilder` is designed for building up a string from multiple parts. Here we just want to allocate a single string, so `Str::from_str_in` is faster, and simpler. Using `ArenaStringBuilder` was a weird choice. 🚨 Slop alert! 🚨 Also, return a `Str<'a>`. There's no advantage to coverting to a `&str`, and this removes having to convert _back_ to a `Str` elsewhere. |
||
|
|
065baf8a19 |
refactor(react_compiler): rename atom method to str (#23848)
Pure refactor. Rename `atom` method to `str`. `Atom` type became `Str` some time ago, so the name "atom" is outdated. |
||
|
|
8e31977907 |
refactor(react_compiler): switch to new AstBuilder (#23838)
Part of #23043. Migrate `oxc_react_compiler` over to new `AstBuilder`. |
||
|
|
163ed51285 |
refactor(allocator): Box::new_in take &GetAllocator (#23756)
Part of #23043. Same as #23755, but for `Box`. `Box::new_in` previously took an `&'a Allocator`. Instead make it take an `&A where A: GetAllocator<'a>`. This makes it more flexible - you can pass a reference to any type which implements `GetAllocator` e.g. `ParserImpl`, `TraverseCtx` etc. ```rust // Before (`ctx` here is `&mut TraverseCtx`) let boxed = Box::new_in(value, ctx.ast.allocator); // After let boxed = Box::new_in(value, ctx); ``` The main motivation is to make it a workable replacement for `AstBuilder::alloc`, which will be removed in the new `AstBuilder`: ```rust // Before (`ctx` here is `&mut TraverseCtx`) let boxed = ctx.ast.alloc(value); // After let boxed = ArenaBox::new_in(value, ctx); // Instead of let boxed = ArenaBox::new_in(value, ctx.ast.allocator); ``` This has the same advantages and disadvantages as `Vec` methods - see #23755. |
||
|
|
5201522945 |
refactor(allocator): Vec construction methods take &GetAllocator (#23755)
Part of #23043. `Vec::new_in`, `Vec::with_capacity_in`, `Vec::from_array_in` etc previously took an `&'a Allocator`. Instead make them take an `&A where A: GetAllocator<'a>`. This makes them more flexible - you can pass a reference to any type which implements `GetAllocator` e.g. `ParserImpl`, `TraverseCtx` etc. ```rust // Before (`ctx` here is `&mut TraverseCtx`) let vec = ArenaVec::new_in(ctx.ast.allocator); // After let vec = ArenaVec::new_in(ctx); ``` The main motivation is to make them workable replacements for what are currently methods on `AstBuilder`, but will be removed in the new `AstBuilder`: ```rust // Before (`ctx` here is `&mut TraverseCtx`) let vec = ctx.ast.vec(); // After let vec = ArenaVec::new_in(ctx); // Instead of let vec = ArenaVec::new_in(ctx.ast.allocator); ``` This is still longer code than the original, which is not ideal, but it's as short as can make them. The main downside of this change is that when calling these methods with an `&Allocator`, you have to double reference: ```rust let allocator: &'a Allocator = get_allocator_somehow(); // Passing `&&Allocator` let vec = ArenaVec::new_in(&allocator); ``` This has no perf impact - these methods are inlined, and compiler collapses the double-reference like it wasn't there, but it's just ugly. Unfortunately, there's no way to implement `GetAllocator` so it'll accept both `&Allocator` and `&mut TraverseCtx`, and the latter is the much more common use case. |
||
|
|
d1f18cda21 |
refactor(all): import arena Box and Vec as ArenaBox / ArenaVec (#23747)
Pure refactor. Use `ArenaBox` and `ArenaVec` everywhere for the arena-allocated types from `oxc_allocator`, instead of plain `Box` and `Vec`. This disambiguates when `Vec` is `oxc_allocator::Vec` and when it's `std::vec::Vec`, making code clearer to follow. Previously it was easy to confuse the two, and such confusion led to UB in `Traverse` (fixed in #23745). Ditto other arena types - `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`. The only exceptions are: 1. Inside `oxc_allocator` crate itself. 2. AST type definitions. 3. Generated code (migrated separately in #23749). |
||
|
|
8c6481bbf9 |
refactor(allocator): export ArenaBox and ArenaVec aliases (#23746)
Pure refactor.
It's a common pattern in repo to import `Vec` and `Box` etc from `oxc_allocator` and alias them to `ArenaBox` / `ArenaVec`.
```rust
use oxc_allocator::{Box as ArenaBox, Vec as ArenaVec};
```
Make this pattern less verbose by re-exporting `Box` and `Vec` as these aliases from `oxc_allocator`.
Ditto other arena types: `ArenaHashMap`, `ArenaHashSet`, `ArenaStringBuilder`.
Use those aliases in all other crates to shorten code.
```rust
use oxc_allocator::{ArenaBox, ArenaVec};
```
|
||
|
|
20375f9e19 |
fix(react_compiler): keep imports referenced only by a computed key (#23586)
## Problem
When the React Compiler hoists an object into a memo cache slot, a computed key like `{ [CSS_VAR]: x }` keeps referencing the imported `CSS_VAR`. But `convert_expression_to_property_key` built **every** identifier key as a static property name, ignoring the `computed` flag. Semantic analysis then never linked the key to the import, so the TypeScript transform's import elision dropped the import — while codegen still emitted `[CSS_VAR]`. The result is output with a dangling reference (a runtime `ReferenceError`).
```tsx
// input
import { iconSizeVar } from "./styles.css";
export function Box({ size }) {
const style = { [iconSizeVar]: size + "px" };
return <div style={style} />;
}
// before: the compiled body still reads `[iconSizeVar]`, but the import is gone ❌
```
## Fix
Thread `computed` through `convert_expression_to_property_key` and build computed keys from the expression, so the identifier stays an `IdentifierReference`, semantic analysis links it to the binding, and import elision keeps the import.
## How it was found / impact
By diffing oxc's React Compiler output against `babel-plugin-react-compiler` across the `oxc-ecosystem-ci` repos. **56 real-world files** (in `kibana`, `posthog`, `affine`, …) emitted code referencing a dropped import — all UPPER_SNAKE config constants or vanilla-extract CSS vars used as dynamic keys. This fixes every one of them, with **0 regressions** in the ~42k-file corpus.
---
🤖 Developed with [Claude Code](https://claude.com/claude-code) (AI-assisted).
|
||
|
|
2bac45ecc7 |
chore(react_compiler): bump forked_react_compiler to 0.2.0 (#23581)
Bumps `forked_react_compiler`, `forked_react_compiler_ast`, and `forked_react_compiler_hir` from `0.1.4` to the published `0.2.0` and adapts the OXC ↔ React Compiler AST bridge to the new API. ## API changes adapted - **`RawNode`** — type-annotation and TS passthrough fields moved from `Box<serde_json::Value>` to the lazily-parsed `RawNode`. Construct via `RawNode::from_value` / `RawNode::null`; read back via `parse_value()`. - **`StringLiteral.value`** is now a JS string type — write with `.into()`, read with `.to_string_lossy()`. - **`ScopeInfo`** maps now use the deterministic Fx hasher (`FxHashMap` / `FxIndexMap`) to match the upstream field types. - **`PluginOptions`** no longer derives `Deserialize`; the test constructs it typed via `default_plugin_options()`. ## Binary size (`napi/transform`) Net **−533 KiB (−7.2%)** on the shipped binary, despite 0.2.0 pulling in new transitive deps. Measured as the release cdylib (fat-LTO, `--features allocator`, `aarch64-apple-darwin`, stripped); only the bump differs between the two builds. | Segment | Before (`main`) | After (this PR) | Δ | |---|--:|--:|--:| | **Total file** | 7,554,352 B | 7,008,592 B | **−545,760 B (−7.2%)** | | `__TEXT` (code) | 7,274,496 B | 6,750,208 B | −524,288 B | | `__DATA_CONST` | 163,840 B | 147,456 B | −16,384 B | | `__LINKEDIT` | 98,304 B | 81,920 B | −16,384 B | Almost all of it is `__TEXT` — likely the eager `serde_json::Value` machinery dropped in favor of `RawNode`'s lazily-parsed raw JSON. ## Verification - `cargo test -p oxc_react_compiler` — 17 passed - `cargo check -p oxc_linter` and `cargo check -p oxc_transformer --features react_compiler` — both clean --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> |
||
|
|
1f53a75916 |
refactor(react_compiler): drop unneeded enum_eval from Semantic build (#23551)
## Summary - The React Compiler's internal `Semantic` build set `.with_enum_eval(true)`, but nothing downstream reads the evaluated enum member values: `convert_ast` lowers every `TSEnumMember` to `serde_json::Value::Null` (member values are dropped on the way into `react_compiler_ast`), and `convert_scope_info` reads no enum constants. The evaluation work was therefore unobservable. - Drop the flag. Transform output is byte-identical across the full upstream React Compiler fixture suite (1,399 compiled files), and the existing unit and transformer-integration tests pass. |
||
|
|
1b7b17b76b |
refactor(react_compiler): remove dead apply_renames (#23550)
## Summary - Remove `apply_renames.rs` and its `build_rename_plan` / `apply_renames` plumbing in `transform`. The pass walks the reverse-converted program to apply the compiler's binding renames to uncompiled sibling references, but it never changes output: every reference it matches already carries the renamed name (the React Compiler bakes the `_0`-suffixed names into its own regenerated codegen), so the rename is always a no-op. - Verified by instrumenting the visitor to log only renames that change a name, then running `transform` over the upstream React Compiler fixture suite (1,399 compiled files) plus the unit and integration suites: the rename plan is frequently populated (up to 23 entries) yet fires zero real renames. Neutering the pass to a no-op likewise leaves every test passing. - Drop the `source_extracted_class_spans_do_not_collide_with_rename_plan` test and its two helpers — they guarded `apply_renames` against corrupting source-preserved bindings, a path that no longer exists. `type_query_casts_are_renamed_with_value_bindings` stays, since its rename comes from the compiler's codegen rather than this pass. - No behavior change. Verified by `cargo test -p oxc_react_compiler` (16 passed) and the `react_compiler` transformer integration test. |
||
|
|
488b382bde |
perf(react_compiler): borrow binding names in prefilter instead of allocating (#23471)
## What & why
The React-compiler prefilter (`has_react_like_functions`) runs on
**every file** in `transform()`
before deciding whether to compile it, and walks the whole program (for
non-React files it never
short-circuits). For **every variable declarator and every
assignment-target identifier** it did:
```rust
current_name: Option<String>,
// ...
Some(ident.name.to_string()) // heap String, used only to call is_react_like_name(&str)
```
`ident.name` is already an arena `Ident<'a>` (`as_str()` returns `&'a
str`), and `current_name` is
only ever read by `is_react_like_name(name: &str)`. So the owned
`String` — a heap alloc + memcpy of
the name bytes, per binding, thrown away immediately — is pure waste.
This borrows it instead:
```rust
struct ReactLikeVisitor<'a> { found: bool, current_name: Option<&'a str> }
// ...
Some(ident.name.as_str()) // zero-cost arena borrow
```
**The positive signal:** one heap `String` allocation (malloc/free +
name-byte memcpy) is removed per
declarator and per assignment-target identifier, across the whole
program, on a path that runs for
every file. On non-React files (e.g. a large `binder.ts`) the prefilter
never short-circuits, so this
is thousands of throwaway allocations eliminated. `&str`/`Ident` is
`Copy` and the struct shrinks
(`Option<&str>` 16B vs `Option<String>` 24B), so it is strictly less
work — never more.
`oxc_react_compiler` is **not** covered by `allocs_*.snap`, so there's
no local snapshot to quantify
this; the `react_compiler` CodSpeed benchmark is the arbiter (removing
system `malloc`/`free` is real
retired instructions, so it should register).
## Behavior-preserving
The borrowed `&'a str` holds the identical bytes the `String` did;
`is_react_like_name` is a pure
read. Prefilter include/exclude decisions are byte-for-byte identical.
The lifetime is sound:
`Ident::as_str() -> &'a str` points into the arena (the program's `'a`),
which outlives the visitor.
`cargo test -p oxc_react_compiler` (17 tests) passes; no snapshot drift.
> Note: `prefilter.rs` is vendored from the upstream React Compiler
(`react_compiler_oxc`), but is
> already substantially oxc-diverged (it adds
`ResourceManagementVisitor`, `memo`/`forwardRef`
> handling, etc.). This is an intentional oxc-side idiom tightening —
flagging it for anyone re-syncing.
---
Prepared with AI assistance.
|
||
|
|
1490a0a069 |
feat(linter/react): implement react-compiler rule (#23202)
Adds a nursery `react/react-compiler` rule that runs the React Compiler (`oxc_react_compiler`) in lint-only mode and reports Rules of React violations: conditional hook calls, setState during render, ref access during render, mutation errors, etc. ### Rule - `run_once` rule calling `oxc_react_compiler::lint()` on the program; the compiler's own prefilter skips files with no React-like functions. - Lints with the same compiler options as `eslint-plugin-react-compiler` (`COMPILER_OPTIONS` in its `RunReactCompiler.ts`): `lint` output mode plus the validations the plugin force-enables (impure functions, capitalized calls, JSX in try, static components, setState in effects, etc.). - Reports only error-severity violations by default. Compiler bail-outs (skipped optimizations, e.g. unsupported syntax) are opt-in via `reportAllBailouts`. - Flow suppressions are handled like the eslint plugin: diagnostics whose primary location sits directly below a `$FlowFixMe[react-rule-hook]` / `$FlowFixMe[react-rule-unsafe-ref]` comment are dropped, while other diagnostics in the same function still surface. ### Tests All test cases from [`eslint-plugin-react-compiler/__tests__`](https://github.com/facebook/react/tree/main/compiler/packages/eslint-plugin-react-compiler/__tests__) (MIT) are ported, covering hooks order, capitalized calls, impure calls in render, JSX in try/catch, ref access in render, useMemo diagnostics, useState mutation (TS), `'use memo'`/`'use no forget'` directives, and Flow suppressions. The only case not ported is "Basic example with component syntax", which uses Flow's experimental `component` declaration syntax that oxc does not parse. `RustBackendComparison-test.ts` replicates the same cases for backend comparison, so it adds no new ones. Every diagnostic message and location in the snapshot matches the upstream assertions. ### Diagnostics `oxc_react_compiler` diagnostics previously had no spans. They now carry labeled spans built from the compiler's source locations (byte offsets flow through from oxc spans): `detail.loc` → sub-detail locations with their messages (e.g. "Found setState() in render") → enclosing function as fallback. Long descriptions moved from the message into `help`. This makes code frames and `oxlint-disable` directives work. Example output: ``` ⚠ react(react-compiler): RenderSetState: Cannot call setState during render ╭─[App.tsx:3:17] 2 │ const [state, setState] = useState(0); 3 │ setState(1); · ────┬─── · ╰── Found setState() in render 4 │ return <div>{state}</div>; ╰──── help: Calling setState during render may trigger an infinite loop. ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Cameron Clark <cameron.clark@hey.com> |
||
|
|
777f02ae10 |
refactor(diagnostics)!: return a Diagnostics type instead of Vec<OxcDiagnostic> (#23217)
## Summary Introduces `oxc_diagnostics::Diagnostics` — a `Vec<OxcDiagnostic>` newtype that knows the difference between errors and warnings — and makes it the standard diagnostics return value across oxc. - **New type**: `Diagnostics` with `has_errors()`, `has_warnings()`, `errors()`, `warnings()`, plus `Vec`-like ergonomics (Deref, `IntoIterator`, `Extend`, `From`/`Into<Vec>`) so most call sites are unchanged. - **Return types**: `ParserReturn`, `SemanticBuilderReturn`, `IsolatedDeclarationsReturn`, the React Compiler `TransformResult`/`LintResult`, and `TransformerReturn` now expose `diagnostics: Diagnostics` instead of `errors: Vec<OxcDiagnostic>` (the React Compiler's separate `errors`/`warnings` are merged into one severity-tagged list). - **Behaviour**: a React Compiler **error** now stops the transform (no output), while **warnings** flow through and the transform continues. `compile()` aborts codegen only when `diagnostics.has_errors()` — warnings alone no longer abort (previously any diagnostic did). ## Breaking change Consumers reading `*.errors` on these return values must read `*.diagnostics`. React Compiler consumers that read `.errors`/`.warnings` separately should use `diagnostics.has_errors()` / `diagnostics.errors()` / `diagnostics.warnings()`. Parser/semantic/isolated-declarations fatality is otherwise unchanged. Builds on the React Compiler transform feature (#23201). 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
de38a3f892 |
fix(react_compiler): keep imports referenced only by a local re-export (#23176)
## Problem
When the React Compiler compiles a module, a named import that is only re-exported via a local `export { … }` clause was dropped from the output, leaving a dangling `export { Foo }` that references a removed binding — i.e. **invalid code**.
Minimal repro:
```tsx
import { Foo } from "./foo";
export { Foo };
export function C() { const x = useState(0); return <div>{x}</div>; }
```
Before this change, the compiled output emits `export { Foo }` with **no** `import { Foo }`. With the React Compiler disabled the import is kept, so the bug is specific to the compiler's AST round-trip.
## Root cause
oxc's parser rewrites a *local* export's specifier `local` from `IdentifierName` to `IdentifierReference` when there is no `source` (`parse_export_named_specifiers`), and semantic analysis only records a reference to the imported binding for an `IdentifierReference` (`binder.rs`). The React Compiler's reverse AST conversion (`convert_ast_reverse`) always rebuilt `local` as a plain `IdentifierName`, so no reference was created, and the downstream TypeScript import elision then removed the "unused" import — while leaving the export clause behind.
## Fix
Mirror the parser: in `convert_export_named_declaration`, when the export has no `source`, build each specifier's `local` as an `IdentifierReference`. Re-exports (`export { x } from "…"`) keep an `IdentifierName`, since there `local` names an export of the other module.
## Verification
- Added regression test `local_reexport_keeps_its_import_binding`; `cargo test -p oxc_react_compiler` passes (17/17), `napi/transform` passes (62/62).
- Found by differential-testing the React Compiler against `babel-plugin-react-compiler` across the ecosystem-ci repos. Re-running the 90 affected files with the fix: the invalid-output bucket drops to **0** and 84/90 now match Babel exactly (the rest are pre-existing, benign output-equivalent differences).
## Note
The upstream `oxc-react-compiler` repo carries an identical copy of this conversion code (`crates/react_compiler_oxc/src/convert_ast_reverse.rs`); the same change should be mirrored there to survive the next vendor sync.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
|
||
|
|
52801a2f17 | refactor(react_compiler): reuse parsed comments instead of re-parsing in preserve_comments (#23169) | ||
|
|
48c6f28c4a |
refactor(react_compiler): split diagnostics into errors/warnings and simplify transform API (#23168)
## What Reworks the `oxc_react_compiler` public API: - Renames the standalone `run` entrypoint to `transform`. It now returns a `TransformResult` instead of taking a `&mut errors` vec and rebuilding `Scoping` internally. Callers use `result.program` (`Some`/`None`) to decide whether to rebuild scoping — the `oxc::Compiler` callsite does exactly this. - Splits `TransformResult`/`LintResult` diagnostics into separate `errors` and `warnings` (partitioned by `Severity`), and documents `errors` / `warnings` / `events`. - Adds `#[derive(Default)]` to `TransformResult` for the bail-out paths. - Inlines the former `transform_impl`; semantic is now built internally, after the cheap prefilter bail-outs (so non-React files skip semantic construction). - Bumps the `forked_react_compiler` family `0.1.3` → `0.1.4`. Example and benchmark updated to the new `transform(&program, &allocator, options)` signature. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
1260f03b4f |
refactor(semantic): don't build the AstNodes store by default (#23005)
## What Make the full `AstNodes` store **opt-in**, so the compiler pipeline (transformer / codegen / minifier) can run semantic analysis without building it at all. Previously every `SemanticBuilder` run allocated and populated the full `AstNodes` store, even for consumers that only need scoping + the syntax checker. This separates the node-related builder state into three independent pieces: - a standalone `NodeId` counter — the source of truth for node ids, valid in either mode; - an **ancestry stack** — an upward-walk-only stack of the current node's ancestors, the only ancestry the syntax checker and binder actually read; - the full **`AstNodes`** store — random access to every node, needed by the linter and mangler. ## How - A new `AstNodeStore` (in `crates/oxc_semantic/src/node/store.rs`) bundles the node-id counter, the current-node cursor/flags, and the storage. The storage is an enum (`AstNodeStoreKind`) with two variants — `Full(AstNodes)` and `Ancestry(AncestryStack)` — so the full store and the ancestry stack are **mutually exclusive by construction**; they can never both be maintained at once. - The checker and binder read ancestry through an `Ancestry` view (`node/ancestry.rs`) that dispatches between the full store (linter / mangler) and the stack (compiler pipeline). They no longer do random `NodeId` access into the store — all ancestry access is upward-walk-only. - `SemanticBuilder` gains `with_build_nodes(bool)`. The default is **off** (ancestry stack only). `new_linter()` turns it on (the linter needs the full store); `new_compiler()` leaves it off. - `CompilerInterface::build_semantic_nodes()` lets `oxc::Compiler` consumers opt back in if their `after_semantic` hook reads `Semantic::nodes`. ## Notes - Behaviour-neutral: semantic conformance snapshots are unchanged. - Allocation snapshots updated — the default (ancestry) path allocates slightly less (the growing `Stack` trades a couple of fixed allocations for a few reallocs). - Builds on the already-merged `SymbolFlags`-based redeclaration checks (#23060) and the `new_linter`/`new_compiler` preset adoption (#23057), which removed the last random node reads from the redeclaration path so the checker works without the store. |
||
|
|
b846ab2d9a |
feat(react_compiler): integrate the Rust port of the React Compiler (#22942)
Closes https://github.com/oxc-project/oxc/issues/10048 Integrates the Rust port of the React Compiler ([facebook/react#36173](https://github.com/facebook/react/pull/36173)) into oxc. ## Notes (resolved) - Published crates can't reference Git URLs, so using this from Rolldown needs the React Compiler crates on crates.io. ✅ Published as a fork at https://crates.io/crates/forked_react_compiler; this PR depends on those `forked_react_compiler*` crates so people can get earlier access. - The React Compiler crates had no license field. ✅ The published fork carries `license = "MIT"` (per the React repo's MIT license), so Cargo Deny and Security Analysis pass. ## Benchmark **Wall-clock overhead vs plain transform** (release `transformSync`, same `jsx: automatic`, toggling only `reactCompiler`): | fixture | without | with | overhead | | --- | --- | --- | --- | | RadixUIAdoptionSection.jsx (2.5 KiB) | 0.03 ms | 1.81 ms | +1.8 ms | | excalidraw `App.tsx` (406 KiB) | 4.09 ms | 14.49 ms | +10.4 ms (3.5×) | So roughly a fixed ~1.8 ms floor per file plus a per-size cost — about **3.5× the transform time** on a large real-world component. ## Binary size Linking the React Compiler pulls the whole compiler pipeline (HIR, lowering, inference, SSA, optimization, reactive scopes, validation) plus the oxc⇄Babel AST conversion into the binding. Release build of the napi transform addon (`transform.darwin-arm64.node`, `--release`, stripped), darwin-arm64: | build | size | | --- | --- | | baseline (`main`) | 3.51 MiB | | with React Compiler | 8.66 MiB | | **delta** | **+5.14 MiB (+146%, 2.46×)** | |