mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/fix-semantic-parameter-resolution
184 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0389c4010e |
release(crates): oxc v0.146.0 (#25912)
### 🚀 Features - |
||
|
|
4a4a1fb2e7 |
release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES - |
||
|
|
b6f1458dec |
release(crates): oxc v0.144.0 (#25474)
### 💥 BREAKING CHANGES - |
||
|
|
030857b509 |
refactor(diagnostics): update oxc-miette to v4 (#25395)
Remove the miette `Report` wrapper and update oxc-miette to v4's narrowed diagnostic and source protocol. AI-assisted by OpenAI Codex. |
||
|
|
cc2e28d3e3 |
fix(parser): reject trailing tokens in Parser::parse_expression (#25371)
## Summary `ReplaceGlobalDefinesConfig` validates replacement values with `Parser::parse_expression`, which previously accepted a valid prefix and ignored the remaining tokens. Require the parser to consume the whole input so invalid values such as `console.log foo` are rejected instead of being truncated. Fixes #25369 |
||
|
|
6be314f226 |
feat(ast)!: remove duplicated VariableDeclarator::kind (#25319)
## Summary
- Removes `VariableDeclarator::kind`.
- Makes `VariableDeclaration::kind` the single source of truth for every declarator in its declaration list.
- Makes contradictory parent and child declaration kinds unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(skip)]
pub kind: VariableDeclarationKind,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
to:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
## Why?
A variable declaration has one declaration kind shared by every declarator in its declaration list:
```js
const first = 1, second = 2;
```
Previously, the AST stored that kind on both the parent declaration and every child declarator:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Const, ... },
VariableDeclarator { kind: Const, ... },
],
}
```
This allowed contradictory ASTs to be constructed:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Var, ... },
],
}
```
Different consumers read different copies:
- Code generation used `VariableDeclaration::kind`.
- Semantic binding used `VariableDeclarator::kind`.
- Lint rules, transforms, and minifier passes used a mixture of both.
As a result, one AST could be emitted as `const` while its binding was treated as function-scoped `var`.
Transforms also had to keep the copies synchronized manually. For example, const-to-let normalization updated the parent and then looped through every declarator to update each child. Explicit resource management performed similar paired mutations for `using` declarations.
Keeping the kind only on `VariableDeclaration` removes this synchronization requirement and prevents consumers from observing conflicting declaration semantics.
## Declaration forms
The declaration kind now belongs exclusively to the node that owns the declaration list:
```js
let value;
```
```text
VariableDeclaration {
kind: Let,
declarations: [
VariableDeclarator { id: value, ... },
],
}
```
Multiple declarators share the same parent kind:
```js
const first = 1, second = 2;
```
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { id: first, ... },
VariableDeclarator { id: second, ... },
],
}
```
## How to migrate
Code that previously read the kind from a declarator:
```rust
if declarator.kind.is_const() {
// ...
}
```
should now read it from the containing declaration:
```rust
if declaration.kind.is_const() {
// ...
}
```
Consumers operating through an AST node store should retrieve the parent `VariableDeclaration`:
```rust
let AstKind::VariableDeclaration(declaration) =
nodes.parent_kind(declarator.node_id())
else {
unreachable!();
};
let kind = declaration.kind;
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `declarator.kind` | Containing `declaration.kind` |
| Match `VariableDeclarator::kind` during semantic binding | Read the parent `VariableDeclaration` from ancestry |
| Pass `kind` to `VariableDeclarator::new` | Remove the `kind` argument |
| Update parent and every child kind | Update only `VariableDeclaration::kind` |
| Recover kind after detaching a declarator | Capture or pass the parent kind before detaching it |
| Inspect a symbol’s declarator kind in lint rules | Resolve its parent declaration through the node store |
When constructing declarators, use the generated kindless `VariableDeclarator` builders and place them inside a `VariableDeclaration` carrying the required kind.
fixes https://github.com/oxc-project/backlog/issues/237
|
||
|
|
45a17c25d1 |
release(crates): oxc v0.143.0 (#25250)
### 💥 BREAKING CHANGES - |
||
|
|
1bdedd11ca |
feat(ast)!: introduce ExportDeclaration, ExportFromDeclaration (#25095)
## Summary
Split the overloaded Rust `ExportNamedDeclaration` into syntax-specific AST nodes:
- `ExportDeclaration` for exported declarations
- `ExportNamedDeclaration` for local exports
- `ExportFromDeclaration` for re-exports
## Breaking Change
The Rust AST changes from:
```rust
pub struct ExportNamedDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub declaration: Option<Declaration<'a>>,
pub specifiers: Vec<'a, ExportSpecifier<'a>>,
pub source: Option<StringLiteral<'a>>,
pub export_kind: ImportOrExportKind,
pub with_clause: Option<Box<'a, WithClause<'a>>>,
}
pub enum ModuleDeclaration<'a> {
// ...
ExportNamedDeclaration(Box<'a, ExportNamedDeclaration<'a>>),
// ...
}
```
to:
```rust
pub struct ExportDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub declaration: Declaration<'a>,
}
pub struct ExportNamedDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub specifiers: Vec<'a, ExportSpecifier<'a>>,
pub export_kind: ImportOrExportKind,
}
pub struct ExportFromDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub specifiers: Vec<'a, ExportSpecifier<'a>>,
pub source: StringLiteral<'a>,
pub export_kind: ImportOrExportKind,
pub with_clause: Option<Box<'a, WithClause<'a>>>,
}
pub enum ModuleDeclaration<'a> {
// ...
ExportDeclaration(Box<'a, ExportDeclaration<'a>>),
ExportNamedDeclaration(Box<'a, ExportNamedDeclaration<'a>>),
ExportFromDeclaration(Box<'a, ExportFromDeclaration<'a>>),
// ...
}
```
## Why?
The previous node represented three distinct syntax forms through optional and mutually exclusive fields:
```text
export const value = 1; -> ExportDeclaration
export { value }; -> ExportNamedDeclaration
export { value } from "mod"; -> ExportFromDeclaration
```
This allowed invalid combinations such as declarations with specifiers or sources, and import attributes without a source. The new nodes make these states unrepresentable and remove repeated `declaration`/`source` branching from consumers.
## How to migrate
Match the syntax-specific `ModuleDeclaration`, `Statement`, or `AstKind` variant instead of inspecting optional fields. Generated builders, visitors, traversal ancestors, and formatter wrappers now expose corresponding APIs for all three nodes.
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `declaration: Some(declaration)` | `ExportDeclaration { declaration }` |
| `declaration.as_ref()` | `&export.declaration` |
| `source.is_none()` for local exports | Match `ExportNamedDeclaration` |
| `source: Some(source)` | `ExportFromDeclaration { source }` |
| `source.as_ref().unwrap()` | `&export.source` |
| `with_clause` | Only available on `ExportFromDeclaration` |
| Stored `export_kind` on exported declarations | `ExportDeclaration::export_kind()` |
| `ExportNamedDeclaration::boxed_plain_declaration(...)` | `ExportDeclaration::boxed(...)` |
| `ExportNamedDeclaration::boxed(..., None, specifiers, None, ...)` | `ExportNamedDeclaration::boxed(..., specifiers, ...)` |
| `ExportNamedDeclaration::boxed(..., None, specifiers, Some(source), ...)` | `ExportFromDeclaration::boxed(..., specifiers, source, ...)` |
closes https://github.com/oxc-project/backlog/issues/219
|
||
|
|
7e1199cbbb |
feat(ast)!: remove conversion to Box from AST builder methods (#25038)
AST builder methods accepted an `IntoIn<ArenaBox<T>>` for all params where an `ArenaBox<T>` is required. You could pass a plain `T`, and the builder method would automatically allocate space for it, and copy it into the arena. This was originally intended as a convenience, but in fact it's a footgun - it encourages allocating late, holding types on the stack during other work, which results in register pressure, and churn of copying to/from the stack. Remove the automatic conversion. All places where we were using auto-allocation are already fixed in previous PRs #25034, #25035, #25036, #25037. Additional benefits of this change: 1. AST builder methods become less polymorphic which should help with compile times. 2. We can get rid of the `NONE` type, which was a hacky workaround, necessary for passing as params where an `IntoIn<Option<ArenaBox<T>>>` is required. Now these all become plain `None`. Most of the diff in this PR is just replacing `NONE` with `None` all over the place. Note: AST builder methods still define params which are `ArenaVec`s as `IntoIn<ArenaVec<T>>`, as this does add genuine value - it allows passing `[]` instead of the much more verbose `ArenaVec::new_in(self)`. |
||
|
|
9fb1e3e2e4 |
perf(transformer): avoid large types on stack (#25035)
Continuation of #25034. In transformer, move some allocations to happen earlier. When we need an `ArenaBox<T>` for later method calls, it's preferable to allocate it into arena as swiftly as possible, so only hold an 8-byte `ArenaBox` on stack, instead of the whole `T`, which is much larger. This reduces stack and register pressure. |
||
|
|
fc702c1fa9 |
release(crates): oxc v0.142.0 (#24977)
### 💥 BREAKING CHANGES - |
||
|
|
e935064d33 |
refactor(ast_visit): migrate JS-only mutable visitors (#24849)
## Summary Migrate seven runtime-only `VisitMut` implementations to `VisitJsMut`. This avoids monomorphizing pure TypeScript type-space traversal for these consumers. ## Binary size On arm64 macOS, `cargo build --release -p oxc_transform_napi` produces a stripped dylib that changes from 3,365,584 to 3,332,480 bytes: **-33,104 bytes (-0.98%)**. Its Mach-O `__text` section changes from 2,909,108 to 2,876,892 bytes: **-32,216 bytes (-1.11%)**. AI assistance was used to implement this change. I reviewed and take responsibility for the result. <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a></sub> |
||
|
|
baf022d74d |
refactor(ast)!: remove old AstBuilder methods (#24563)
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. |
||
|
|
14533a3dc1 |
release(crates): oxc v0.141.0 (#24714)
### 💥 BREAKING CHANGES - |
||
|
|
c06982adff |
refactor(ast_visit, isolated_declarations, minifier, transformer): pass array literals for from_array_in builder args (#24624)
Pure refactor. Just shorten code. #24621 made AST builder methods take arrays as params. Replace `ArenaVec::from_array_in(...)` arguments written inline at AST builder method call sites with the array literals. ```rs let block = Statement::new_block_statement(SPAN, ArenaVec::from_array_in([x, y], ctx), ctx); ``` \-> ```rs let block = Statement::new_block_statement(SPAN, [x, y], ctx); ``` |
||
|
|
b28051fb12 |
refactor(all): pass [v] for single-value ArenaVec builder args (#24623)
Pure refactor. Just shorten code. #24621 made AST builder methods take arrays as params. Replace `ArenaVec::from_value_in(v, alloc)` with `[v]` where the `ArenaVec` is passed to an AST builder method. ```rs let block = Statement::new_block_statement( SPAN, ArenaVec::from_value_in(return_stmt, ctx), ctx, ); ``` \-> ```rs let block = Statement::new_block_statement(SPAN, [return_stmt], ctx); ``` |
||
|
|
c1a074cdc8 |
refactor(all): pass [] for empty ArenaVec args to AST builder methods (#24622)
Pure refactor. Just shorten code. #24621 made AST builder methods take arrays as params. Replace `ArenaVec::new_in(...)` arguments written inline at AST builder method call sites with the empty array literal `[]`. ```rs let block = Statement::new_block_statement(SPAN, ArenaVec::new_in(ctx), ctx); ``` \-> ```rs let block = Statement::new_block_statement(SPAN, [], ctx); ``` |
||
|
|
54cc121250 |
feat(ast)!: split MetaProperty into ImportMeta and NewTarget (#24557)
## Summary
- replace the generic Rust MetaProperty AST node with dedicated ImportMeta and NewTarget nodes
- preserve the ESTree MetaProperty shape and generated public TypeScript types
- update parser, formatter, codegen, lint, transform, minifier, traversal, and React compiler consumers
## Summary
Remove `MetaProperty` AST node, in favour of two separate nodes (`ImportMeta` and `NewTarget`)
## Breaking Change
The Rust AST field changes from:
```rust
pub struct MetaProperty<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub meta: IdentifierName<'a>,
pub property: IdentifierName<'a>,
}
```
to:
```rust
/// /// `import.meta` in `console.log(import.meta);`.
pub struct ImportMeta {
pub node_id: Cell<NodeId>,
pub span: Span,
}
/// `new.target` in `function F() { return new.target; }`.
pub struct NewTarget {
pub node_id: Cell<NodeId>,
pub span: Span,
}
```
## Why?
The current AST could represent shapes such as:
```
foo.bar
```
By constructing the node manually. However this doesn't make sense, and is invalid.
## How to migrate?
1. Replace any `Expression::MetaProperty` matches/checks with the dedicated `Expression::ImportMeta` and `Expression::NewTarget` varients
2. Replace any `new_meta_property` construction with the new `new_import_meta` or `new_new_target` APIs.
3. Inspecting the `meta`/`property` identifier names is no longer required.
|
||
|
|
8e0ed2ebb9 |
release(crates): oxc v0.140.0 (#24453)
### 🚀 Features - |
||
|
|
5b26643408 |
perf(transformer_plugins): dispatch global defines by trailing name (#23666)
## Summary `ReplaceGlobalDefines` matched every configured define against every AST node by linearly scanning the full define list — `O(nodes × defines)`. For each identifier reference it scanned all identifier defines; for each member expression it scanned all dot defines and all meta defines. This keys defines by their **trailing segment** (the outermost property name) — the only segment a node can match on — so each node becomes a single hash lookup instead of a full scan: - `identifier` defines → `FxHashMap<name, value>` - `dot` defines (`process.env.NODE_ENV`) → `FxHashMap<last_part, Vec<DotDefine>>` - non-wildcard `meta` defines (`import.meta.env.MODE`) → `FxHashMap<last_part, Vec<MetaPropertyDefine>>` - wildcard `meta` defines (`import.meta.env.*`) match a *variable* trailing name, so they can't be keyed; they stay a small linear list, scanned only for `import.meta`-rooted expressions. The config-time `sort` is removed — "specific wins over wildcard" is now structural (the map is consulted before the wildcard list). Computed members (`a["b"]`) extract the static key once and skip entirely when the key is dynamic. ## Correctness `is_meta_property_define` now verifies `import` / `meta` at its `MetaProperty` arm, so `new.target.env` is no longer wrongly replaced by an `import.meta.env` define (previously it matched *any* meta property — `import.meta` **or** `new.target`). Added a regression test. ## Benchmark Isolated `ReplaceGlobalDefines::build()` (re-parsing outside the timer) with a ~60-entry Vite-like define config: | file | before | after | speedup | |---|--:|--:|--:| | RadixUIAdoptionSection.jsx | 2.38 µs | 0.64 µs | 3.7× | | react.development.js | 72.2 µs | 39.0 µs | 1.9× | | App.tsx | 852 µs | 307 µs | 2.8× | | binder.ts | 243 µs | 88 µs | 2.8× | | kitchen-sink.tsx | 1345 µs | 677 µs | 2.0× | Speedup scales with the number of defines. With a single define the two are roughly even (an `FxHash` of a short property name ≈ the previous single leaf-compare), so no regression on small configs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
69b2dfc181 |
release(crates): oxc v0.139.0 (#24218)
### 🚀 Features - |
||
|
|
ff10855dce |
perf(transformer): use ReplaceWith instead of TakeIn (#24015)
Replace `take_in` with `replace_with` in as many places as possible in transformer. See #24012 for explanation of the advantage of `replace_with`. This has only a minimal perf effect according to Codspeed benchmarks (1% improvement). But it does cut the number of arena allocations on TS files by 50%, so reduces memory usage. |
||
|
|
8bf69a3076 |
release(crates): oxc v0.138.0 (#23934)
### 💥 BREAKING CHANGES - |
||
|
|
326fe25f3a |
feat(transformer_plugins): support typeof define keys (#23605)
This PR adds support for `define` so it does accept `typeof` expression keys in addition to what is supported at the moment. ## Why support `typeof` define keys? SSR frameworks need this behavior as they build the same code for different runtimes. Checks such as `typeof window === "undefined"` are used to choose between browser and server code. Replacing that expression at build time lets the bundler remove dead branches and their imports. Unlike defining `window` itself, it does not alter real uses such as `window.location` (which could cause bugs in prod). Supporting typeof keys in Oxc keeps the behavior in the AST-aware define transform and makes it available consistently to both Vite and Rolldown without framework-specific replacement plugins. ## Real World benefits/use cases - **TanStack Start / React Router:** Their shared hydration and router packages contain many `typeof window` and `typeof document` branches that Vite could eliminate per environment. - **Nuxt:** Uses `typeof window` and `typeof document` with webpack alredy. Its Vite build currently has to use the broader workaround of defining the globals themselves as `undefined`. - **Vinext:** Needs a custom AST transform to match Next.js, which folds `typeof window` in its compiler. - **General client-only deps:** Applications often guard DOM libraries with `typeof window !== "undefined"`. Folding the check lets server builds remove the guarded code and imports entirely. |
||
|
|
3d61dea7e2 |
docs(all): correct capitalization in comments (#23887)
Just fix capitalization in comments. |
||
|
|
94fbacb524 |
feat(ast)!: only export AstBuilder and NONE in builder module (#23876)
Part of #23043. Do not export `AstBuilder` and `NONE` from root of `oxc_ast` crate. They are available via `builder` module e.g. `use oxc_ast::builder::NONE;`. We're going to be adding more AST builder types, and I don't think we should throw them all into the crate's top-level exports. This change is behind the `disable_old_builder` Cargo feature (see #23875) so is not an immediate breaking change - consumers would need to make this change while migrating to new `AstBuilder`. |
||
|
|
f2091b3846 |
feat(ast): unify old and new AstBuilders (#23875)
Part of #23043. Recent PRs have introduced a new `AstBuilder` type, and migrated all Oxc's crates over to it. To allow downstream consumers to incrementally migrate from old `AstBuilder` to new one, unify the two implementations into a single `AstBuilder` type. Remove the `builder_new` Cargo feature and instead add a new feature `disable_old_builder`. When enabled, this feature removes all the methods from `AstBuilder`, and makes `AstBuilder` non-`Copy`. Consumers should migrate, and then enable this feature to ensure later changes don't use the methods which are slated for removal. Enable the `disable_old_builder` feature in all Oxc crates which use `AstBuilder` _in tests_. This ensures we don't go back to using the deprecated methods, while not forcing the feature on consumers (e.g. Rolldown) who use e.g. `oxc_transformer` but also use `AstBuilder` directly. |
||
|
|
83b64d8508 |
refactor(ast): hide allocator field of new AstBuilder (#23874)
Part of #23043. Do not expose `allocator` field of new `AstBuilder`. It's available via `allocator` method provided by `GetAllocator` trait. This gives us more flexibility in future. |
||
|
|
063a65d314 |
refactor(transformer): shorten AST builder code (#23858)
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. |
||
|
|
dc0ef389c0 |
feat(transformer)!: switch to new AstBuilder (#23831)
Part of #23043. Migrate `oxc_transformer` and `oxc_traverse` over to new `AstBuilder`. This PR is a breaking change to users of `Traverse`, but I intend to rectify that in a later PR (before next crates release), so that these changes to `AstBuilder` are not a breaking change, and downstream projects can incrementally migrate over to the new `AstBuilder`. |
||
|
|
e1886a0c76 |
perf(transformer, minifier): use static_ident! macro to create static Idents (#23727)
Optimize the use of `Ident`s in transformer (and a couple in minifier).
2 separate optimizations:
### 1. Reduce allocations
There's no need to allocate strings into arena for static identifiers - can just refer to the `&'static str` without allocating it.
i.e. `ctx.ast.ident("require")` -> `static_ident!("require")`.
### 2. Hash idents at compile time not run time
Where an `Ident` is created directly from a static string, calculating the hash can be const-folded and performed at compile time. But for dynamic strings, the hash is calculated at runtime.
```rust
// Before - runtime hash calculation (performed in `identifier_name` via `Into<Ident>`)
let name = if needs_await { "a" } else { "u" };
let id = ctx.ast.identifier_name(SPAN, name);
// After - compile time hash calc (pass `identifier_name` a precalculated `Ident`)
let name = if needs_await { static_ident!("a") } else { static_ident!("u") };
let id = ctx.ast.identifier_name(SPAN, name);
```
Similarly, passing strings into a shared function and converting to `Ident` in the function will perform runtime hashing. Instead, make the function take an `Ident`, and create the `Ident`s in the callers with `static_ident!(...)` (compile time calc). `&str` and `Ident` are both 16 bytes, so it makes no difference to the cost of calling the function, only removes work within the function.
Alter `create_member_callee` (class properties transform) and `create_global_identifier` (decorators transform) to take an `Ident`, to avoid the runtime hash calculations.
|
||
|
|
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};
```
|
||
|
|
680ffbc225 |
perf(transformer): allocate AST nodes in arena directly (#23711)
Same as #23709. Small perf optimizations around AST builder calls. - AST nodes which end up in `Box`es, allocate into the `Box` as early as possible, to increase chance compiler sees the type can be built directly in arena, rather than built on the stack, and then copied from stack into arena. - Functions return `Box<T>` rather than `T` where the value needs to be boxed anyway. If function is not inlined, this avoids stack allocation + copy. Also, shorten code where possible by using more specific `AstBuilder` methods. In `transform_ts_import_equals`, additionally avoid unnecessarily allocating the string `"require"` into arena, by using `static_ident!` instead. |
||
|
|
36009ddf68 |
feat(allocator)!: GetAllocator::allocator take &self (#23676)
Make `GetAllocator::allocator` method take `&self`, instead of `self`. Alter `TakeIn::take_in` accordingly - now takes `&A` where`A:GetAllocator` (previously took owned `A`). This requires changing every `take_in` / `take_in_box` call site. However, this change also means that `GetAllocator` can be implemented on all types which hold an `&Allocator` e.g. `TraverseCtx`, which shortens the code everywhere. - Before: `expr.take_in(ctx.ast)` - After: `expr.take_in(ctx)` Change all those call sites to this shorter version. This matches the pattern that new `AstBuilder` is going to introduce (#23043), and clears the way for that change. |
||
|
|
6d56d57535 |
release(crates): oxc v0.137.0 (#23591)
### 💥 BREAKING CHANGES - |
||
|
|
bcb3894d76 |
perf(minifier): incremental scoping refresh, delete LiveUsageCollector (#23197)
> Developed with AI assistance (Claude Code); reviewed, tested, and benchmarked by the contributor. ## Summary Deletes the per-pass `LiveUsageCollector` walk over the entire live program. The typed mutation helpers (previous PR in this stack) now walk only the dropped/replaced subtrees in place, accumulating dead `ReferenceId`s into a per-pass `PassDirty` arena `BitSet` that the `Compressor` driver consumes in one batch (after `Normalize` and after each peephole pass) via the new `Scoping::retain_resolved_references_excluding`. Direct-eval flag refresh runs only when an `eval(...)` call was actually dropped. Cross-crate support riding in this PR: `Scoping::retain_resolved_references_excluding` (oxc_semantic) and `BitSet::capacity()` / `is_empty()` / `contains()` (oxc_allocator). ## Breaking Removes `oxc_semantic::Scoping::retain_resolved_references` — its only consumer was `LiveUsageCollector`, deleted here. No other in-repo or known downstream callers. ## Benchmarks (CodSpeed, this PR vs stack base) | Benchmark | Base → Head | Efficiency | |---|---|---| | `minifier[kitchen-sink.tsx]` | 63.6 ms → 52.5 ms | **+21.2%** | | `pipeline[kitchen-sink.tsx]` | 122.3 ms → 110.7 ms | +10.5% | | `minifier[App.tsx]` | 12.5 ms → 12.1 ms | +3.3% | | everything else | — | unchanged | > **Why earlier runs showed ~2× on kitchen-sink:** the initial cut of this branch had a reference-leak bug — dropped subtrees weren't always marked dead, leaked references made symbols look used, and the minifier silently skipped most of kitchen-sink's DCE, producing output **22% larger than main** (invisible to minsize, which doesn't include that file). Those runs measured a minifier doing less work, not a faster minifier. The leak is fixed (see below); the numbers above are measured at byte-identical output to main. Memory: kitchen-sink.tsx arena allocs 100230 → 90553, sys allocs 2917 → 2639; all other tracked files at or slightly below base. ## Relation to #22736 This re-cuts the approach prototyped and discussed in #22736 (kept open during development as the design record), with three deviations from the POC: 1. **Mark-only `DropDiff`** — the POC's "Resurrect" mode existed for one call site (`substitute_is_object_and_not_null`) that reused `ReferenceId`s in replacement values. That site now mints fresh references (same symbol/resolution, new id), so the resurrect walk and its cross-call aliasing invariant are gone entirely. 2. **`mutated: bool` instead of a `mutations: u64` snapshot counter** — after this PR the signal's only reader is the fixed-point loop, so a private bool with read-and-reset `take_mutated()` suffices. 3. **Bounds-checked mark path** — references minted mid-pass and dropped in the same pass are skipped (treated live → conservative output) instead of the POC's debug-assert + potential release panic. Safety net: `debug_assert_no_over_prune` walks the live program in debug builds after each dirty pass, so the whole test + conformance corpus doubles as an over-prune detector at zero release cost. ## Reference-leak fix + simplification follow-ups The initial cut leaked references at three classes of drop sites. The leak direction is safe (stale extra references block optimizations, never corrupt output), but it cost real output size on `void ident`-heavy inputs — kitchen-sink.tsx minified 22% larger than main: 1. **`Normalize` raw drops** (`void x` → `void 0`, `drop_console`) never reached `PassDirty`. `Normalize` now records drops through the same typed helpers, and the driver flushes once before the fixed-point loop so pass 1 already observes pruned reference counts (no extra peephole pass). 2. **`remove_unused_template_literal`** silently drained elements that `remove_unused_expression` collapsed to nothing, without a drop walk. 3. **Dropped variable declarators** walked only `init`; references in the binding's TS type annotation (e.g. computed keys in a type literal) leaked. New `drop_variable_declarator` helper walks the whole declarator. Each class has a leak-regression test; kitchen-sink.tsx output is verified **byte-identical to main**. Two simplification passes followed: flush hoisted into the `Compressor` driver (no cross-traversal coupling), `DropDiff` reduced to a thin `Visit` over `&mut PassDirty` (no scoping lookup on the mark hot path; marks for unresolved refs are inert), `BitSet::clear()` reuse instead of a per-pass arena realloc, a shared `as_direct_eval_call` predicate for producer and consumer, plain `clone_in` (id-less) at the one site that cloned semantic ids, and removal of dead `drop_console` repair code. ## Verification - `cargo test -p oxc_minifier -p oxc_mangler -p oxc_semantic -p oxc_allocator` — all pass, incl. the new leak-regression tests - `just minsize` — **bit-identical output**; kitchen-sink.tsx (not in minsize) separately verified byte-identical to main - `cargo coverage -- minifier` — test262 44825/44829, babel 1785/1785, snapshots unchanged - clippy (all features/targets), fmt clean ## Test alignment in oxc_transformer_plugins The `replace_global_defines` integration-test helper previously reused RGD's returned scoping directly for DCE — a path production does not take: `crates/oxc/src/compiler.rs` marks scoping dirty whenever RGD changed the AST (`scoping_dirty |= ret.changed`) and rebuilds before compress/DCE, and rolldown minifies freshly-parsed chunks. With the full-walk `LiveUsageCollector` gone, that test-only path surfaced stale ambient-`declare` references. The helper now mirrors the production pipeline (rebuild when `ret.changed`, reuse otherwise). No `ReplaceGlobalDefines` source changes. Two `declare`-define contract tests added. ## Known trade-off: defines-heavy DCE pays O(dropped) drop-walks Profiling `pipeline[react.development.js]` (with `process.env.NODE_ENV` defines) shows the worst case of walk-what-you-drop: DCE drops one `if` subtree covering ~90% of the program's nodes, so the drop-walk approaches a full-program traversal where the old per-pass live walk only covered the small remainder (−3% CodSpeed / +7% local wall on that bench; attribution: ~72% drop-walks, 0% double-marking, ~0% bitset cost). A bounded-walk + live-walk-fallback mitigation was prototyped and validated (recovers to +1.4%, all other wins preserved), but we decided the added mechanism wasn't worth the maintenance cost for a one-bench regression — the same file's plain minification *improves* 12%, and kitchen-sink-class inputs (the load-bearing case) improve ~2×. Revisit if real-world defines-heavy workloads surface it. |
||
|
|
827248a7fb |
release(crates): oxc v0.136.0 (#23443)
### 💥 BREAKING CHANGES - |
||
|
|
52d0c31ed4 |
fix(transformer): replace ambient dot defines (#23231)
## Summary Dot define replacement now treats ambient TypeScript declarations as replaceable global roots. This fixes cases like service worker code that declares `self` for type checking while still expecting `self.__WB_MANIFEST` to be replaced. Before this change, dot defines only matched when the root identifier was unresolved/global: ```ts foo.bar ``` would replace only if `foo` had no local symbol. In the bug case: ```ts declare let self: ServiceWorkerGlobalScope; precacheAndRoute(self.__WB_MANIFEST); ``` semantic analysis resolves `self` to the local `declare let self` symbol, so `ident.is_global_reference(scoping)` returned `false` and `self.__WB_MANIFEST` was treated as shadowed. The new helper still allows true globals, but also allows symbols marked ambient: ```rust .is_none_or(|symbol_id| scoping.symbol_flags(symbol_id).is_ambient()) ``` That means: - unresolved `self` -> replace - `declare let self` -> replace, because it is TypeScript-only and not a runtime binding - `let self` or function parameter `self` -> do not replace, because those are real runtime shadows So this fixes the bug without making dot defines scope-insensitive. Fixes #23224 |
||
|
|
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) |
||
|
|
e9b8fd595c |
release(crates): oxc v0.135.0 (#23117)
### 💥 BREAKING CHANGES - |
||
|
|
6c30f81eca |
release(crates): oxc v0.134.0 (#22882)
### 🚀 Features - |
||
|
|
83328b65ce |
release(crates): oxc v0.133.0 (#22734)
### 🚀 Features - |
||
|
|
702b14e808 |
fix(minifier): preserve IIFE structure in DCE-only mode (#22547)
Fixes upstream rolldown/rolldown#9437. Supersedes the `iife_inline_would_lose_pure` gate from #22349. ## Bug In DCE-only mode (rolldown's per-module preprocess via `Compressor::dead_code_elimination_with_scoping`), `substitute_iife_call` inlines `const x = /* @__PURE__ */ (() => [1, 2, 3])()` down to `const x = [1, 2, 3]`, dropping the `@__PURE__` annotation downstream tree-shakers rely on. The previous `iife_inline_would_lose_pure` gate only bailed for side-effecting bodies, so array / object / primitive bodies still got inlined. ## Cross-tool comparison Empirical, same input `export const x = /* @__PURE__ */ (() => [1, 2, 3])();`: | Tool / Mode | Result | | ---------------------------------------- | ---------------------- | | Rollup 4.60 `treeshake: true` | preserved | | esbuild 0.27 `--tree-shaking=true` | preserved | | SWC 1.15 default (no minify) | preserved | | Terser 5.46 no `-c` (no compress) | preserved | | Terser 5.46 `-c` (compress / minify) | inlined to `[1, 2, 3]` | | **oxc DCE-only — before this PR** | inlined to `[1, 2, 3]` | | **oxc DCE-only — after this PR** | preserved | | oxc full-minify (unchanged by this PR) | inlined to `[1, 2, 3]` | DCE-only is "tree-shake without minify" — the analogous reference is the "non-minify" rows. oxc DCE-only was the only mode inlining there. ## Fix `try_take_iife_body` bails on `ctx.state.dce` directly. The `iife_inline_would_lose_pure` helper is removed. Full-minify is untouched — `state.dce` is only set to `true` for `dead_code_elimination[_with_scoping]` entry points (exactly two writers in `compressor.rs`). DCE-only IIFE rewrites that **still fire** (real DCE): - Dropping pure-annotated IIFEs whose result is unused — `is_expression_result_unused` → `void 0` - Replacing fully-empty IIFEs with `void 0` - Dropping unused-var pure IIFEs via #22349's widened `is_expression_result_unused` (the original #17480 fix) Covered by `preserve_iife_in_dce_mode` (renamed + extended from `preserve_pure_iife_in_used_position_for_downstream_treeshake`). 501 / 501 `cargo test -p oxc_minifier` pass, idempotent under `--twice`, `minsize` / `allocs` snapshots unchanged. |
||
|
|
63b129182d |
release(crates): oxc v0.132.0 (#22527)
### 🐛 Bug Fixes - |
||
|
|
564ebbf46f |
release(crates): oxc v0.131.0 (#22459)
### 🚀 Features - |
||
|
|
650417ffd0 |
release(crates): oxc v0.130.0 (#22316)
### 🚀 Features - |
||
|
|
a099b03c61 |
fix(codegen): emit call end mapping at ) position, not past it (#22199)
Follow-up on #22001. Move `add_source_mapping_end(span)` to before `print_ascii_byte(b')')` in `print_arguments` so the call's end mapping lands at the generated position **of** `)` rather than one past it. Each generated paren now has at most one mapping at its exact position, matching the convention used by esbuild and Babel. A second follow-up (#22200) extends the same fix to block-end `}`, curly-braces `}`, and the `]`/`}` of `ArrayExpression` / `ObjectExpression`. ## Why After #22001, `add_source_mapping_end` correctly points the source side at `span.end - 1` (the closing source char), but the generated position was still placed AFTER `)` was printed. For chained calls like `factory()()`, this collided with the next AST node's start position: - inner call's end mapping at `gen 7:9 → src 7:8` (just past inner `)`) - the outer call's `(` lives at `gen 7:9` in the generated code Result: a V8 stack-trace lookup at `gen 7:9` (the outer `(`) returned `src 7:8` (the inner `)`) — one column off. ## How Emit the end mapping at the gen position **of** `)`, not after it. Now: - `gen 7:8 → src 7:8` (inner `)`) - `gen 7:9` has no direct mapping → falls back to inner end → `src 7:8` - `gen 7:10 → src 7:10` (outer `)`) The chained-call shadow at the outer `(` becomes a plain "no mapping here, fall back to inner `)`" — same accepted behavior as esbuild, Babel, and TypeScript. None of them special-case chained calls. ## Visual comparisons Generated against a fixture covering classes, arrow bodies, arrays, objects, and chained calls in the [source-map viewer](https://source-map-viewer.void.app): - **Pre-PR vs Full fix** (full picture): https://source-map-viewer.void.app/compare?a=j9GHxMTX&b=UcCYKDih - **PR-only vs Full fix** (just the changes #22199 + #22200 add): https://source-map-viewer.void.app/compare?a=ngs4XAsh&b=UcCYKDih - **esbuild vs Full fix** (alignment with esbuild): https://source-map-viewer.void.app/compare?a=26JVF9DI&b=UcCYKDih ## Reference esbuild (`internal/js_printer/js_printer.go`): ```go if e.CloseParenLoc.Start > expr.Loc.Start { p.addSourceMapping(e.CloseParenLoc) } p.print(")") ``` Babel (`packages/babel-generator/src/printer.ts::rightParens`): ```ts this.sourceWithOffset("end", node.loc, -1); this.token(")"); ``` ## Test plan - [x] New test `call_end_mapping_lands_at_close_paren` — pins both `)` mappings of `factory()()` to their close-paren positions - [x] `cargo test -p oxc_codegen` passes (99 tests) - [x] `stacktrace_is_correct` snapshot unchanged |
||
|
|
2493bdde8e |
fix(codegen): correct sourcemap end mappings for closing delimiters (#22001)
This fixes incorrect sourcemap end mappings for closing delimiters emitted by `oxc_codegen`. This came up while investigating [Remix 3](https://remix.run/) test coverage reporting for code transformed with Oxc via the Remix `assets` package, which we were using in our `test` package to run in-browser tests. V8 reported uncovered functions and branches correctly, but `v8-to-istanbul` could not always resolve the end of those covered ranges back through Oxc's sourcemap, which caused coverage to be over-reported. This meant that we saw cases of 100% coverage being incorrectly reported because `v8-to-istanbul` dropped the whole function block when it could not map the closing `}` line of an uncalled function. `add_source_mapping_end` used `Span::end` directly even though spans are end-exclusive, so the generated token could land one character past the real source span. Block printers also did not emit end mappings for closing `}` consistently. Consumers that resolve V8 coverage range ends through the sourcemap can then miss those block boundaries and over-report coverage. This PR maps end tokens to `span.end - 1` and emits them for block closers, then extends the sourcemap tests with: - a runtime stacktrace regression for normal function and arrow block bodies - a direct regression for the synthesized wrapper-block path used to preserve `else` binding The snapshot updates in `stacktrace_is_correct` are expected for the same reason. Those column numbers come from Node consuming the generated sourcemap at runtime. For example, one frame changes from `/project/input.js:10:12` to `/project/input.js:10:11` for the source line `obj.fn({a})()`. In 1-based columns, that line is `o1 b2 j3 .4 f5 n6 (7 {8 a9 }10 )11 (12 )13`. The error happens in the first call expression, so the end position should resolve to column 11, the first `)`, which is the closing character of `obj.fn({a})`. Before this change it resolved to column 12, the next `(`, because the end mapping pointed one character too far to the right. After switching end mappings to the final real source character and adding the missing block-closer mappings, that frame moves left by one column to the correct source position. Please note, I created this PR with AI assistance, but I worked closely with the agent to design, review, and iterate on the changes until it was at a level I was happy with. |
||
|
|
ec00055e64 |
release(crates): oxc v0.129.0 (#22108)
### 💥 BREAKING CHANGES - |