mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/parser-grouped-expression-allocation
1792 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60fa13878c |
release(crates): oxc v0.149.0 (#26397)
### 💥 BREAKING CHANGES - |
||
|
|
116c8b8303 |
fix(transformer/class-properties): keep private methods in class-expression scope (#26308)
Private methods lowered from a class expression inside a concise arrow
were inserted next to the arrow's outer statement, while private field
storage remained inside the arrow. Starting with Oxc 0.143, this could
leave the generated helper unable to resolve the private field and throw
a `ReferenceError` at runtime.
Concise arrow bodies have no statement list where the helper declaration
can be inserted. In that context, the helper is now emitted as an
assignment in the class-expression sequence, with its `var` binding
inside the expanded arrow body. Ordinary statement-backed class
expressions keep block-local function declarations so classes created by
repeated block evaluation retain their own captured values. The
generated symbol flags and scope parents match the emitted form in both
cases.
## Example
```js
const makeClass = () => class {
#field = 41;
#read() { return this.#field; }
run() { return this.#read() + 1; }
};
new (makeClass())().run();
```
Before Oxc emitted a helper outside `makeClass`, and the call threw
`ReferenceError: _field is not defined`. After this change it returns
`42`.
Verified with the private-method transform conformance suite, runtime
regressions for concise arrows and repeated block evaluation, `cargo
test -p oxc_transformer`, and transformer clippy.
Fixes the upstream cause of
[rolldown/rolldown#10818](https://github.com/rolldown/rolldown/issues/10818).
AI assistance: Cursor was used for the original patch; OpenAI Codex was
used for follow-up review, fixes, and testing.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Dunqing <dengqing0821@gmail.com>
|
||
|
|
d4b4e61cf6 |
docs(transformer): document ES2015 target floor (#26378)
Oxc Transformer supports output down to ES2015, but the contributor guidance did not state that floor and the Rust API docs still listed `es5`. This made plugin-only transform combinations look like supported target configurations. Document the ES2015 floor and require target-dependent regressions to be reproducible through public target selection. Legacy runtime targets may still select available transforms, but do not imply complete compatibility with pre-ES2015 engines. Stacked on #26326. Verified with `cargo fmt --check`, `just doc`, and `cargo test -p oxc_transformer`. `just ready` completed formatting and workspace checks, but stopped in the test phase on the existing Node-version-only `stacktrace_is_correct` snapshot difference (`v25.8.1` locally versus `v26.5.0` recorded). AI assistance was used to investigate the documentation gap, edit the guidance, and run verification. I remain responsible for reviewing and understanding the changes before merge. |
||
|
|
3d95477214 |
fix(transformer): gate async-only rewrites by owning transform (#26326)
For targets that support async functions but not async generators, async-only converter mode still treated every async function as if the enabled transform would move its body. Plain async arrows and methods gained generated `this` and `super` bindings, and class-field arrows were wrapped unnecessarily.
Use the shared body-movement predicate for every async-only rewrite. Functions left native by the selected target now remain untouched, while async generator functions that are lowered keep their required lexical captures.
## Example
A plain async method using `super` previously gained a generated `_superprop_getValue` binding even though the method remained native. This made `eval("typeof _superprop_getValue")` return `"function"` instead of `"undefined"`. The method now remains unchanged.
Verified with focused output/runtime conformance, full transformer output conformance, and `cargo test -p oxc_transformer`.
Stacked on #26317.
AI assistance was used to diagnose the remaining transform boundaries, implement the predicate checks, and add tests. I remain responsible for reviewing and understanding the changes before merge.
|
||
|
|
f45a9ee58b |
fix(transformer): limit arguments capture to lowered async functions (#26317)
For targets that support async functions but not async generators, plain async methods and arrows remain native. Async-only converter mode still captured `arguments` for all of them, exposing generated `_arguments` bindings even though their bodies never move.
Gate `arguments` capture on the transform that owns the function body. Native async functions now remain untouched, while lowered async generator methods still capture their original arguments.
## Example
This native async method previously changed from:
```js
async method() {
void arguments;
return eval("typeof _arguments");
}
```
to:
```js
async method() {
var _arguments = arguments;
void _arguments;
return eval("typeof _arguments");
}
```
The generated binding changed the return value from `"undefined"` to `"object"`. The method now remains unchanged.
Verified with focused output/runtime conformance, full transformer output conformance, and `cargo test -p oxc_transformer`.
Stacked on #26227.
AI assistance was used to diagnose the transform boundary, implement the fix, and add tests. I remain responsible for reviewing and understanding the changes before merge.
|
||
|
|
789969f8a1 |
fix(transformer): gate this capture by owning async transform (#26227)
For targets that support async functions but not async generators, plain async methods remain native. The shared arrow converter still captured `this` whenever either async transform was active, injecting an observable `_this` binding into methods whose bodies never move.
Track async-to-generator and async-generator lowering separately. Plain async methods are now left untouched when only async generators require lowering, while transformed async generator methods still preserve their receiver.
## Example
With a Chrome 55 target, this method previously gained a generated binding:
```js
async method() {
var _this = this;
void _this;
return eval("typeof _this");
}
```
It now remains unchanged, so the direct evaluation returns `"undefined"` as it does in the source program.
Verified with a Chrome 55 target output/runtime fixture, full transformer output conformance, and `cargo test -p oxc_transformer`.
AI assistance was used to diagnose the transform boundary, implement the fix, and add tests. I remain responsible for reviewing and understanding the changes before merge.
|
||
|
|
2c9a947363 |
feat(parser)!: reduce MAX_LEN to 256 bytes below u32::MAX (#26352)
Technically a breaking change, but one that's extremely unlikely to affect any real-world applications. On 64-bit systems, the maximum source text length was exactly `u32::MAX`. Lower it by 256 bytes, for 2 reasons: 1. `oxc_lexer` will require source text to be followed by 64 bytes of padding. Reducing max source length ensures _padded_ length can always fit in a `u32`. 2. Counts of tokens, comments and errors are all bounded by source length plus a small constant. By reducing max source length, these can all be stored as `u32`s without any possibility of overflow. `oxc_lexer` only needs 64 bytes of padding. Have gone with 256 to leave headroom in case later redesigns require more than 64. This avoids another breaking change later. We can always switch to 64 later on if we want, and this later relaxation would not be a breaking change. All the changes to other crates in this PR are just updating comments which talk about max source length. |
||
|
|
894c8f9cd8 |
release(crates): oxc v0.148.0 (#26198)
### 💥 BREAKING CHANGES - |
||
|
|
243b685c3d |
fix(transformer/object-rest): lower multiple declarators correctly (#26147)
## Summary
Fix ES2018 object-rest lowering when one variable declaration contains multiple object-rest declarators.
### Before
Given:
```js
let { a, ...r } = x, { b, ...s } = y;
```
Oxc produced:
```js
let { a } = x, { b } = y,
s = babelHelpers.objectWithoutProperties(y, ["b"]),
{} = null;
```
The `r` binding was lost.
Each replacement was recorded using its original declarator index. Replacing the first declarator expanded it into multiple entries, shifting the second declarator. The subsequent replacement still used the old index and overwrote part of the first expansion.
### After
Oxc now produces:
```js
let { a } = x,
r = babelHelpers.objectWithoutProperties(x, ["a"]),
{ b } = y,
s = babelHelpers.objectWithoutProperties(y, ["b"]);
```
Queued replacements are applied from right to left. Replacing a later declarator cannot change the index of an earlier one, so every recorded index remains valid.
Regression coverage includes multiple `let`, `const`, and `var` declarators.
|
||
|
|
4e258430cd |
release(crates): oxc v0.147.0 (#26046)
### 🚀 Features - |
||
|
|
d5d575c1de |
chore(deps): update rust to v1.98.0 (#25945)
This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [rust](https://rust-lang.org/) ([source](https://redirect.github.com/rust-lang/rust), [changelog](https://redirect.github.com/rust-lang/rust/blob/main/RELEASES.md)) | toolchain | minor | `1.97.1` → `1.98.0` | --- ### Configuration 📅 **Schedule**: (in timezone Asia/Shanghai) - Branch creation - "before 10am on monday" - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR was generated by [Mend Renovate](https://mend.io/renovate/). View the [repository job log](https://developer.mend.io/github/oxc-project/oxc). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zNS40IiwidXBkYXRlZEluVmVyIjoiNDQuMzUuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Cameron <cameron.clark@hey.com> |
||
|
|
0389c4010e |
release(crates): oxc v0.146.0 (#25912)
### 🚀 Features - |
||
|
|
4a4a1fb2e7 |
release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES - |
||
|
|
561001d33e |
test(transformer): cover baseline browserslist targets (#25697)
Adds transformer integration coverage for Baseline Browserslist queries and verifies feature-specific lowering across dated and year-based targets. Created with AI assistance. |
||
|
|
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. |
||
|
|
a33788e13e |
feat(ast)!: group class heritage into ClassHeritage (#25360)
## Summary
- Groups `Class::super_class` and `Class::super_type_arguments` into `Class::heritage`.
- Introduces `ClassHeritage` containing the superclass expression and its optional TypeScript type arguments.
- Makes superclass type arguments without a superclass expression unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub super_class: Option<Expression<'a>>,
#[ts]
pub super_type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
```
to:
```rust
pub struct Class<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub r#type: ClassType,
pub decorators: Vec<'a, Decorator<'a>>,
pub id: Option<BindingIdentifier<'a>>,
#[ts]
pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
pub heritage: Option<ClassHeritage<'a>>,
#[ts]
pub implements: Vec<'a, TSClassImplements<'a>>,
pub body: Box<'a, ClassBody<'a>>,
#[ts]
pub r#abstract: bool,
#[ts]
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub struct ClassHeritage<'a> {
pub expression: Expression<'a>,
#[ts]
pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```
## Why?
ECMAScript defines the complete `extends` clause as `ClassHeritage`:
```text
ClassHeritage : extends LeftHandSideExpression
```
TypeScript additionally permits type arguments on the heritage expression:
```ts
class Derived extends Base<T> {}
```
Previously, the AST stored the expression and type arguments independently:
```text
Class {
super_class: Some(Base),
super_type_arguments: Some(<T>),
...
}
```
This allowed an invalid state to be constructed:
```text
Class {
super_class: None,
super_type_arguments: Some(<T>),
...
}
```
Superclass type arguments cannot exist without an `extends` expression.
The parser already treated these fields as a pair. It parsed a class heritage entry and then split its expression and type arguments into two independent fields. Builders and transforms therefore had to preserve their relationship manually.
Grouping them into `ClassHeritage` makes the syntactic dependency explicit and prevents consumers from observing or constructing contradictory class heritage.
## Class heritage forms
A class without an `extends` clause has no heritage:
```js
class Base {}
```
```text
Class {
heritage: None,
...
}
```
A class extending an expression has heritage without type arguments:
```js
class Derived extends Base {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: None,
}),
...
}
```
TypeScript type arguments belong to the same heritage structure:
```ts
class Derived extends Base<T> {}
```
```text
Class {
heritage: Some(ClassHeritage {
expression: Base,
type_arguments: Some(<T>),
}),
...
}
```
The heritage expression may be any valid class heritage expression:
```js
class Derived extends mixin(Base) {}
class NullDerived extends null {}
```
## How to migrate
Code that previously accessed the superclass expression directly:
```rust
if let Some(super_class) = &class.super_class {
// ...
}
```
can use the compatibility accessor:
```rust
if let Some(super_class) = class.super_class() {
// ...
}
```
Code that needs both parts should access the grouped heritage:
```rust
if let Some(heritage) = &class.heritage {
let expression = &heritage.expression;
let type_arguments = heritage.type_arguments.as_deref();
}
```
Mutating consumers should update the nested fields:
```rust
if let Some(heritage) = &mut class.heritage {
heritage.type_arguments = None;
}
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `class.super_class` | `class.heritage.as_ref().map(\|h\| &h.expression)` or `class.super_class()` |
| `class.super_type_arguments` | `class.heritage.as_ref()?.type_arguments` or `class.super_type_arguments()` |
| `class.super_class.is_some()` | `class.heritage.is_some()` |
| Mutate `class.super_class` | Mutate `class.heritage.expression` |
| Clear `class.super_type_arguments` | Clear `class.heritage.type_arguments` |
| Pass superclass and type arguments separately to a `Class` builder | Construct and pass `ClassHeritage` |
| Clone both fields independently | Clone `class.heritage` |
```ts
superClass: Expression | null;
superTypeArguments?: TSTypeParameterInstantiation | null;
```
No `heritage` property or `ClassHeritage` node is added to the ESTree output.
fixes https://github.com/oxc-project/backlog/issues/216
|
||
|
|
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
|
||
|
|
44fd320324 |
feat(ast)!: split TS external modules & Namespace Declarations (#25284)
## Summary
- Splits `TSModuleDeclaration` into two dedicated declaration nodes:
- `TSExternalModuleDeclaration` for string-literal modules and module augmentations.
- `TSNamespaceDeclaration` for identifier-based `module` and `namespace` declarations.
- Removes `TSModuleDeclaration`, `TSModuleDeclarationName`, `TSModuleDeclarationBody`, and `TSModuleDeclarationKind`.
- Makes invalid combinations of names, bodies, and declaration kinds unrepresentable.
- Ensures only namespace declarations introduce a normal binding.
- Updates all AST consumers, generators, traversal, formatting, code generation, semantic analysis, lint rules, transforms, and serialization.
- Keeps normal and eager-raw ESTree output compatible: both declarations remain `TSModuleDeclaration` nodes.
- Allows experimental lazy raw deserialization to expose the two new node types directly.
## Breaking Change
The Rust AST changes from:
```rust
pub enum Declaration<'a> {
VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
FunctionDeclaration(Box<'a, Function<'a>>) = 33,
ClassDeclaration(Box<'a, Class<'a>>) = 34,
TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 38,
TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 39,
TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 40,
}
pub struct TSModuleDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: TSModuleDeclarationName<'a>,
pub body: Option<TSModuleDeclarationBody<'a>>,
pub kind: TSModuleDeclarationKind,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
```
to:
```rust
pub enum Declaration<'a> {
VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
FunctionDeclaration(Box<'a, Function<'a>>) = 33,
ClassDeclaration(Box<'a, Class<'a>>) = 34,
TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
TSExternalModuleDeclaration(Box<'a, TSExternalModuleDeclaration<'a>>) = 38,
TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 39,
TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 40,
TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 41,
}
pub struct TSExternalModuleDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: StringLiteral<'a>,
pub body: Option<Box<'a, TSModuleBlock<'a>>>,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub struct TSNamespaceDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: BindingIdentifier<'a>,
pub body: TSNamespaceDeclarationBody<'a>,
pub kind: TSNamespaceDeclarationKind,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub enum TSNamespaceDeclarationBody<'a> {
TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 0,
TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
}
pub enum TSNamespaceDeclarationKind {
Module = 0,
Namespace = 1,
}
```
The corresponding inherited `Statement` variants, `AstKind` variants, builders, and visitor methods have also changed.
## Why?
`TSModuleDeclaration` previously represented two declaration families with different syntax and semantics:
```ts
declare module "foo" {}
declare module "*.css";
module Foo {}
namespace Foo {}
namespace Foo.Bar {}
```
The shared representation allowed invalid states such as:
- A namespace with a string-literal name.
- An external module with an identifier name.
- An identifier namespace without a body.
- A dotted external module.
- An external module containing another module declaration as its body.
- A declaration whose `kind` disagreed with its name.
Consumers also had to inspect `id` before determining whether the node introduced a binding or represented an external module.
The two declaration families have stronger independent invariants:
### External modules
- Always have a string-literal name.
- Never introduce a normal binding identifier.
- May omit their body.
- Cannot use dotted names.
- Use module-body import and export rules.
### Namespace declarations
- Always have a binding identifier.
- Always have a body.
- May contain nested namespace declarations for dotted names.
- Explicitly preserve the `module` or `namespace` spelling.
- Introduce a namespace/module symbol.
- Use namespace-body restrictions.
Dedicated nodes encode these differences directly.
## Declaration forms
External modules use `TSExternalModuleDeclaration`:
```ts
declare module "foo" {}
declare module "*.css";
module "foo" {}
```
Identifier-based declarations use `TSNamespaceDeclaration`:
```ts
module Foo {}
namespace Foo {}
```
Dotted namespaces are represented as nested namespace declarations:
```ts
namespace Foo.Bar {}
```
```rust
TSNamespaceDeclaration {
id: Foo,
body: TSNamespaceDeclarationBody::TSNamespaceDeclaration(
TSNamespaceDeclaration {
id: Bar,
body: TSNamespaceDeclarationBody::TSModuleBlock(...),
// ...
}
),
// ...
}
```
Only `TSExternalModuleDeclaration::body` is optional.
## ESTree compatibility
Normal serialization and eager raw deserialization continue to emit the existing ESTree shape:
```ts
interface TSModuleDeclaration {
type: "TSModuleDeclaration";
id: BindingIdentifier | StringLiteral | TSQualifiedName;
body: TSModuleBlock | null;
kind: "module" | "namespace";
declare: boolean;
global: false;
}
```
Dotted namespaces continue to serialize as a single `TSModuleDeclaration` with a `TSQualifiedName`.
Experimental lazy raw deserialization is not compatibility-preserving. It now exposes `TSExternalModuleDeclaration` and `TSNamespaceDeclaration` directly, with separate constructors, layouts, and visitor type IDs.
## How to migrate
Code that previously matched all forms through one variant:
```rust
if let Declaration::TSModuleDeclaration(declaration) = declaration {
// ...
}
```
should now match the required declaration family:
```rust
match declaration {
Declaration::TSExternalModuleDeclaration(declaration) => {
// String-literal external module
}
Declaration::TSNamespaceDeclaration(declaration) => {
// Identifier-based module or namespace
}
_ => {}
}
```
The same applies to `Statement` and `AstKind`:
```rust
Statement::TSExternalModuleDeclaration(declaration)
Statement::TSNamespaceDeclaration(declaration)
AstKind::TSExternalModuleDeclaration(declaration)
AstKind::TSNamespaceDeclaration(declaration)
```
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `TSModuleDeclarationName::StringLiteral(id)` | `TSExternalModuleDeclaration::id` |
| `TSModuleDeclarationName::Identifier(id)` | `TSNamespaceDeclaration::id` |
| `body: None` | Only valid on `TSExternalModuleDeclaration` |
| `TSModuleDeclarationBody::TSModuleBlock` | External `body` or `TSNamespaceDeclarationBody::TSModuleBlock` |
| `TSModuleDeclarationBody::TSModuleDeclaration` | `TSNamespaceDeclarationBody::TSNamespaceDeclaration` |
| `TSModuleDeclarationKind::Module` | External module, or `TSNamespaceDeclarationKind::Module` |
| `TSModuleDeclarationKind::Namespace` | `TSNamespaceDeclarationKind::Namespace` |
| Inspect `id` to determine binding behavior | Match `TSNamespaceDeclaration` |
| `declaration.id()` for string modules | Now returns `None` |
| `visit_ts_module_declaration` | `visit_ts_external_module_declaration` or `visit_ts_namespace_declaration` |
When constructing nodes, use the generated `TSExternalModuleDeclaration` or `TSNamespaceDeclaration` builders rather than recreating the previous shared representation.
fixes https://github.com/oxc-project/backlog/issues/218
|
||
|
|
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
|
||
|
|
c917f204a7 |
feat(ast)!: introduce ArrowFunctionBody enum (#24987)
## Summary
- Introduces `ArrowFunctionBody` to represent concise expression and block bodies explicitly.
- Removes the `ArrowFunctionExpression::expression` boolean.
- Makes invalid arrow body combinations unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct ArrowFunctionExpression<'a> {
pub expression: bool,
pub body: Box<'a, FunctionBody<'a>>,
}
```
to:
```rust
pub enum ArrowFunctionBody<'a> {
FunctionBody(Box<'a, FunctionBody<'a>>) = 64,
INHERIT(Expression<'a>),
}
pub struct ArrowFunctionExpression<'a> {
pub body: ArrowFunctionBody<'a>,
}
```
Concise arrow bodies are now stored directly as expressions instead of synthetic `FunctionBody` and `ExpressionStatement` nodes. Block bodies continue to use `FunctionBody`.
## Why?
The previous representation required consumers to maintain this implicit invariant:
```text
expression == true
→ no directives
→ exactly one statement
→ that statement is an ExpressionStatement
```
Invalid combinations could cause panics, skipped work, or fallback behavior. The enum encodes the grammar directly and makes those invalid states unrepresentable.
Concise expressions also now have `ArrowFunctionExpression` as their direct traversal ancestor, without synthetic `FunctionBody` or `ExpressionStatement` nodes.
## How to migrate
Use the arrow body accessors or match on `ArrowFunctionBody` rather than inspecting `expression` and synthetic statements.
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `arrow.expression` | `arrow.is_expression()` |
| `arrow.get_expression()` | `arrow.get_expression()` |
| `&arrow.body.statements` | `arrow.get_function_body().map(\|body\| &body.statements)` |
| `&mut arrow.body.statements` | `arrow.get_function_body_mut().map(\|body\| &mut body.statements)` |
| Synthetic expression statement in `arrow.body` | Direct `ArrowFunctionBody` expression variant |
| `arrow.body = function_body` | `arrow.body = ArrowFunctionBody::FunctionBody(function_body)` |
| Expression-to-block conversion | Create a `FunctionBody` containing a return statement |
| Block-to-expression conversion | Assign the expression directly to `arrow.body` |
| Builder `(expression, body)` arguments | Pass a single `ArrowFunctionBody` |
closes https://github.com/oxc-project/backlog/issues/211
|
||
|
|
8b7420a7e8 |
refactor(transformer): decouple React Compiler (#25065)
Moves React Compiler integration out of `oxc` and `oxc_transformer` into the dedicated binding. AI-assisted. |
||
|
|
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> |
||
|
|
0126aba1d9 |
fix(codegen): preserve orphaned file coverage comments (#24815)
File-level V8 and Istanbul coverage directives attach to the first AST statement. When TypeScript import removal or another upstream pass deletes that statement, codegen never visits the comment attachment position and silently drops the directive. Coverage tooling then instruments a file that was explicitly excluded.
The lexer now classifies `v8 ignore file` and `istanbul ignore file` separately, including line-comment forms and explanatory suffixes. Codegen lets those directives share the existing legal-comment orphan flush, preserving source order without scanning source text during printing. Position-sensitive directives such as `ignore next`, `start`, and `stop` remain unchanged.
The recovery path is named and documented around orphaned attachments. Hashbang ordering remains intact, and disabling annotation comments still suppresses these directives. The transformer regression stays in the Rust integration harness because transform conformance deliberately clears comments before codegen.
## Example
Input:
```ts
/* v8 ignore file */
import type { Foo } from "./types";
export default { typed: "stuff" } satisfies Foo;
```
Before:
```js
export default { typed: "stuff" };
```
After:
```js
/* v8 ignore file */
export default { typed: "stuff" };
```
Covered by direct codegen regressions for removed anchors, hashbang ordering, disabled annotation comments, and position-sensitive negative cases, plus a TypeScript transformer integration test. Verified with `env -u NO_COLOR just ready`, including all-feature tests, linting, documentation, generated-code checks, formatting, and a clean-tree check.
closes #23667
This PR was assisted by OpenAI Codex.
|
||
|
|
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 - |
||
|
|
79e6d775fb |
fix(minifier): preserve RegExp feature detection (#24712)
> **PR stack (3 of 3)** > 1. #24739 > 2. #24742 > 3. **#24712 (this PR)** > > Review #24739 and #24742 first. This PR is based on #24742, so GitHub shows only the RegExp behavior change. Stacked on #24742, which exposes strict engine-target capabilities to side-effect analysis. RegExp constructor calls used for engine feature detection were removed by DCE when their pattern was valid according to the current Oxc RegExp parser. A pattern can be valid at build time but still throw a `SyntaxError` on the configured runtime, so removing the call changes the result of surrounding `try`/`catch` checks. RegExp purity now requires both syntactic validity and support from every configured target. Consumers without concrete targets remain conservative, while an explicit target allows the minifier to remove constructors known to be safe for those engines. Target-independent flag validation and pattern compatibility checks now live in `oxc_regular_expression`; side-effect analysis only maps target support to unsupported flags and patterns. String patterns, modern flags, named groups, property escapes, lookbehind, modifiers, invalid flags, and lone-surrogate strings are covered. A RegExp literal with replacement flags is removable only when the target supports both those flags and the dedicated `ES2015RegExpConstructorCanAlterFlags` compatibility feature. Explicitly targeting `esnext` opts into current features. ## Example Input: ```js export function supportsUnicodeRegExp() { try { new RegExp("\\p{Ll}", "u"); return true; } catch { return false; } } ``` Before: ```js export function supportsUnicodeRegExp() { try { return true; } catch { return false; } } ``` After: ```js export function supportsUnicodeRegExp() { try { new RegExp("\\p{Ll}", "u"); return true; } catch { return false; } } ``` Verified with all `oxc_regular_expression`, `oxc_compat`, `oxc_ecmascript`, and `oxc_minifier` tests; the no-feature/`side_effects`/`constant_evaluation` `oxc_ecmascript` matrix; affected-crate clippy; formatting; and diff checks. Fixes rolldown/rolldown#10279. Implemented with AI assistance (OpenAI Codex and Claude). The contributor remains responsible for reviewing, understanding, and submitting these changes under the repository AI usage policy. |
||
|
|
5293431203 |
feat(str)!: move IdentBuildHasher to oxc_str crate (#24717)
Move all `Ident`\-hashing code from `oxc_allocator` into `oxc_str`, to sit alongside `Ident`'s code. This feels like the natural place for this code, and reduces the public API surface - we no longer need to expose `Ident`'s internals (`ident_hash`). Now only `IdentHasher` and `IdentBuildHasher` are exposed. Only change to code is removing `pack_len_hash`, as it was only used in tests. |
||
|
|
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); ``` |
||
|
|
7aa4739f8f |
feat(syntax,transformer): move JSX entity decoder to oxc_syntax (#24617)
## Summary - move the JSX entity decoder from `oxc_transformer` to `oxc_syntax` - keep the existing accumulator-based API and transformer call sites unchanged - move the stray-ampersand regression test with the decoder This is a behavior-neutral preparation for handling JSX entity decoding closer to parsing in a follow-up. |
||
|
|
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.
|
||
|
|
cef9143aed |
refactor(transformer,minifier,react_compiler): switch JS-only visitors from Visit to VisitJs (#24532)
Migrate the transform-pipeline `Visit` implementors to `VisitJs` (#24499): `oxc_transformer` (3), `oxc_minifier` (4), `oxc_react_compiler` (1), plus the parser regex example and the coverage driver. Part of #24317; linter half in #24530. Behavior is unchanged. Together with #24530: `oxlint` −129.4 KiB (−0.94%) stripped, `oxc_transform` napi −16.2 KiB (−0.46%). ## Why each visitor can switch A visitor is safe on `VisitJs` when its hooks cannot observe anything inside the pruned type grammar — either the hooked node kinds are value-space-only, or the AST it walks cannot contain type nodes at that pipeline stage. | Visitor | File | Why equivalent | |---|---|---| | `PrivateInExpressionDetector` | `crates/oxc_transformer/src/decorator/legacy/mod.rs` | Hooks `visit_private_in_expression` (+`visit_decorators`); `#x in y` is runtime-only and cannot occur in type grammar, even inside decorator expressions carrying casts. | | `BindingMover` | `crates/oxc_transformer/src/es2017/async_to_generator.rs` | Its formal-parameter overrides visit only pattern + initializer (`BindingPattern` carries no type field — annotations are sibling fields it never touches); runs in the exit phase after TS strip. | | `UsedInJSXBindingsCollector` | `crates/oxc_transformer/src/jsx/refresh.rs` | Runs on un-stripped TSX, but its hooks (JSX-shaped calls, `visit_jsx_opening_element`, declarations/imports) cannot fire inside type grammar; its hand-written `visit_ts_type_annotation` skip-stub is deleted — `VisitJs` prunes that natively. | | `Finder` | `crates/oxc_react_compiler/src/react_compiler/entrypoint/program.rs` | Matches functions by `scope_id`; real JS function scopes never live in pure type grammar, and `VisitJs` still walks the places one could hide (enum initializers, namespace bodies, cast inner expressions). | | `KeepVar` | `crates/oxc_minifier/src/keep_var.rs` | Hooks `visit_statement`/`visit_variable_declaration`; statements and `var` declarations don't exist in type space, and namespace/enum bodies (which can contain them) are still walked by `VisitJs`. | | `DirectEvalFlagCheck` | `crates/oxc_minifier/src/peephole/mod.rs` | Debug-only; hooks `visit_call_expression` only — call expressions cannot occur in valid type grammar. | | `LiveDirectEvalCollector` | `crates/oxc_minifier/src/peephole/mod.rs` | Same single call-expression hook; type-space-immune regardless of input. | | `TestVisitor` | `crates/oxc_minifier/tests/ecmascript/prop_name.rs` | Test helper parsing `SourceType::default()` (plain JS); single non-recursing `visit_object_expression` hook. | | `RegularExpressionVisitor` | `crates/oxc_parser/examples/regular_expression.rs` | Finds `RegExpLiteral` / `new RegExp(...)`; neither node kind can occur in type grammar. | | `CheckASTNodes` | `tasks/coverage/src/driver.rs` | Its only active override is `visit_reg_exp_literal` (regex print idempotency); `RegExpLiteral` cannot occur in type space. (The commented-out `visit_span` full-AST check would need `Visit` if ever revived.) | Left on `Visit`: the minifier's incremental-scoping bookkeeping (`DropDiff`, `OverPruneCheck`, `LiveRefCollector`) — dropping `let x: Foo = …` must unmark the type-space `Foo` reference, which the `peephole::remove_unused_declaration` tests verify (they fail under `VisitJs`; confirmed and reverted during this work). `ChildScopeCollector` also stays: TS type nodes (mapped/conditional/function types) carry real `scope_id`s it must collect. --- 🤖 This PR was written with the assistance of Claude Code (Claude Fable 5). |
||
|
|
a149e95624 |
perf(transformer): outline rare expression exits (#24512)
## Summary - inline cheap expression-kind guards for arrow, JSX, and React Refresh exit hooks - outline only the rare mutation bodies that use ReplaceWith - reduce work in the generated expression walker without adding an unconditional exit call ## Assembly Using the same release configuration and codspeed cfg as CI, walk_expression changes from: - 7,911 to 7,131 instructions - 421 to 372 calls - 398 to 355 branches The earlier whole-exit outlining approach only moved work and was neutral in CodSpeed. This revision removes that change and uses targeted hot/cold splits instead. |
||
|
|
8e0ed2ebb9 |
release(crates): oxc v0.140.0 (#24453)
### 🚀 Features - |
||
|
|
6427542c8d |
refactor(react_compiler): split public API into compile + in-place CompileOutput::transform (#24401)
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) |
||
|
|
b6d2a29e47 |
refactor(semantic, transformer, react_compiler): use if-let match guards (#24383)
Rust 1.95 stabilized if-let guards in match arms (MSRV bumped in #24359). This converts the three places which guard on `is_some()` and then re-extract the same value in the body — the guard now binds the value directly: - `oxc_semantic` `binder.rs` — `export_decl.declaration.is_some()` guard followed by `.as_ref().unwrap()`. - `oxc_transformer` `typescript/annotations.rs` — `decl.declaration.is_some()` guard followed by an `is_some_and` re-check. - `oxc_react_compiler` `validate_preserved_manual_memoization.rs` — `state.manual_memo_state.is_some()` guard followed by `.as_mut().unwrap()`; the guard now binds `&mut` directly. The sibling `StoreLocal` arm used the same unwrap idiom in a body `if`, converted to a let chain for consistency. I audited the other ~15 `is_some()` match guards in the workspace: they only test the value without extracting it in the body, so they stay as plain guards. Verified: crate tests pass, semantic coverage suites and Babel transform conformance produce **zero snapshot diffs**, clippy clean for the changed files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
ab22e80f45 |
perf(transformer): fix Rust 1.97 performance regression (#24354)
## What regressed?
The Rust 1.97 upgrade changed LLVM’s inlining decisions around the ES2022 transformer expression visitor.
The hot benchmark path is:
```text
oxc_traverse::generated::walk::walk_expression
-> TransformerImpl::enter_expression
-> ES2022::enter_expression
-> ClassProperties::enter_expression
```
`ClassProperties::enter_expression` is meant to be cheap for most expressions. It first checks whether we are currently inside a class with private fields:
```rust
if self.private_field_count == 0 {
return;
}
```
For most benchmark files, this is the common path. If there are no active private fields, none of the private-field expression transforms can apply.
With Rust 1.96, LLVM kept the large private-field transform implementations out of the hot visitor, so `ES2022::enter_expression` was still small enough to inline into the generated walker:
```text
Rust 1.96:
<ES2022 as Traverse>::enter_expression
inlined into oxc_traverse::generated::walk::walk_expression
cost=-11965, threshold=250
```
With Rust 1.97, LLVM started inlining several large private-field transform bodies into `ES2022::enter_expression` first:
```text
Rust 1.97:
transform_assignment_expression_impl
inlined into ES2022::enter_expression
cost=-8825, threshold=250
transform_update_expression_impl
inlined into ES2022::enter_expression
cost=-5390, threshold=250
transform_tagged_template_expression_impl
inlined into ES2022::enter_expression
cost=-14555, threshold=250
```
After those large branches were pulled in, `ES2022::enter_expression` became too large to inline into the generated walker:
```text
Rust 1.97:
<ES2022 as Traverse>::enter_expression
not inlined into oxc_traverse::generated::walk::walk_expression
cost=4265, threshold=250
```
So the regression is not a semantic change. It is a codegen shape change: the generated walker lost a very cheap inlined fast path and started paying an extra call / worse layout on every expression visit.
## Assembly shape
In the Rust 1.96 build, `walk_expression` had the ES2022/ClassProperties fast path inlined. The walker checks whether the class-properties transform is active and whether private fields are currently in scope, then only branches into private-field handling when needed.
A representative part of the Rust 1.96 walker looked like this:
```asm
; ClassProperties state exists?
ldr x8, [x20, #0x478]
cbz x8, <skip_private_field_transform>
; Load expression discriminant
ldr x8, [sp, #0x88]
ldrb w8, [x8]
; Dispatch only to relevant expression cases
cmp w8, #0xc
b.eq <assignment_expression_case>
...
cmp w8, #0x1d
b.ne <skip_private_field_transform>
; Only call the heavy helper after matching the expression kind
bl transform_update_expression_impl
b <skip_private_field_transform>
```
For example, the clean Rust 1.96 walker still had direct calls to the rare helpers, but only behind expression-kind checks:
```asm
bl transform_call_expression_impl
bl transform_update_expression_impl
bl transform_tagged_template_expression_impl
bl transform_private_field_expression_impl
bl transform_assignment_expression_impl
bl transform_unary_expression_impl
bl transform_chain_expression_impl
```
In the bad Rust 1.97 shape, the outer ES2022 visitor was no longer inlined into `walk_expression`, because LLVM had already inflated it by inlining the large private-field branches. That left the hot walker path with a call to the ES2022 expression visitor instead of the cheap inlined guard.
## Fix
This PR makes the intended hot/cold split explicit:
```rust
#[inline]
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(class_properties) = &mut self.class_properties {
class_properties.enter_expression(expr, ctx);
}
}
```
and:
```rust
#[inline]
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if self.private_field_count != 0 {
self.enter_expression_with_private_fields(expr, ctx);
}
}
#[inline(never)]
fn enter_expression_with_private_fields(...) {
match expr {
Expression::PrivateFieldExpression(_) => ...
Expression::CallExpression(_) => ...
Expression::AssignmentExpression(_) => ...
...
}
}
```
The important bit is that the common case remains tiny:
```text
if private_field_count != 0 {
slow_private_field_dispatch(...)
}
```
The large private-field expression dispatch is still available, but it no longer pollutes the hot expression visitor and no longer prevents the ES2022 hook from being inlined into the generated walker.
The patched walker now has the intended shape:
```asm
; Check class-properties/private-field state
ldr x8, [x20, #0x478]
cbz x8, <skip_private_field_transform>
; Only call out when private fields are active
add x0, x20, #0x3f8
ldr x1, [sp, #0x90]
mov x2, x28
bl ClassProperties::enter_expression_with_private_fields
<skip_private_field_transform>:
```
This keeps the common no-private-fields path small while preventing Rust 1.97 from re-inlining the large rare private-field transform bodies into the generated walker.
|
||
|
|
66e1a25c0e |
chore(all): fix clippy::manual_assert_eq (#24351)
|
||
|
|
721eb0ba2b | fix(transformer/decorator): scope accessor class binding (#24330) | ||
|
|
af4922be6d | fix(transformer): clear lowered namespace redeclarations (#24300) | ||
|
|
f05dfabe5c |
fix(transformer): correct symbol flags for lowered namespaces (#24271)
Closes #24224 — the first commit is @camc314's fix from there. Lowered namespaces kept their `ValueModule`/`NameSpaceModule` symbol flags, so the flags no longer matched the emitted JavaScript: - Namespaces lowered to a declaration become `let` → `BlockScopedVariable`. - Namespaces lowered to an assignment only lose their module bits. This removes over 2,000 stale symbol-flag mismatches from the snapshots. Unlike #24224 there is no pending state: the type-only check for merged namespaces now reads per-declaration flags from the redeclaration list instead of the symbol's live flags, so each declaration can update the flags right when it is lowered. |
||
|
|
84eeb5570b |
fix(transformer): correct symbol flags for lowered enums (#24269)
Closes #24226 — the first commit is @camc314's fix from there. Lowered enums kept their `RegularEnum`/`ConstEnum` symbol flags, so the flags no longer matched the emitted JavaScript: - Top-level enums become `var` → `FunctionScopedVariable`. - Exported and nested enums become `let` → `BlockScopedVariable`. - Merged enums lowered to an assignment only lose their enum bits. This removes over 1,000 stale symbol-flag mismatches from the snapshots. Unlike #24226 there is no pending state: enum member inlining now identifies enums through `EnumData` (#24268) instead of `SymbolFlags`, so the flags can be updated right when the enum is lowered. |
||
|
|
c3057dac59 |
fix(transformer): preserve generated class binding spans (#24220)
## Summary
- preserve outer and inner class binding spans during legacy decorator lowering
- add a span-aware traversal binding generator for transformed source-backed bindings
- update semantic coverage snapshots, removing stale `Span { start: 0, end: 0 }` mismatches
|
||
|
|
8260096253 |
fix(transformer): correct span for lowered namespace symbol (#24222)
## Summary
- preserve source spans when TypeScript namespace declarations are lowered to variable bindings
- update semantic coverage snapshots, removing stale `Span { start: 0, end: 0 }` mismatches
|