Use `Ident` in the property mangler. This reduces the `Set::contains` operation faster as the hashing doesn't happen.
The property names are not always an "ident" so it may not make sense to use the `Ident` type. But currently the `Ident` type allows characters that are not allowed in identifiers so the code works.
I used AI to generate the code and then reviewed the code and iterated a bit.
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.
### Feature
- `include` selects names to mangle. `exclude` and `reserved` keep names unchanged.
- The cache can give a property a fixed output name or keep it unchanged.
- The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names.
- `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key.
- Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided.
- Bundlers can collect names from several programs, create one shared map, and apply it to every program.
- Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass.
### Design
Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`.
For one program, the order is:
```text
collect names → assign names → rewrite once → compress → mangle variables and private names
```
Property names are rewritten before compression because compression can remove information about quotes and comments.
For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.
The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.
### Example
With `include: "^_"` and `mangle_quoted: true`:
```js
obj[`_field`] = 1;
obj._field = 2;
```
becomes:
```js
obj[`e`] = 1;
obj.e = 2;
```
### Limitation
- Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved.
- Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not.
- The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names.
- Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled.
- TypeScript enum reverse mappings are not supported because the transform creates quoted strings.
- Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names.
The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.
Supersedes #23772.
AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
- dispatcher
- string_embedder
- tailwind_sorter
Group these 3 together as `SessionServices`, make them available to `FormatSession`, and pass them around.
Add a method `Utf8ToUtf16::convert_program_and_comments`, and use it in a couple of places.
This is marginally faster than `convert_program` followed by `convert_comments`, as it avoids branching on the same condition twice.
Closes the migration tracked in #23043.
The new builder API (methods on the AST types themselves) has been in place for a while, all Oxc crates are migrated, and every crate which uses `AstBuilder` already enables `disable_old_builder` in its tests. The module docs said "after a few weeks, we will remove `AstBuilder`'s own methods entirely" — this does that.
## What's removed
- `crates/oxc_ast/src/generated/ast_builder.rs` and `builder/methods.rs` — `AstBuilder`'s own node and primitive constructors.
- `AstBuilderGenerator`, which produced the former.
- The `disable_old_builder` Cargo feature, plus the dev-dependency hacks it required across 33 manifests. Eight crates depended on `oxc_ast` *purely* to turn the feature on; those deps and their `cargo-shear` ignore entries are gone too.
## One thing worth reviewing
`AstBuilderGenerator` owned the `#[builder(default)]` / `#[builder(skip)]` attr registration that `BuilderMethodsGenerator` only *reads* — its doc comment said as much. Deleting it naively would have silently dropped `skip`/`default` handling for the 12 AST types using those attrs, so `attrs()` and `parse_attr()` move to `BuilderMethodsGenerator`.
The evidence this was faithful: after regenerating, `builder_methods.rs` and every other generated file are **byte-identical**. The only generated change in the diff is the deletion.
## Breaking changes
- `AstBuilder`'s own methods are gone. Use the AST type methods: `Statement::new_expression_statement(span, expr, &builder)` instead of `builder.statement_expression(span, expr)`.
- `AstBuilder` is no longer `Copy`/`Clone`, and its `allocator` field is private — use the `allocator` method from `GetAllocator`.
- `AstBuilder` and `NONE` are no longer re-exported from the crate root; import them from `oxc_ast::builder`. (The `disable_old_builder` feature already gated these off, so nothing in-tree used the root path.)
- The `disable_old_builder` feature no longer exists — remove it from any manifest that sets it.
A migration note covering all of the above is in `crates/oxc_ast/src/builder/mod.rs`.
---
Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
Splits `oxc_react_compiler`'s public API into two phases so the transform rewrites the caller's program in place instead of returning a whole-program clone:
- `compile(&Program, &Semantic, &Allocator, PluginOptions) -> (Option<CompileOutput>, Diagnostics)` — read-only; `None` means no changes (or error / lint output mode).
- `CompileOutput::transform(self, &mut Program, &Allocator)` — substitutes the compiled functions into the program in place. `CompileOutput` owns only arena data (replacements + `ProgramContext`), so the caller drops its `Semantic` between the two calls, releasing the shared borrow that previously forced `ox_splice_program` to `clone_in_with_semantic_ids` the entire program.
- `lint` is unchanged and structurally never runs the transform phase; the oxlint rule needed no changes.
The old `transform` free function and `TransformResult` are removed; `oxc_transformer` no longer does the `*program = result.program.take()` swap and just applies the output to its `&mut Program`.
The 1806-fixture snapshot corpus is byte-identical, and the transform path drops one whole-program clone per compiled file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
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.
Adds `MangleOptions::reserved` — equivalent to terser / uglify-js / swc `mangle.reserved`: names in the set are never used as mangled names, and bindings that already carry them keep them. Default empty; default output is byte-identical to main (minsize snapshot unchanged).
## Motivation
Node's cjs-module-lexer detects a CommonJS module's named exports by **lexically** scanning for `exports.<name> =` / `module.exports` token patterns — no scope analysis ([its README documents renamed identifiers as "DETECTS: NO EXPORTS"](https://github.com/nodejs/cjs-module-lexer)). UMD / CommonJS wrappers bind `exports` / `module` as ordinary function parameters, and the mangler renames them, erasing every named export the lexer can see.
Surfaced by the monitor-oxc mangler runtime test ([failing run](https://github.com/oxc-project/monitor-oxc/actions/runs/28569108563/job/84702922472), previously masked by #24033 / #24036): `archiver` does `import { queue } from "async"`, and `async`'s dist is a UMD bundle — mangled, the factory parameter `exports` became `e` and node fails with `Named export 'queue' not found`. With `reserved: ["exports", "module"]` the output imports correctly (verified end-to-end in node, including running a queue task).
## Why an opt-in list instead of always keeping these names
Renaming them is ecosystem-standard. Verified empirically — minifying the same UMD sample and importing it in node:
| minifier | renames `exports` param? | `import { queue }` from its output | escape hatch |
| --- | --- | --- | --- |
| terser 5.48 | yes | fails | `mangle.reserved` (verified working) |
| uglify-js 3.19 | yes | fails | `reserved` (verified working) |
| esbuild 0.28 `--minify` | yes | fails | none for identifier mangling |
| swc | yes | fails | terser-style `mangle.reserved` ([swc#3424](https://github.com/swc-project/swc/issues/3424)) |
An earlier revision of this PR kept `exports` / `module` unconditionally; the Minification Size check showed the real cost: ~1% minified on UMD-heavy files (d3: +3.3 kB from ~590 `exports` references, flipping it to *larger than esbuild*), because the point is keeping the literal 7-char token at every export site. No other minifier pays this by default — and rolldown bundles bind `exports` / `module` in every CJS interop wrapper, where node's lexer never looks. So oxc gets the same escape hatch as terser/swc, with the same name.
`MangleOptions` loses `Copy` (it now holds the set); construction sites switch to `..MangleOptions::default()` / `clone()`. Exposed in `oxc-minify` NAPI options (`mangle.reserved: string[]`) and the mangler example (`--reserve-exports`).
monitor-oxc will pass `["exports", "module"]` for its mangler runtime test (its corpus imports prebuilt CJS/UMD dists directly).
For reference, esbuild solves this for its *own* node-platform CJS bundles differently — a dead-code annotation `0 && (module.exports = { a, b })` that cjs-module-lexer recognizes ([esbuild#960](https://github.com/evanw/esbuild/issues/960), [cjs-module-lexer#44](https://github.com/nodejs/cjs-module-lexer/issues/44)); verified that pattern also works appended to fully-mangled output. That could be a future zero-size-cost alternative at the minifier level.
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.
## 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).
Part of #23043.
We don't enable `oxc_ast`'s `disable_old_builder` Cargo feature in any of our published crates, or crates that are depended on by published crates.
Tasks are private to the repo, so enable this feature on them, to ensure no usage of legacy `AstBuilder` methods sneak in during the transition period before we delete these methods.
## Summary
The `pipeline` benchmark configured only a single `process.env.NODE_ENV`
dot define and a single `Buffer` inject, so it never exercised most of
the work a real config triggers:
- **Defines** — only the dot path ran. Identifier defines,
`import.meta.env.*`, and trailing wildcards were never matched, even
though those are the common cases in a Vite/bundler setup and each adds
per-node lookup cost.
- **Injects** — only one named specifier, so the per-reference inject
matching was barely exercised.
This swaps in a representative configuration:
- A ~18-entry Vite-style **define** set covering all four dispatch
paths: identifier (`__DEV__`, …), dot (`process.env.NODE_ENV`, …),
specific `import.meta.env.*` (`MODE`, `PROD`, `VITE_*`, …), and trailing
wildcards (`import.meta.env.*`, `import.meta.*`).
- A set of common **injects**: Node.js polyfills (`Buffer`, `process`,
`setImmediate`, `clearImmediate`) and a classic-runtime JSX auto-import
(`React`), covering both named and default specifiers.
No production code changes — benchmark configuration only. The pipeline
runs end-to-end on the test files (verified locally); the added
define/inject matching shows up as a small, expected increase in
measured pipeline time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Previously, whether to serialize AST to ESTree JSON with/without TS fields was a compile-time option - via 2 different configs `CompactTSSerializer` and `CompactJSSerializer` (and their pretty-printing and with-fixes counterparts).
Turn this into a runtime option instead -
This make serialization a little slower (more branches in either mode, and dead code in JS-only mode) but reduces the size of `oxc-parser` binary as only one `ESTreeSerializer` is generated, instead of 2 (JS-only, and with-TS).
## 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)
## 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)
Rework the pipeline benchmark to run the full compilation through the
`oxc` root crate's `Compiler`/`CompilerInterface` instead of hand-wiring
each `oxc_*` stage.
- A `PipelineCompiler` implements `CompilerInterface`, overriding the
option hooks with common options:
- **transform**: `esnext` target
- **define**: `process.env.NODE_ENV` → `'production'`
- **inject**: `Buffer` from `node:buffer`
- **compress**: `CompressOptions::smallest()`, plus default mangle and
codegen
- The bench body just calls `compiler.compile(...)`, so parse → semantic
→ transform → define → inject → minify → mangle → codegen is driven by
the interface.
- `Cargo.toml`: added `oxc` (with `full` feature) to the `compiler`
feature and to `cargo-shear` ignored list.
The CI matrix maps the `pipeline` component to the `compiler` feature,
which now includes `oxc`, so no workflow changes are needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## What
The `Bench react_compiler` job on `main` panics:
```
thread 'main' panicked at oxc_index-5.0.0/src/indexing.rs:38:10:
index out of bounds: the len is 0 but the index is 2
```
[Failing
run.](https://github.com/oxc-project/oxc/actions/runs/27154333851/job/80153249638)
## Why
#23005 made the full `AstNodes` store opt-in via
`SemanticBuilder::with_build_nodes(true)` (default now off). The react
compiler reads `semantic.nodes()` in `convert_scope.rs`, so it needs the
store.
That PR updated every call site that feeds `transform` —
`transform_source`, `lint_source`, `run`, and the example — to enable
`with_build_nodes(true)` and `with_enum_eval(true)`, but missed this
benchmark. With nodes off, `semantic.nodes()` is an empty `IndexVec` and
the first `NodeId` access panics. PR CI didn't catch it because the
benchmark only runs on `main`.
## How
Match the other call sites: enable `with_build_nodes(true)` and
`with_enum_eval(true)` in the benchmark's `SemanticBuilder`.
Verified locally by building with `--features react_compiler` and
running the bench across all 5 inputs (RadixUIAdoptionSection.jsx,
react.development.js, App.tsx, binder.ts, kitchen-sink.tsx) — all
complete, no panic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## 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.
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×)** |
Replaces ad-hoc `SemanticBuilder::new().with_*()` chains with the `new_linter()` / `new_compiler()` presets where the preset already expands to the exact same option set, so the config lives in one place.
- linter example + linter benchmark → `new_linter()` (cfg + class table + syntax)
- semantic benchmark, cfg example, integration test util → `new_compiler()` (syntax)
Behavior-neutral — each preset matches the options the callsite set by hand. Carved out of the larger store-optional refactor so it can land independently on `main` first.
## Summary
Follow-up cleanup to #22862 (which made the `oxc_semantic` `ClassTable` optional and off by default). Two small improvements:
1. **Collapse the duplicate flag.** #22862 added a `build_class_table` field on `SemanticBuilder` that existed only to be OR'd into the `ClassTableBuilder::enabled` flag that already exists. `with_class_table` now writes `class_table_builder.enabled` directly, and `build()` folds in the checker dependency with `enabled |= self.check_syntax_error`. One source of truth instead of two flags plus a bridge; behaviour is identical and order-independent.
2. **Fix linter-benchmark fidelity.** `tasks/benchmark/benches/linter.rs` runs `ConfigStoreBuilder::all()` but built its semantic without the class table, so after #22862 flipped the default the class-dependent rules (`no_dupe_class_members`, `no_unused_private_class_members`, `max_classes_per_file`) silently no-op'd there — the benchmark measured less work than production. Added `.with_class_table(true)` to match `service/runtime.rs`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Summary
Follow-up to #22590. That PR replaced the synthetic `cal.com.tsx` fixture — a concatenation of Next.js pages that declares top-level names 70–113× and emits thousands of redeclaration diagnostics, inflating semantic `sys_allocs` — with excalidraw's real, single-file `App.tsx`. But it only touched `TestFiles::complicated()` (used by `track_memory_allocations`).
This applies the same swap to every remaining benchmark that still used `cal.com.tsx`, and unifies **all** excalidraw `App.tsx` references on one pin (`f6d85bc8`):
| File | Change |
| --- | --- |
| `tasks/common/src/test_file.rs` | `minimal()` (feeds every `tasks/benchmark` criterion bench): `cal.com.tsx` → `App.tsx@f6d85bc8`. `formatter()` re-pinned `@v0.18.0` → `@f6d85bc8` |
| `napi/parser/bench.bench.js` | parser benchmark fixture → `App.tsx@f6d85bc8` |
| `napi/parser/test/parse-raw.test.ts` | bench-fixture list + size comment → `App.tsx@f6d85bc8` |
| `tasks/benchmark/benches/semantic.rs` | rewrote the now-false "`cal.com.tsx` has many errors" comment |
| `crates/oxc_ast/src/serialize/mod.rs` | removed the stale `cal.com.tsx` row from the capacity-ratio doc table |
## Why a single pin
`get_source_text` caches downloads by the last URL segment only (`App.tsx`). With two different pins live, a single `cargo bench` run would download both into the same `target/App.tsx` and silently bench whichever landed first. Re-pinning `formatter()` to `f6d85bc8` removes that collision — and the pre-existing latent one between `cargo bench` (`@v0.18.0`) and `cargo allocs` (`@f6d85bc8`).
## Notes
No committed snapshots change: these fixtures feed only timing benches and a `programRaw.toEqual(programStandard)` self-consistency test; the snapshotted `complicated()` / `minifier()` sets are untouched.
---
This PR was prepared with AI assistance; I reviewed, tested, and verified the changes.
Align `oxc_formatter` with `oxc_formatter_json`, both now expose `format(...) -> Result<Formatted<'a, _>, OxcDiagnostic>`.
- Add text-in `format` and `format_fragment`, also `format_program`, `parse_for_format` and the `FragmentContext` enum
- Parse options, JSX enabling, js-in-xxx context aware handlings are applied internally
- Make the `Formatter` struct and AST/IR primitives crate-internal
- Fold `get_parse_options` / `enable_jsx_source_type` and the `service` module into `parse_for_format`
## Summary
`bench_mangler` parsed each `.ts`/`.tsx` fixture and ran the mangler on the resulting **TypeScript** AST. But the mangler only ever runs on JavaScript — in the real minification pipeline TypeScript is transformed (types stripped) before the mangler runs — so mangling an *untransformed* TS AST measures work the mangler never actually does (e.g. walking type-annotation nodes that don't exist at that stage).
This transforms each fixture to esnext first — the same step `bench_minifier` already performs — then mangles the resulting JavaScript, so the benchmark reflects the input the mangler really receives.
## Effect on the numbers
This is **not** a mangler speedup — it changes *what the `.ts`/`.tsx` cases measure*. After transform, the TypeScript fixtures are smaller (types gone), so the mangler does less work on them; JavaScript fixtures are unaffected (transform is a no-op). Local criterion:
| fixture | before (raw TS) | after (transformed JS) |
| --- | --- | --- |
| `react.development.js` | 17.5 µs | 17.5 µs — unchanged (already JS) |
| `binder.ts` | ~60 µs | ~48 µs |
| `cal.com.tsx` | ~340 µs | ~320 µs |
| `kitchen-sink.tsx` | ~520 µs | ~330 µs |
The CodSpeed baseline for the `mangler/*.ts` / `*.tsx` cases shifts accordingly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Split `oxc_formatter` into:
- `oxc_formatter(_js)`: JS/TS specific formatter, comment handling, sortImports, jsdoc, etc
- `oxc_formatter_core`: Language agnostic IR, printer, etc
The naming convention is different from Biome or Ruff (they do not have `_core`, and `_formatter` is equivalent to the core), but this is because in Oxc, items without `_js` are the default.
As a basic task, I have added `Context` as a generic to all relevant parts to ensure support for multiple languages.
There might still be other components that could be refactored into the core, but I would like to finish them as I implement support for other languages.
Also moved some shared utilities to the core in advance.
## Summary
Adds `kitchen-sink.tsx` — a comprehensive synthetic TypeScript+JSX fixture maintained at [oxc-project/benchmark-files](https://github.com/oxc-project/benchmark-files) — to both `TestFiles::minimal()` (bench input set) and `TestFiles::complicated()` (alloc-tracking input set). The existing files in each set are untouched; this is a strict append.
## Why
The existing bench input set didn't reliably surface general-purpose perf wins above the ~1-2% measurement noise floor:
- #22580 (semantic pre-reserve) — visible because `binder.ts` exercises it
- #22594 (formatter buffer) — visible
- #22596 (minifier `try_fold_concat`) — **not visible** on the old set
- #22599 (semantic resolve-refs no-temp-Vec) — **not visible**
- #22603 (semantic var-hoist SmallVec) — **not visible**
The kitchen-sink fixes that by exercising every AST node, every transformer plugin, every minifier optimization opportunity, and every semantic step in one large file. Verified by re-benching #22596 against this fixture: **minifier mean −1.5%, min −3.7%** — above noise, signal confirmed.
## Fixture stats (cross-checked locally)
| Metric | Value |
|---|---|
| Source size | 21,117 lines / 732.90 kB |
| AST nodes | ~133,000 |
| Scopes | ~4,750 |
| Symbols | ~7,000 |
| Resolved references | ~16,000 |
| Semantic diagnostics | 0 errors / 0 warnings |
## Snap baselines
`tasks/track_memory_allocations/allocs_*.snap` updated with the kitchen-sink row across all 5 pipelines (parser / semantic / transformer / minifier / formatter). Future PRs that change allocation behavior on this fixture will produce a snap diff in CI.
## Bench-cleaner fix
`tasks/benchmark/benches/lexer.rs`'s `SourceCleaner` was missing `visit_ts_template_literal_type` — TypeScript type-level template literals (e.g. `` `${T}-${U}` `` in conditional / mapped types) are syntactically identical to value-level template literals, so the bench-mode lexer (without parser context) cannot distinguish them. Without the cleaner converting them to plain strings, kitchen-sink's type-level templates caused the lexer bench to swallow ~1 KB spans as a single `TemplateHead` and produce spurious "Unterminated string" / "Invalid Unicode escape" errors. One-line fix to mirror the existing `visit_template_literal` handling.
AI disclosure: drafted with Claude Code, reviewed manually.
## Summary
- `bench_mangler`'s `keep_names` variant was missing the `allocator.reset()` / `temp_allocator.reset()` calls that every other write-into-arena bench has. Without resets, the arenas grew across criterion's warmup + measurement iterations and hit libc allocation paths — explicitly called out as non-deterministic in `tasks/benchmark/src/lib.rs`.
- This was the source of recurring ~3% noise on `mangler[RadixUIAdoptionSection.jsx_keep_names]` in CodSpeed, appearing as false-positive regressions on unrelated PRs.
- Fix mirrors the working sibling pattern at the regular `bench_mangler` immediately above.
AI disclosure: drafted with Claude Code, reviewed manually.
Pre-requisite for #22252.
## Why
#22252 adds a `debug_assert!` in the transformer that requires `Scoping` to be built with `SemanticBuilder::with_enum_eval(true)` whenever a `TSEnumDeclaration` reaches the transform pass. The internal `Compiler` (`crates/oxc/src/compiler.rs`) and the playground already enable it; the CI run on #22252 surfaced a few internal callers that don't.
Land that fix on its own first, so the assert PR doesn't trip CI on these tasks.
## Sites updated
Every `SemanticBuilder` site that feeds `Transformer::build_with_scoping`:
- `tasks/track_memory_allocations/src/lib.rs` — both call sites (`cargo allocs`).
- `tasks/benchmark/benches/transformer.rs`
- `tasks/benchmark/benches/pipeline.rs`
- `tasks/benchmark/benches/minifier.rs`
- `tasks/benchmark/benches/codegen.rs`
Other semantic builders in these files that feed only the minifier / codegen / compressor are left alone — they don't go through enum lowering.
## Test plan
- [x] `cargo check -p oxc_track_memory_allocations` — clean
- [x] `cargo check --benches -p oxc_benchmark` — clean
I added these benchmarks while working on improving perf of tokens APIs in linter. That work is now complete, so these benchmarks are no longer needed. We can always restore them later on if we start working on perf of tokens again.
Add a function `update_tokens` which converts tokens to ESTree format in place, mutating the `Token`s in the `Vec<Token>` in place.
This is what will be used for sending tokens to JS side via raw transfer.
Support for serializing tokens to JSON is also retained. The 2 implementations share the same AST visitor, and hook into it via separate `Context` implementations.
Add a benchmark for preparing tokens on Rust side to be sent to JS with raw transfer. This isn't implemented yet, so initially this benchmark measures perf of serializing to JSON.
Introduce `ESTreeTokenConfig` trait and allow option for the "flavor" of tokens to be set at compile time.
When producing Espree-style tokens, this allows cutting out all the special-case logic related to `JSXIdentifier` tokens and removing 2 branches from `emit_identifier_at`.
Small effect on perf - only +0.3% on ESTree tokens benchmark.
Previously we used a 3-pass process for serializing tokens in ESTree format.
* Pass 1: Walk AST and produce a list of "overrides" - which tokens need their `Kind` changed.
* Pass 2: Loop through all tokens, apply any overrides, convert to `ESTreeToken`s, and save them in a `Vec`.
* Pass 3: Serialize the `Vec<ESTreeToken>` with `ESTree` trait (JSON serializer).
Compress this all to a single pass:
* Create an `ESTreeSequenceSerializer` which can have items sent to it to serialize one-by-one (streaming).
* Walk AST.
* When encounter a token which needs its `Kind` altered:
* Flush all previous tokens, serializing each in turn, using its default `Kind`.
* Serialize the current token with the amended `Kind`.
* Continue walking.
* At the end, flush any remaining tokens to the serializer.
This avoids multiple passes, and also removes the `Vec<ESTreeToken>` which was just intermediate data, and grew very large.
+18% perf improvement on ESTree tokens benchmark.
Refactor.
Previously we converted spans from UTF-8 to UTF-16 in AST first, then serialized tokens. This meant that when serializing tokens, we had to convert spans to UTF-16 before comparing offsets of overrides to the token's start offset (because the override offsets are taken from the AST nodes during the AST walk pass).
Instead, convert token spans first, *then* the AST. This removes span conversion from the critical path. On a CPU with instruction-level parallelism, other work can start without being blocked on waiting for the result of span conversion, and the two can happen in parallel.
This should be a slight perf improvement, though it likely won't show up on CodSpeed benchmarks as CodSpeed's CPU simulation to basic to emulate instruction-level parallelism.
But mainly, the motivation is that this makes the process simpler and easier to reason about.
Fix a bug where we didn't produce any tokens for files which start with a BOM.
As a side effect, re-use the existing `Utf8ToUtf16` span converter created for converting spans in the AST again for tokens, rather than creating another one.
Refactor. We had pointlessly complex logic where caller would call `collect_token_context` and then pass the result back into some other tokens code `to_estree_tokens_json`. Combine the 2 functions to make the call sites simpler.
Add benchmarks for:
1. Parsing with tokens.
2. Serializing tokens to ESTree-style JSON.
Neither of these benchmarks needs to be kept indefinitely, but they'll be useful while working on the tokens code. There's a lot of room for optimization.
### What this PR does
Introduce `ParserConfig` trait (another try at #16785).
The aim is to remove the large performance regression in parser that #19497 created, by making whether the parser generates tokens or not a compile-time option.
`ParserConfig::tokens` method replaces `ParseOptions::collect_tokens` property. The former can be const-folded at compile time, where the latter couldn't.
### 3 options
This PR also introduces 3 different config types that users can pass to the parser:
* `NoTokensParserConfig` (default)
* `TokensParserConfig`
* `RuntimeParserConfig`
The first 2 set whether tokens are collected or not at compile time. The last sets it at runtime.
All 3 implement `ParserConfig`.
`NoTokensParserConfig` is the default, and is what's used in compiler pipeline. It switches tokens off in the parser, and makes all the tokens-related code dead code, which the compiler eliminates. This makes the ability of the parser to generate tokens zero cost when it's not used (in the compiler pipeline).
`TokensParserConfig` is the one to use where you *always* want tokens. This is probably the config that linter will use.
`RuntimeParserConfig` is the one to use when an application decides whether to generate tokens or not at runtime. This config avoids compiling the parser twice, at the cost of runtime checks. This is what NAPI parser package will use.
### Future extension
#### Supporting additional features
In future we intend to build the UTF-8 to UTF-16 offsets conversion table in the parser. This will be more performant than searching through the source text for unicode characters in a 2nd pass later on. But this feature is only required for uses of the parser where we're interacting with JS side (NAPI parser package, linter with JS plugins).
`ParserConfig` can be extended to toggle this feature on/off at compile time or runtime, in the same way as you toggle on/off tokens.
#### Options and configs
This PR introduces `ParserConfig` but leaves `ParseOptions` as it is. So we now have 2 sets of options, passed to `Parser` with `with_options(...)` and `with_config(...)`. This is confusing.
We could merge the 2 by making `ParseOptions` implement `ParserConfig`, so then you'd define all options with one `with_options` call.
This would have the side effect of making all other parser options (e.g. `preserve_parens`) able to be set at either runtime or compile time, depending on the use case.
For users consuming `oxc_parser` as a library, with specific needs, they could also configure `Parser` to their needs e.g. create a parser which only handles plain JS code with all the code paths for JSX and TS shaken out as dead code. This would likely improve parsing speed significantly for these use cases.
### Implementation details
Why a trait instead of a cargo feature?
IMO a trait is preferable for the following reasons:
1. We have 3 different use cases we need to support (the 3 provided configs). 3 different Cargo features would be unwieldy.
2. This situation would become far worse once we introduce more features e.g. UTF-8 -> UTF-16 conversion.
3. No problems around feature unification. We found Cargo features caused headaches when we used them in linter for toggling on/off JS plugins support.
4. No clippy errors which only appear when the feature is/isn't disabled, requiring complex `#[cfg_attr(feature = "whatever", expect(clippy::unused_async))]` etc.
The introduction of a trait does not seem to significantly affect compile time:
```
# Before this PR
cargo build -p oxc_parser 15.33s user 3.27s system 254% cpu 7.316 total
cargo build -p oxc_parser --release 17.36s user 2.26s system 231% cpu 8.477 total
cargo build -p oxc_parser --example parser 18.43s user 3.75s system 271% cpu 8.156 total
cargo build -p oxc_parser --example parser --release 32.52s user 2.59s system 180% cpu 19.454 total
# After this PR
cargo build -p oxc_parser 15.00s user 3.24s system 272% cpu 6.692 total
cargo build -p oxc_parser --release 16.71s user 2.12s system 287% cpu 6.539 total
cargo build -p oxc_parser --example parser 18.50s user 3.91s system 285% cpu 7.845 total
cargo build -p oxc_parser --example parser --release 33.48s user 2.63s system 169% cpu 21.263 total
```
Measured on Mac Mini M4 Pro, `cargo clean` run before each. The difference appears to be mostly within the noise threshold.