Commit Graph

192 Commits

Author SHA1 Message Date
Boshen 2fe0a0dfd4 fix(react_compiler): preserve caught property loads (#25498)
Preserve catch-observable property and computed loads during dead-code elimination, and record their root operands as reactive-scope dependencies so throwing reads remain inside `try`.

This prevents memoization from suppressing catch behavior or hoisting potentially throwing property access into cache guards.

Validated with the React Compiler crate suite, NAPI transform suite, and original-vs-compiled runtime cases.

AI assistance was used to investigate and implement this change.
2026-08-11 07:15:55 +00:00
Boshen c3a9731624 fix(react_compiler): preserve underscore-prefixed JSX components (#25496)
Preserve JSX component classification through React Compiler HIR and codegen so underscore-prefixed components are emitted as identifiers instead of intrinsic string tags.

Update the existing context-variable JSX fixture to cover `_Component`.

Validated with the React Compiler crate suite, NAPI suite, and all six ecosystem reproductions.

AI assistance was used.
2026-08-11 06:53:51 +00:00
Boshen 1645d93cfb fix(react_compiler): preserve source spans (#25462)
React Compiler rebuilt source-derived AST nodes with empty spans, so compiled function bodies had no useful source mappings. Preserve retained HIR and reactive spans during codegen while keeping synthetic memoization scaffolding unmapped.

AI-assisted: implementation used Codex; the author remains responsible for review.
2026-08-11 02:10:30 +00:00
Boshen 93c83d3d9b fix(react_compiler): model tagged templates as calls (#25485)
Model tagged-template expressions as function applications during mutation and alias inference. This keeps mutations of returned objects within the memoization scope and preserves the implicit template argument position.

Checked with `cargo test -p oxc_react_compiler` and the transform-react NAPI suite.

AI-assisted.
2026-08-10 15:43:19 +00:00
Boshen 8d7f9cff3f fix(react_compiler): honor eslint suppressions (#25394)
Honor React ESLint suppressions independently of internal compiler validation settings, matching `babel-plugin-react-compiler@1.0.0`.

Closes #25392.

AI-assisted.
2026-08-10 10:42:23 +00:00
Boshen 2e35238d65 fix(react-compiler): compile forwardRef callbacks with non-ref param names (#25468)
The discovery walker let a declarator name flow through `forwardRef`/`memo` calls, so `const Foo = forwardRef((props, viewTracker) => ...)` was treated as a named component and rejected by the component param check (a second param must look like a ref). Upstream only names a function from its direct parent (declarator/assignment/property); a function in call arguments is anonymous and is detected through the forwardRef/memo callback branch, which never checks params.

Fixes the two Simorgh files from the ecosystem comparison (`TopStoriesItem`, `LatestMediaItem`): both now compile with output matching upstream, including identical memo cache sizes. Adds a fixture for the pattern; no existing fixture snapshots change.

Created with assistance from Claude.
2026-08-10 08:54:59 +00:00
Boshen fa7fdd2123 fix(react-compiler): compile parenthesized arrow components (#25463)
Compile parenthesized arrow components by preserving declarator name inference through nested parentheses.

Adds a React Compiler fixture for the ecosystem reproduction.

Created with assistance from Codex.
2026-08-10 07:30:16 +00:00
Boshen 28c2e09838 feat(react_compiler): recognize oxlint suppressions (#25456)
Recognize `oxlint-disable*` and `oxlint-enable` comments wherever React Compiler accepts their ESLint-prefixed equivalents.

AI-assisted.
2026-08-10 05:19:14 +00:00
Boshen e3683f29d8 fix(react_compiler): materialize compound assignment reads (#25450)
Materializes identifier reads before compound-assignment binary operations so constant propagation matches the upstream React Compiler. AI-assisted.
2026-08-09 16:25:11 +00:00
Boshen 92426aefeb fix(react_compiler): align nested ref validation (#25427)
Align ref validation with Babel React Compiler 1.0 by limiting call-site alias-effect checks to values actually passed to the call. This keeps refs captured by returned event handlers valid while preserving diagnostics for refs passed to invoked functions.

Validated with `cargo test -p oxc_react_compiler`, the rebuilt `oxc-transform-react` binding against Outline’s `ContentEditable.tsx`, and `just ready`.

AI-assisted; the contributor remains responsible for review and submission.
2026-08-09 13:15:18 +00:00
Boshen 937825c6a6 fix(react_compiler): disable exhaustive memo validation by default (#25417)
Disable exhaustive memo dependency validation by default and align
suppression handling.

AI-assisted.

<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-08-09 11:18:12 +08:00
Boshen 7d8a9b5f2f fix(react_compiler): preserve hook spans for React Refresh (#25393)
Fix React Refresh when used with the React Compiler N-API transform by
preserving original hook argument and binding-pattern spans through
compiler codegen.

This prevents refresh signature generation from panicking and keeps
signatures based on the original source.

Validated with `just ready` and the React transform N-API suite.

Generated with AI assistance; reviewed and validated by the contributor.
2026-08-08 08:46:36 +00:00
camc314 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
2026-08-07 14:18:59 +00:00
camc314 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
2026-08-05 16:31:46 +00:00
camc314 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
2026-08-05 14:37:35 +00:00
Boshen 495b02483d fix(react_compiler): preserve parenthesized non-node returns (#25289)
Preserve Babel's non-node classification for parenthesized object returns so ordinary PascalCase factories are not compiled as React components.

Validated against the React Compiler suite and the Kibana reproduction.

AI-assisted.
2026-08-05 01:38:14 +00:00
Boshen db407f61cc fix(react_compiler): discover functions in parameter defaults (#25287)
Fix React Compiler discovery of `memo` and `forwardRef` callbacks in runtime parameter defaults when the enclosing function is skipped.

Validated against the React Compiler suite and the Kibana reproduction.

AI-assisted.
2026-08-04 14:31:31 +00:00
camc314 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
2026-07-31 10:25:47 +00:00
camc314 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
2026-07-29 13:32:32 +00:00
Boshen 5d8ccf510b fix(react_compiler): preserve nonfatal diagnostics (#25066)
Preserves nonfatal React Compiler diagnostics and covers warning bailouts through the N-API binding.

AI-assisted.
2026-07-29 13:09:20 +00:00
Boshen d349e4a449 fix(react_compiler): match eslint suppression gating (#25068)
Matches React Compiler ESLint suppression gating for internal validation options.

AI-assisted.
2026-07-29 12:41:00 +00:00
Boshen 0dc16ba3a0 fix(react_compiler): track catch block declarations (#25067)
Tracks declarations introduced by catch bindings in React Compiler HIR.

AI-assisted.
2026-07-29 12:29:08 +00:00
overlookmotel 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)`.
2026-07-28 22:40:04 +00:00
overlookmotel 7251e52a69 perf(react_compiler): avoid large types on stack (#25037)
Continuation of #25034.

In React compiler, 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.
2026-07-28 22:40:03 +00:00
Boshen 9d7745af95 fix(react-compiler): preserve warning bailouts (#24935)
Preserve warning-level React Compiler diagnostics instead of wrapping pipeline bailouts in a synthetic fatal error. This keeps expected incompatible-library and Todo bailouts non-fatal so sibling functions can still compile.

Adds regression coverage and refreshes the affected snapshots. Validated with `just ready`.

AI-assisted: Codex was used to investigate, implement, and validate this change.
2026-07-26 06:26:02 +00:00
overlookmotel e130d85a37 refactor(react_compiler): shorten AST builder method calls (#24844)
Shorten AST builder calls in React Compiler, by collapsing multiple calls into one.

I came across these while testing the codemod.
2026-07-23 16:01:15 +00:00
Marius Schulz 98f2ecbb10 perf(react_compiler): compare value set pointers before probing in state merge (#24805)
## Context

Value sets in the aliasing inference state are `Rc<FxHashSet<ValueId>>`,
shared between states rather than copied (#24117). When two states meet
at a join, the merge still walks every variable and probes one set
against the other looking for a value the current state doesn't have.
Most of the time both sides point at the same `Rc`, so that probe is
comparing a set to itself.

## This PR

Check `Rc::ptr_eq` before probing. If the pointers match there is
nothing to add, so we skip the scan. Both merge paths did this check in
slightly different ways, so I pulled it into one helper. The sets are
never mutated in place, which is what makes the pointer check safe.

### Performance

| workload | main | this PR | delta |
| ----------------------------------------------------- | -------: |
-------: | ------: |
| 3.6k-line control-flow heavy component, instructions | 23.762 B |
22.034 B | -7.3% |
| same file, wall time |   4.55 s |   4.13 s |     -9% |
| 16.5k-file real corpus, instructions (1 thread) | 50.135 B | 49.697 B
| -0.87% |

This PR was assisted by Claude Code.
2026-07-22 18:46:45 +08:00
Boshen 9f3e30088d fix(react_compiler): preserve aliasing diagnostic locations (#24757)
Preserve source-specific labels for aliasing diagnostics while retaining
their shared analysis identity. Previously, interning by place, message,
and help reused the first source location for later effects.

Validated with `just ready` and React ecosystem comparisons.

AI-assisted: Codex was used to implement and validate this change.
2026-07-21 20:21:01 +08:00
Boshen 4b81f4d963 fix(react_compiler): honor default suppressions (#24747)
Honor Babel React Compiler 1.0.0's default ESLint and Flow suppression bailouts even when validation flags are enabled.

Validation:
- `cargo test -p oxc_react_compiler`
- `cargo clippy -p oxc_react_compiler --all-targets -- -D warnings`
- Direct fixture comparison with `babel-plugin-react-compiler@1.0.0`

Caveat: Babel 1.0.0 treats block disables as open-ended; this change preserves that behavior.

AI-assisted.
2026-07-21 07:02:25 +00:00
Boshen 3fe5c87bbd perf(react_compiler): transform reactive blocks in place (#24703)
The reactive-tree transform passes rebuilt each mutated block by
deep-cloning statements into a fresh vector. With the tree now
arena-backed, those clones (and the abandoned scratch vectors) were
never freed until the per-file arena dropped — a churn tail on top of
the tree's relocation into the arena. This removes it.

## What changed

- `traverse_block` — the shared transform engine used by all 7
reactive-scopes passes — now mutates the block's own arena buffer
directly: `Keep` transforms in place, `Replace` overwrites the slot,
`Remove` is `Vec::remove`, `ReplaceMany` is `Vec::splice`. This drops
(a) the deep `clone_in` of the block prefix on the first structural
change, (b) the scratch `next_block` vector, and (c) the per-statement
placeholder swap. The index bookkeeping (advance on Keep/Replace, hold
on Remove, skip inserted on ReplaceMany) reproduces the exact original
order.
- Merge Pass-3 already `mem::replace`s the block out — so it owns the
statements — but then cloned them back into the result; it now moves
them via `into_iter`.

Behavior is unchanged — snapshot corpus and transform tests remain
byte-identical.

## Memory

Arena bytes across the full 1269-fixture corpus (fresh allocator per
file, after `compile`): **12.97 MB → 12.12 MB (−6.5%)**, recovering most
of the reactive-tree flip's churn tail. The deep `clone_in` of statement
subtrees is replaced by cheap moves (a CPU win on transform-heavy
components, in addition to the allocation drop).

## Stack

- #24664
- #24666
- #24696
- #24700
- #24702
- **this PR** — stacked on top of #24702

🤖 This PR was written with the assistance of Claude Code.
2026-07-21 09:49:25 +08:00
Boshen 165c465396 refactor(react_compiler): move the reactive-function tree into the arena (#24702)
The final subtree of the react_compiler HIR-to-arena migration: the
ReactiveFunction tree's own structural vectors and boxes move off the
heap into the compilation arena, so the reactive tree stops straddling
two allocators.

## What changed

- `ReactiveBlock` (`= Vec<ReactiveStatement>`, every block body),
`ReactiveValue::SequenceExpression.instructions`, and
`ReactiveTerminal::Switch.cases` become `oxc_allocator::Vec<'a>`.
- The tree's `Box` fields (`ReactiveStatement::Terminal`, the boxed
`ReactiveValue` sub-expressions, ...) become `oxc_allocator::Box<'a>` —
required so the element types are `!needs_drop` and can live in an arena
`Vec` (`ReactiveStatement` was `Drop` via its std boxes). This completes
the tree's move (Vec + Box) into the arena.
- The transform passes' `std::mem::take` on block bodies (arena `Vec`
has no `Default`) become `std::mem::replace(.., Vec::new_in(alloc))`;
construction sites allocate from `env.allocator`.
- `Transformed::ReplaceMany` deliberately stays a std `Vec` — a
transient replacement list that `traverse_block` drains into the arena
block — so the generic visitor enum and every pass's `ReplaceMany`
construction are untouched.

Behavior is unchanged — snapshot corpus and transform tests remain
byte-identical.

## Memory

Arena bytes across the full 1269-fixture corpus (fresh allocator per
file, after `compile`):

| | total arena used | max single fixture |
|---|---|---|
| before (tree on heap) | 9.93 MB | 228 KB |
| after (tree in arena) | 12.97 MB | 297 KB |

The +3.04 MB (+31%) is a relocation of the tree off the heap — smaller
than the CFG subtree's +82% shift, and no fixture balloons.

## Follow-up (not in this PR)

The reactive-scopes passes rebuild block bodies with a clone-prefix +
take/rebuild pattern; with the arena the abandoned spine isn't freed
until the per-file arena drops (a bounded churn tail, included in the
+31% above). Rewriting `traverse_block` and the merge Pass-3 to
drain/splice the block in place would collapse that churn tail **and**
delete a pre-existing wasteful deep-clone (a CPU win). Deferred to its
own PR because the in-place splice must be verified byte-identical
across the whole corpus.

## Stack

- #24664
- #24666
- #24696
- #24700
- **this PR** — stacked on top of #24700

🤖 This PR was written with the assistance of Claude Code.
2026-07-21 09:49:24 +08:00
Boshen b6b058fdf6 refactor(react_compiler): move the CFG HIR into the arena (#24700)
The largest step of the react_compiler HIR-to-arena migration: moves the
control-flow-graph HIR and the reactive-function tree off the global
heap into the compilation arena, eliminating the per-node
`malloc`/`free` churn (block instruction/phi vectors, predecessor sets,
phi operand maps, instruction-value vectors) that dominated the
compiler's allocation traffic.

## What changed

- `HIR`, `BasicBlock`, `Phi`, `Terminal`, `AliasingEffect`,
`ArrayPattern`, and `AliasingSignature` gain an `'a` lifetime; their
`Vec` fields become `oxc_allocator::Vec<'a>` (block instructions/phis,
terminal effects/cases, all `InstructionValue` vectors, patterns,
aliasing args/captures, function
params/context/directives/aliasing-effects, ...).
- The two per-node insertion-ordered containers move to the arena:
`BasicBlock.preds` → `ArenaOrderedSet<'a, BlockId>` and `Phi.operands` →
`ArenaOrderedMap<'a, BlockId, Place>` — new arena-backed variants
(`oxc_allocator::Vec` + `oxc_allocator::HashMap`) added alongside the
existing std `OrderedMap`/`OrderedSet`, which are kept.
- `HIR.blocks` deliberately stays a std `OrderedMap` (one map per
function, no allocation payoff, and `HirFunction` is never arena-boxed —
it lives in a std `IndexVec`). This also keeps
`get_reverse_postordered_blocks` returning a std map, bounding arena
high-water.
- Arena `Vec` is not `Clone`, so the arena-carrying HIR and
reactive-tree types drop `#[derive(Clone)]` and get hand-written
same-arena `CloneIn` impls; construction/clone sites across ~30 files
thread the allocator from `env.allocator`.

Behavior is unchanged — the snapshot corpus and transform tests remain
byte-identical.

## Memory

Arena bytes used across the full 1269-fixture corpus (fresh allocator
per file, measured after `compile`):

| | total arena used | max single fixture |
|---|---|---|
| before (HIR on heap) | 5.45 MB | 171 KB |
| after (HIR in arena) | 9.93 MB | 228 KB |

The +4.48 MB is the HIR moving from the heap into the arena (offsetting
the corresponding heap allocations). No pathological ballooning:
RPO-heavy fixtures grow ~2-3× at negligible absolute scale, and arenas
free per-file so nothing accumulates across the corpus.

## Stack

- #24664
- #24666
- #24696
- **this PR** — stacked on top of #24696

🤖 This PR was written with the assistance of Claude Code.
2026-07-21 09:49:24 +08:00
Boshen e7fa27b23a refactor(react_compiler): store ReactiveScope collections in the arena (#24696)
Continues the react_compiler HIR-to-arena migration by moving the
per-compilation `ReactiveScope` collections off the global heap and into
the compilation arena.

## What changed

- `ReactiveScope::{dependencies, declarations, reassignments, merged}`
and `ReactiveScopeDependency::path` change from `std::vec::Vec` to
`oxc_allocator::Vec<'a>`.
- Arena `Vec` is not `Clone`, so
`ReactiveScope`/`ReactiveScopeDependency` drop `#[derive(Clone)]`.
`ReactiveScopeDependency` and `DependencyPathEntry` get hand-written
same-arena `CloneIn` impls (leaves copy, the arena `Vec` recurses);
`ReactiveScopeDeclaration` becomes `Copy`.
- Construction and clone sites across the reactive-scope and inference
passes now allocate from `env.allocator` (`clone_in` /
`ArenaVec::*_in`); a few dependency-collection helpers take an explicit
`&'a Allocator`.

Behavior is unchanged — the snapshot corpus and transform tests remain
byte-identical.

## Stack

- #24664
- #24666
- **this PR** — stacked on top of #24666

🤖 This PR was written with the assistance of Claude Code.
2026-07-21 09:49:24 +08:00
Boshen bfecb1a3f8 refactor(react_compiler): back HIR ordered maps with Vec + FxHashMap (#24666)
The HIR's insertion-ordered CFG containers were `indexmap`-based:

- `HIR.blocks: FxIndexMap<BlockId, BasicBlock>` — iteration order is
reverse-postorder, which is load-bearing (forward dataflow visits each
block after its predecessors).
- `BasicBlock.preds: FxIndexSet<BlockId>` — predecessor order sets
phi-operand order.
- `Phi.operands: FxIndexMap<BlockId, Place>` — one operand per
predecessor, in that order.

`indexmap` has no arena-allocator variant, which blocks moving the HIR
into an arena. `Vec` and hash maps do (`oxc_allocator::Vec` /
`oxc_allocator::HashMap`), so this replaces those three fields with
`OrderedMap` / `OrderedSet` — ordered containers backed by `Vec<(K, V)>`
plus an `FxHashMap<K, usize>` index. They mirror the subset of the
`IndexMap` / `IndexSet` API the HIR uses
(`get`/`get_mut`/`insert`/`shift_remove`/`retain`/`drain`/`keys`/`values`/`iter[_mut]`/`get_index_of`,
`IntoIterator`, `Index`, `DoubleEndedIterator`), preserving insertion
order and O(1) lookup, so call sites are unchanged.

This is a preparatory step: the three fields are now composed of
arena-convertible primitives, so the eventual arena move is a mechanical
`Vec` -> `Vec<'a>` / `FxHashMap` -> arena `HashMap` swap rather than an
impossible `indexmap` port.

The `SymbolId`-keyed builder maps and `StabilizeBlockIds`' scratch set
stay `FxIndexMap` / `FxIndexSet` — they are pass-local working data, not
HIR CFG structures.

Pure internal change: the fixtures snapshot is byte-identical; new unit
tests cover the `shift_remove` / `retain` / `drain` reindexing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-21 09:49:23 +08:00
Boshen a62edfcbb2 refactor(react_compiler): intern AliasingEffect diagnostics behind a DiagnosticId (#24664)
The `MutateFrozen`/`MutateGlobal`/`Impure` variants of `AliasingEffect`
each held an owned `OxcDiagnostic`. Objects allocated into an oxc arena
are never dropped, so `OxcDiagnostic` — which owns `Cow<'static, str>`
message/help plus a `Labels` vec — is a `Drop` type that blocks moving
the HIR into an arena.

Move the diagnostics into an `Environment`-owned side table and store a
`Copy` `DiagnosticId` in the effect instead:

- `Environment::intern_aliasing_diagnostic` interns a diagnostic and
returns its id, deduplicated on `(place, message, help)` — the same
granularity as the effect interning key (`EffectKey`). Identical errors
still collapse to one stored diagnostic, so `EffectKey` can key on the
id directly instead of cloning the message/help strings.
- The table uses `RefCell` because two effect-building passes
(`compute_signature_for_instruction`,
`compute_effects_for_legacy_signature`) only hold `&Environment`.
Interning is off the hot path — it happens only when an error effect is
produced.
- The range pass resolves the id back to the diagnostic at emit time
(`env.record_diagnostic(env.aliasing_diagnostic(id))`), the point at
which these errors were already surfaced.

`AliasingEffect` no longer carries any `Drop` diagnostic; its remaining
non-arena fields are the `Vec`s, which are the mechanical arena-`Vec`
migration tracked separately.

Pure internal change: the fixtures snapshot is byte-identical.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-21 09:49:23 +08:00
overlookmotel 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.
2026-07-20 10:48:48 +00:00
Boshen 22f6523b96 refactor(react_compiler): remove dead code and tighten visibility (#24701)
A follow-up dead-code sweep of `oxc_react_compiler`. Everything here is
**behavior-preserving** — the removed branch/variant never executed, and
the rest is stale attributes, a pass-through merge, and visibility.

## Dead code removed
- **Never-wired branch in `rename_variables`** —
`rename_variables_with_parent`'s `parent_names` was always `None` (its
only caller passes `None`; it doesn't recurse), so the `Some(..)`
"outlined functions" block and the wrapper split were dead. Inlined into
`rename_variables`.
- **`ApplyArgConfig::Spread`** — never constructed anywhere (only a
defensive match arm); removed the variant, its write-only `kind` field,
`Hole.kind`, and the now-orphaned `ApplyArgSpreadKind` /
`ApplyArgHoleKind` enums. (`ApplyArgConfig` derives only `Debug, Clone`,
so nothing built it via serde either.)

## Stale attributes / redundant machinery
- Delete 4 `#[allow(dead_code)]` markers whose items are actually used
now: `OxcContext::{fbt_operands, record_error}`, `Driver.env`,
`InferredDependency::Local.context`.
- Merge the pure pass-through `Unifier::unify` into `unify_impl`
(identical signature, no accumulator) — a single `unify`.

## Visibility
Tighten 8 items referenced only within their own module to private /
`pub(crate)`: `GlobalRegistry`/`ShapeRegistry::into_inner`,
`post_dominators_of`, `collect_maybe_memo_dependencies`,
`is_always_invalidating_type`, `MEMO_CACHE_SENTINEL`, `next_type_id`,
`validate_context_variable_lvalues_with_errors`.

`14 files changed, +17 / -85`. `cargo clippy` clean; unit + transform
tests pass.
2026-07-20 14:58:13 +08:00
Boshen cb102dedd1 refactor(react_compiler): drop dead error-plumbing and coercion helpers in globals (#24697)
Follow-up cleanup of `globals.rs` (and its two callers) in `oxc_react_compiler`. Removes machinery that no longer earns its keep — no behavior change.

## What

- **Collapse the `install_type_config` split.** `install_type_config_with_errors` wrapped a private `install_type_config_inner` whose `errors: &mut Option<&mut Vec<String>>` was *always* `Some` — the sole caller passed `Some` and the recursion only forwarded it. Vestigial from a non-error variant that no longer exists. Now a single `install_type_config` taking `&mut Vec<String>`.
- **Remove `shrink_global` / `shrink_shape`.** These were `fn(&T<'static>) -> &T<'a> { x }` identity functions; lifetime variance already performs that coercion in `Registry::get`, so they were no-op noise.
- **Tighten visibility.** `build_builtin_shapes` / `build_default_globals` are only used inside the `BASE` initializer, so they no longer need to be `pub`.

Net `3 files changed, 27 insertions(+), 53 deletions(-)`.
2026-07-20 04:58:28 +00:00
Boshen 3f03a5be7f refactor(react_compiler): drop Rc<FunctionSignature> from AliasingEffect::Apply (#24662)
`AliasingEffect::Apply` held `Option<Rc<FunctionSignature>>`. Objects allocated into an oxc arena are never dropped, so a refcounted `Rc` — like any `Drop` type — blocks moving the HIR into an arena.

The `Rc` was never shared ownership for its own sake; it was a borrow-detachment device. The signature is a deterministic lookup (`env.get_function_signature(env.types[…])`), read exactly once in the same pass that builds it and ignored by every later pass. The `Rc` existed only to snapshot the registry signature into owned data so `apply_effect` could read its fields while calling `&mut env` effect computations.

Store the callee's `TypeId` instead and resolve the `FunctionSignature` on demand. `TypeId` is `Copy`/non-`Drop` and lifetime-free, so no lifetime is forced onto `AliasingEffect`/`Terminal`/`HIR`. The id is propagated unchanged through the place-substitution remap in `compute_effects_for_aliasing_signature`, preserving the exact signature identity (re-deriving from the *substituted* callee place would not). The read site clones the resolved signature into a local to reproduce the previous borrow detachment. The now-dead `get_function_call_signature` helper is removed.

Pure internal change: the fixtures snapshot is byte-identical.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

<sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
2026-07-19 09:58:15 +00:00
Boshen c91d2a42c6 refactor(react_compiler): derive Copy for leaf HIR types (#24660)
The `react_compiler` HIR is an SSA/CFG IR, not an AST clone, so most of it cannot be `Copy` (the aggregate nodes hold `Vec`/`FxIndexMap`/`Box`). But a number of its **leaf** types have all-`Copy` fields and were needlessly deriving only `Clone`.

This derives `Copy` on the 23 leaf HIR types where it is free — `Place`, `Identifier`, `MutableRange`, `LValue`, the pattern/element enums (`ParamPattern`, `SpreadPattern`, `ArrayPatternElement`, `ArrayElement`, `PlaceOrSpread`, `ObjectPropertyOrSpread`, `ObjectProperty`, `ObjectPropertyKey`), the JSX leaves (`JsxTag`, `JsxAttribute`, `BuiltinTag`), `LoweredFunction`, `TemplateQuasi`, `PropertyNameKind`, and the binding/dep leaves (`VariableBinding`, `NonLocalBinding`, `ManualMemoDependencyRoot`, `DependencyPathEntry`) — and drops the now-redundant `.clone()` / lazy-eval calls that clippy flags as a result.

The aggregate nodes are deliberately untouched. This is a pure internal cleanup: the fixtures snapshot is byte-identical.

Net **-151 lines** across 31 files.
2026-07-19 06:47:24 +00:00
overlookmotel d76d89996f refactor(react_compiler): shorten code by using NONE (#24630)
Pure refactor.

Shorten code in React compiler by using `NONE` instead of writing out the type long-hand.

e.g. `None::<ArenaBox<TSTypeParameterDeclaration>>` -> `NONE`
2026-07-18 02:22:44 +00:00
overlookmotel 18ce449607 refactor(react_compiler): shorten code constructing ObjectPropertyKind (#24627)
Pure refactor. Shorten code by using an AST builder method that creates the desired type directly,
2026-07-17 13:49:41 +00:00
overlookmotel 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);
```
2026-07-17 13:49:39 +00:00
overlookmotel 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);
```
2026-07-17 13:49:39 +00:00
camc314 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.
2026-07-15 14:48:32 +00:00
Boshen 71c2f53e5e refactor(react_compiler): use unstable sorts (#24544)
Mechanical migration of all 14 stable sort calls in `oxc_react_compiler` to their unstable variants: 8 `.sort_by()` to `.sort_unstable_by()` and 6 `.sort_by_key()` to `.sort_unstable_by_key()`.

Unstable sort avoids the stable sort's temporary allocation and is the repo-wide convention where stability isn't required. Sorted outputs were checked against the full fixture snapshot corpus (`cargo test -p oxc_react_compiler`, insta-based over the upstream fixtures) and are unchanged, so no call site observably relies on stable-sort tie ordering. All sites were migrated; none needed to be kept stable.

Binary size is unchanged (measured at opt-level 3 with fat LTO); this is a code hygiene/consistency change, not a size optimization.

Note for future re-syncs with upstream `react_compiler_oxc`: this is a systematic porting rule — JS `Array.prototype.sort` (stable) becomes Rust `sort_unstable*` unless tie order is observable in output, in which case the stable variant is kept with a comment. Re-apply the rule when re-vendoring.

Authored with Claude Code; reviewed by a human before merge.
2026-07-15 13:11:43 +00:00
Boshen 246b426210 refactor(react_compiler): share one AST walk across entrypoint replace visitors (#24546)
Every distinct `Visit`/`VisitMut` implementor monomorphizes the entire generated AST walk; `react_compiler/entrypoint/program.rs` had three such types (`OxcReplaceFnVisitor`, `OxcReplaceWithGatedVisitor`, and the local `Finder`), so the walk was instantiated three times. They are now a single `OxcVisitor` selected by an `OxcVisitMode` enum (`ReplaceFns` / `ReplaceWithGated` / `FindOriginalFn`), so the walk is instantiated once.

Each overridden method dispatches on the mode and behaves exactly like the default walk in the modes that do not use it, preserving traversal order and the early-exit semantics (`remaining == 0`, `done`, `found.is_some()`). `Finder` moves from the read-only `VisitJs` walk onto the shared `VisitMut` walk — a superset traversal; it matches a unique `scope_id`, and function scopes cannot occur in the pruned type grammar (per #24532), so the lookup result is unchanged. The Babel-mirroring notes (`ReplaceFnVisitor` / `ReplaceWithGatedVisitor`) stay on the corresponding modes, and the per-node replacement logic is ported verbatim into free helpers (`ox_replace_function`, `ox_replace_arrow`, `ox_build_gated_const_decl`).

Measured on this crate's release example binary (`--example react_compiler`, workspace release profile with fat LTO): machine code (`__text`) shrinks by 24,112 bytes (~24 KB), file size by 16,592 bytes (~17 KB). The three walk instantiations measured ~34K/~16K/~14K in the symbol-level size analysis of the release napi binary that motivated this change.

Behavior is verified unchanged on the full fixture snapshot corpus (1,736 snapshots, including the `gating__*` fixtures that exercise the gated-replacement and original-function-lookup paths), and the release example binary produces byte-identical output before and after across the gating and general fixtures.

Authored with Claude Code; reviewed by a human before merge.
2026-07-15 12:15:06 +00:00
Boshen ba35a0d2e9 perf(react_compiler): use IndexVec for dense id-keyed maps (#24549)
Each distinct `FxHashMap<K, V>` monomorphizes its own copy of hashbrown; maps keyed by the dense u32 newtype ids become direct-indexed `IndexVec<Id, Option<V>>`s, removing those instantiations and the per-lookup hashing.

### Migration criteria

A map was migrated only when all of these hold:

1. The key is a dense id (`IdentifierId`, `DeclarationId`, `BlockId`) with a sound bound available at construction time: `env.identifiers.len()` (declaration ids share the identifier id index space — `DeclarationId::from_usize(id.index())` at allocation), `env.next_block_id_counter` (+1 for the synthetic exit node in the dominator), or the max block id present in the function body.
2. The map is only accessed by key (`get`/`insert`/`contains_key`); it is never iterated in a way that can reach the output. The one iterated map (the optional-chain temporaries merge) writes into another map with distinct keys, so iteration order is not observable.
3. `Option<V>` slots preserve the exact presence semantics, including "insert twice keeps latest" and `entry().or_insert()` first-wins. Where `map.len()` carried meaning, it was replaced with an explicit counter (`Rewrites::len` for the phi-elimination fixpoint, `BlockIdMappings::next` for sequential id assignment).
4. The map is allocated once per pass, not per inner loop iteration.

### Migrated maps

| File | Map |
| --- | --- |
| `react_compiler_hir/dominator.rs` | `PostDominator::nodes`, `Graph::node_index`, `raw_nodes`, `doms` (kills `FxHashMap<BlockId, BlockId>`, `<BlockId, Node>`, `<BlockId, usize>`) |
| `react_compiler_reactive_scopes/stabilize_block_ids.rs` | block id `mappings` (new `BlockIdMappings` with explicit `next` counter) |
| `react_compiler_optimization/prune_maybe_throws.rs` | `terminal_mapping` |
| `react_compiler_ssa/eliminate_redundant_phi.rs` | `rewrites` (new `Rewrites` with explicit entry count for the fixpoint check) |
| `react_compiler_validation/validate_hooks_usage.rs` | `value_kinds` (`Option<Kind>` is 1 byte) |
| `react_compiler_reactive_scopes/promote_used_temporaries.rs` | `State::pruned`, `inter_state` |
| `react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs` | `last_usage`, `temporaries` |
| `react_compiler_inference/propagate_scope_dependencies_hir.rs` | `temporaries` + `temporaries_read_in_optional` (the crate's most-instantiated map type, ×11), the declaring-scope map in `find_temporaries_used_outside_declaring_scope`, and `DependencyCollectionContext::{declarations, reassignments}` |

This eliminates 11 distinct id-keyed hashbrown instantiations from the crate.

### Deliberately skipped

- `outline_jsx.rs` `globals` and `infer_types.rs` / `name_anonymous_functions.rs` `names`: these create a fresh map per nested function, so a dense vec sized by the env-wide identifier count would allocate O(identifiers) for a handful of entries on every nested function.
- `outline_jsx.rs` `rewrite_instr` and other `EvaluationOrder`-keyed maps: no clean bound at the construction site and keys are sparse.
- `get_assumed_invoked_functions` temporaries in `propagate_scope_dependencies_hir.rs`: allocated per inner function (same per-iteration concern).
- `FxIndexMap` uses: insertion order is semantic there.
- Maps whose iteration feeds diagnostics or output ordering (e.g. `validate_no_derived_computations_in_effects.rs`).

### Measured effect

Release example binary (`examples/react_compiler`, macOS arm64): `__text` (machine code) shrinks 2,767,800 → 2,756,116 bytes (**−11,684 bytes, −0.42%**). The on-disk file size only changes by 16 bytes because the `__TEXT` segment is page-aligned; the machine-code section is the meaningful metric. The example links the full parser/codegen stack, so the relative saving within this crate's own code is larger. The main win is runtime: every migrated lookup is now a bounds-checked array index instead of a hash + probe.

The upstream fixture corpus (1,736 snapshots) is byte-for-byte unchanged, and `cargo clippy -p oxc_react_compiler --all-targets` is clean.

### Note for upstream re-syncs

This intentionally deviates from the upstream TypeScript structure (`Map`-based sidemaps in `PropagateScopeDependenciesHIR.ts`, `Dominator.ts`, etc.). When re-syncing this crate with upstream via `react_compiler_oxc`, please preserve the `IndexVec` representation for these maps.

Authored with Claude Code; reviewed by a human before merge.
2026-07-15 12:09:08 +00:00
Boshen f5040bed13 refactor(react_compiler): data-drive builtin global shape construction (#24551)
The builtin shape/global registries are built exactly once per process inside the `BASE` `LazyLock`; this PR replaces their straight-line construction code with const descriptor tables interpreted by small loops (and marks the builders `#[inline(never)]`/`#[cold]`), removing cold init code that the optimizer was aggressively inlining and unrolling.

**What changed**

- New const tables — `BUILTIN_SHAPE_DEFS`, `STATE_SHAPE_DEFS`, `REACT_HOOK_DEFS`, `TYPED_GLOBAL_OBJECTS` plus `PRIMITIVE_GLOBAL_FNS`/`COLLECTION_CTORS` — encode the repetitive `addFunction`/`addObject`/`addHook` registrations (method signatures, effects, aliasing configs, plain properties). Two small interpreters (`add_method`, `add_object_from_def`) replay them into the registries.
- Descriptors are kept compact so the code-for-data trade stays profitable: a 24-byte `TypeDef` mirror of the `Type` literals and `Option<&'static AliasingSignatureConfig>` instead of a 96-byte inline `Option`.
- Genuinely irregular entries stay imperative rather than being forced into tables: `use`, `useEffectEvent` (function-typed return), the React namespace and `_jsx` functions, `Infinity`/`NaN`, and the `globalThis`/`global` wiring. `build_default_globals`, `UNTYPED_GLOBALS`, `get_reanimated_module_type`, and the `install_type_config` path are untouched apart from the new attributes.
- All one-time builders (`build_builtin_shapes`, `build_default_globals`, `build_react_apis`, `build_typed_globals`, and their helpers) are `#[cold]` + `#[inline(never)]`, so they no longer get expanded into the `LazyLock` initializer and are optimized for size.

**Size impact (measured)**

Comparing `cargo build -p oxc_react_compiler --release --example react_compiler` (fat LTO, arm64 macOS) against the parent commit:

- `__text`: 2,767,800 → 2,739,076 bytes (−28.7 KB of machine code)
- const data (`__DATA_CONST.__const` + `__TEXT.__const`): +9.5 KB (the descriptor tables)
- net content: −19.2 KB; binary file size 3,098,512 → 3,082,352 bytes (−16.2 KB, page-granular)

For reference, a symbol-level size analysis of the release napi binary had attributed ~38 KB to this file's builders (~20 KB `build_builtin_shapes`, ~9 KB `build_typed_globals`).

**Resulting registries are unchanged**

The full upstream fixture snapshot corpus passes with zero snapshot changes, and a structural dump of both base registries (every global name resolved to its full type/signature, every named shape, and both maps' key iteration orders) is byte-identical before and after. The only internal difference is that a few anonymous `<generated_N>` shape ids are minted in upstream property order rather than the port's hoisted-`let` order; those ids are opaque registry keys, and every name-to-signature mapping is identical.

The data tables intentionally mirror the upstream Globals.ts / ObjectShape.ts entry order (and keep the upstream comments) so future re-syncs can diff them directly against the TS source.

Authored with Claude Code; reviewed by a human before merge.
2026-07-15 12:04:31 +00:00
Boshen 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).
2026-07-15 08:08:18 +00:00