mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/fix-semantic-parameter-resolution
903 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
a185ac3ff3 |
fix(codegen): print # before private identifier in TS signature key (#25938)
`TSPropertySignature` and `TSMethodSignature` were printing a `PrivateIdentifier` key as `key.name`, dropping the `#`, so `interface I { #x: number }` was printed as `interface I { x: number }`.
This PR switches `oxc_codegen` to delegating to the `PrivateIdentifier` printer, as every other property key site already does. It also applies the same thing to JS printer `oxc-codegen`.
This is not legal syntax anyway - TS rejects it with TS18016, and Oxc parser with "Private identifier '#x' is not allowed in property names" - so this is only reachable from a recovered parse or a hand-built AST. But, since we have this code, it might as well do what you'd expect it to.
|
||
|
|
0389c4010e |
release(crates): oxc v0.146.0 (#25912)
### 🚀 Features - |
||
|
|
4a4a1fb2e7 |
release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES - |
||
|
|
8ab883afaa |
fix(codegen): preserve property key annotations (#25766)
`@__KEY__` and `#__KEY__` are currently treated as normal comments. Codegen can drop them before a later property-mangling pass. The property may then be renamed while the annotated string keeps its old name.
This classifies the marker as a property-key annotation. Codegen keeps it next to string and no-substitution template literals when annotation comments are enabled, and the property mangler reads that classification instead of scanning source text. Final output still follows the normal comment options, so whitespace-minified or annotation-disabled output removes the marker.
## Example
Input:
```js
const object = { _field: 1 };
const key = /* #__KEY__ */ "_field";
object[key];
```
Before, an intermediate codegen pass could remove the marker:
```js
const object = { _field: 1 };
const key = "_field";
object[key];
```
After, the marker survives for the later property-mangling pass.
Verified with the affected crate suites, `just minsize`, workspace lint, docs, AST generation, and independent review. `just ready` is only blocked locally by existing Node-version and terminal-color snapshot differences.
Implemented with AI assistance (OpenAI Codex). I reviewed and understand the changes and remain responsible for this contribution under the repository AI usage policy.
|
||
|
|
c1369a71ff |
fix(codegen): resolve private names in class heritage (#25588)
### Summary
Fix private-member mangling for class heritage expressions.
The code generator previously activated a class's private-member mapping before emitting that class's decorators and `extends` expression. A private identifier in those expressions is evaluated in the enclosing lexical class scope, not the class currently being declared.
This change:
- reserves each class's mapping ID in AST traversal order;
- activates that mapping only immediately before emitting its class body;
- adds a mangler/codegen regression test covering a nested class with identically named private members.
### Reproduction
```js
class HasOuterBrand {}
class MissingOuterBrand {}
class Outer {
#x;
create(o) {
return class Inner extends (
#x in o ? HasOuterBrand : MissingOuterBrand
) {
#x;
};
}
}
```
`#x` in `Inner`'s heritage expression refers to `Outer.#x`. `Inner.#x` is not in scope until the `Inner` class body.
### Scope model
```text
Outer class body: active private mapping
Outer.#x -> #e
create(o) {
class Inner extends (#x in o ? ... : ...)
│
└─ resolves in Outer scope: #e
Inner class body: active private mapping
Inner.#x -> #t
}
```
### Before
Codegen entered `Inner` before emitting the entire class header:
```rust
p.enter_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
...
self.body.print(p, ctx);
```
That made private-identifier lookup prefer `Inner`'s mapping while printing `extends`, producing the wrong brand:
```js
return class t extends (#t in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
```
The `#t` in the heritage expression is invalid because `Inner.#t` is not yet in scope.
### After
The class ID is reserved first, preserving mapping IDs assigned in traversal order, but the class mapping is pushed only for the body:
```rust
let class_id = p.reserve_class();
p.print_decorators(&self.decorators, ctx);
...
heritage.expression.print_expr(...);
p.enter_class(class_id);
self.body.print(p, ctx);
p.exit_class();
```
```text
Class-ID allocation Active private-name scope
──────────────────── ─────────────────────────────
reserve Outer ID enclosing class / none
reserve nested header IDs enclosing class / none
enter Outer ID Outer body
enter Inner ID Inner body
```
Separating reservation from activation matters: moving allocation until after heritage could shift IDs for nested classes encountered in a heritage expression and break the mangler's precomputed mapping association.
### Regression coverage
The existing minifier mangler integration harness now snapshots this case with private-member mappings enabled. It confirms that the generated heritage expression uses Outer’s mapping (`#e`) while Inner’s field uses its distinct mapping (`#t`):
```js
class Outer {
#e;
create(e) {
return class t extends (#e in e ? HasOuterBrand : MissingOuterBrand) {
#t;
};
}
}
```
|
||
|
|
c002f29226 |
fix(codegen): escape sources for empty import specifiers (#25586)
## Summary
- Route empty named-import sources through the shared string-literal printer.
- Cover quote and newline escaping, including minified quote selection.
Given:
```ts
import {} from 'h"i'
```
Before it would be printed as:
```ts
import {} from "h"i" // missing escape of `"` or should be using `'` quotes
```
After:
```ts
import {} from 'h"i'
```
|
||
|
|
b4e6a9e717 |
fix(codegen): output newline after export default interface (#25487)
|
||
|
|
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. |
||
|
|
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. |
||
|
|
bbd6ed38df |
fix(codegen): wrap string literal in parens after directives (#25435)
When printing statements (`Codegen::print_directives_and_statements`), if first statement is an `ExpressionStatement` containing just a `StringLiteral`, it has to be parenthesized to avoid it becoming a directive.
Fix 4 different bugs in this logic.
## 1\. Only protected when there were no directives at all
The check was gated on `directives.is_empty()`, but the statement at risk is the first non-directive one, whatever its index. The second statement below printed bare, and re-parsing turned it into a directive - switching the program from sloppy to strict mode.
```js
// Input
"use server";
("use strict");
// Output, before
"use server";
"use strict"; // Now a directive - program is in strict mode
// Output, after this PR
"use server";
("use strict");
```
## 2\. No indent on the protected path
The path printed the statement itself instead of going through `ExpressionStatement`'s printer, and never printed the indent.
```js
// Output, before
function f() {
("x");
}
// Output, after this PR
function f() {
("x");
}
```
## 3\. Leading comments dropped on the protected path
Same cause - the statement's own printer is what prints its comments.
```js
// Input
// keep me
("use strict");
// Output, before
("use strict");
// Output, after this PR
// keep me
("use strict");
```
## 4\. Unprotected in minify mode
Protection was skipped entirely when minifying. A string usually minifies to a template literal, which cannot be a directive - but only where that is the shortest form. One backtick or `${` makes quotes shorter, and then it's printed as a directive.
```js
// Input
("`");
// Output, before
"`"; // Now a directive
// Output, after this PR
`\``;
```
Minified output is now always a template literal:
```js
("`"); // -> `\``;
("${}"); // -> `\${}`;
```
A template is never a directive, costs no more than `("...")`, and survives a round trip.
In the common case (no backticks or `${` in the string), printing as a template literal (`` `use strict`; ``) is more compact than a parenthesized quoted string (` ("use strict"); `).
Note: this visibly changes minified output for this construct - a template literal where there was a parenthesized string. Behavior is unchanged.
|
||
|
|
d07a293d12 |
fix(codegen): preserve cast parentheses in conditional tests (#25115)
## Summary - preserve parentheses around as and satisfies expressions used as conditional tests - prevent a following regular expression consequent from being parsed as part of the type expression - add regression coverage for both TypeScript expression forms Part of #25111. Closes #25114. |
||
|
|
8b7d4419ef |
fix(codegen): preserve satisfies parentheses before bitwise operators (#25113)
## Summary - preserve parentheses around a satisfies expression used as the left operand of bitwise OR or AND - prevent the operator from being consumed as part of the satisfies type - add regression coverage for both operators Part of #25111. Closes #25112. |
||
|
|
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
|
||
|
|
5c5cdcd522 |
feat(ast)!: narrow TSInterfaceHeritage::expression to TSTypeName (#24360)
## Summary
Narrow `TSInterfaceHeritage` from a general JavaScript `Expression` to `TSTypeName`.
```rust
pub struct TSInterfaceHeritage<'a> {
pub type_name: TSTypeName<'a>,
pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```
## Breaking Change
The Rust AST field changes from:
```rust
expression: Expression<'a>
```
to:
```rust
type_name: TSTypeName<'a>
```
## Why?
The current AST can represent shapes such as:
```ts
interface A extends foo() {}
interface A extends A + B {}
interface A extends new Foo() {}
interface A extends true {}
```
Which is an overly wide type since all of the above variants are invalid.
This PR tightens up the AST, making these invalid variants irrepresentable.
## How to migrate?
This should be a trivial migration:
1. instead of accessing `expression` on `TSInterfaceHeritage`, now access `type_name`
2. change any pattern matching on the `type_name` field to use `TypeName` (this is a smaller subset so should allow deleting code!)
closes https://github.com/oxc-project/backlog/issues/215
|
||
|
|
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
|
||
|
|
4d723caaff |
fix(parser): report TS1035 for quoted module names (#25305)
Quoted module names are only valid in ambient contexts in TypeScript, but we were accepting them in regular TypeScript files. This reports TS1035 when a quoted module name appears outside a declaration file or ambient declaration. The conformance snapshots now cover both TypeScript cases, and the old invalid codegen fixture has been replaced with a valid internal module declaration. |
||
|
|
45a17c25d1 |
release(crates): oxc v0.143.0 (#25250)
### 💥 BREAKING CHANGES - |
||
|
|
067da8c4e7 |
feat(ast)!: store single parameter in TSIndexSignature::parameter (#25154)
|
||
|
|
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
|
||
|
|
fc702c1fa9 |
release(crates): oxc v0.142.0 (#24977)
### 💥 BREAKING CHANGES - |
||
|
|
dba374912a |
refactor(ecmascript): share number literal serialization (#24876)
Codegen is not the only consumer that needs the exact emitted form of a number, but the serializer currently lives in the codegen crate. Other consumers would otherwise need to duplicate its formatting policy or depend on codegen. Move the callback helper extracted in #24888 from `oxc_codegen` to `oxc_ecmascript`, then reuse it from codegen. The serialization algorithm is unchanged; this PR only makes the helper public and relocates its dependencies. This PR depends on #24888. #24889 handles allocation-free candidate construction, and #24875 adds the minifier consumer. Verified independently with `cargo test -p oxc_ecmascript -p oxc_codegen`, Clippy, and formatting. AI assistance: Codex was used to implement and verify this change. |
||
|
|
2ee6e7a759 |
refactor(codegen): extract number literal serialization (#24888)
Number literal serialization is coupled to `Codegen`, even though the formatter produces one string before codegen writes it. That coupling makes the formatting policy difficult to reuse without mixing a move with an implementation rewrite. Extract the existing algorithm into a codegen-local callback helper. Generated output and allocation behavior are unchanged. This is the first PR in the stack. #24876 moves the helper unchanged into `oxc_ecmascript`. Verified with `cargo test -p oxc_codegen`, Clippy, formatting, and the combined stack with `just ready`. AI assistance: Codex was used to implement and verify this change. |
||
|
|
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 - |
||
|
|
6cc4a34307 |
fix(codegen): wrap negative BigInt literal base of ** in parentheses (#23621)
### Summary
Fixes invalid codegen output where a negative `BigInt` literal used as
the base of an exponentiation (`**`) expression was emitted without the
required parentheses, producing code that the parser rejects.
### Problem
Minifying `x = (0n + -1n) ** 2n;` folds `0n + -1n` into a single
negative `BigIntLiteral` and then prints `x = -1n ** 2n;`, which is a
`SyntaxError` ("Unexpected exponentiation expression") — the same
grammar restriction that makes `-1 ** 2` illegal. The equivalent number
case already prints correctly as `x = (-1) ** 2;`.
### Root cause
In `crates/oxc_codegen/src/binary_expr_visitor.rs`, the `**` base
special-case raises the left operand precedence to `Precedence::Call`
(forcing parentheses) for `UnaryExpression`, `AwaitExpression`,
`TSTypeAssertion` and `NumericLiteral`, but omitted `BigIntLiteral`,
which also prints with a leading `-` for negative values. So a negative
BigInt base kept `left_precedence = Exponentiation` and was emitted
without parentheses.
### Fix
Add `BigIntLiteral` to that match so a negative BigInt base is
parenthesized like a negative number base. A positive BigInt base (e.g.
`2n ** 3n`) is unaffected and not over-parenthesized.
### Testing
- Added regression test `test_exponentiation_negative_bigint_base` in
`crates/oxc_minifier/tests/peephole/obscure_edge_cases.rs` (a negative
BigInt literal only arises from constant folding, not from parsing).
- `cargo test -p oxc_codegen` passes; the only failing snapshot,
`sourcemap::stacktrace_is_correct`, is a pre-existing Node.js version
mismatch unrelated to this change.
|
||
|
|
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); ``` |
||
|
|
70994ae544 |
fix(codegen): preserve comments before expression operands (#24510)
Coverage directives attached to expression operands were silently dropped by codegen in logical right-hand sides, conditional consequents, object property values, and template literal interpolations. The AST expressions survive, but those emitters never consume their leading comment groups. Print the comment group at each surviving expression boundary, probing parenthesized layers (`a || /* c */ (x)` anchors the comment at the `(`, `a || (/* c */ x)` at `x`). Parenthesized function and arrow IIFEs keep comments inside their generated wrapper. Logical right-hand sides and conditional consequents only print groups containing annotations. This is codegen policy for mutated ASTs: comments are anchored by source position, and mutating consumers (minifier statement merging, rolldown finalization) move statements into exactly these positions — a dissolved statement's leading normal-comment group can coincide with the moved operand's span start, and printing it there misplaces statement-level trivia and breaks minify-twice idempotency (test262 `language/asi/S7.9_A5.8_T1.js` and friends). Annotations are the one comment kind with expression-level meaning, so they pass through. Printing comments at expression boundaries also exposed three latent spacing bugs in the shared comment printer, surfaced by monitor-oxc over real node_modules: a full indent was injected before mid-line groups (doubling indentation on every pass, invisible at indent 0), a group ending in a newline left the next operand at column 0, and minify-with-comments mode emitted a glue space or not depending on which printer consumed the group. All three are fixed with regression tests. ## Example Input: ```js const value = a ?? /* istanbul ignore next */ []; ``` Before: ```js const value = a ?? []; ``` After: ```js const value = a ?? /* istanbul ignore next */ []; ``` Verified with `cargo test -p oxc_codegen` (134 tests), full `cargo coverage` (minifier at baseline, zero snapshot drift), `--twice` idempotency over webpack/typescript/eslint-utils/storybook/sigstore files in plain, dce, and minify-with-comments modes, and monitor-oxc green on codegen/dce/whitespace/transformer (remaining failures are main's pre-existing baseline). closes #21301 closes #22049 closes #24324 This PR was assisted by OpenAI Codex and Claude. |
||
|
|
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.
|
||
|
|
6ea3e0620c |
chore(deps): update Node.js to 26.5.0 (#24507)
## Summary - update .node-version from 24.14.0 to 26.5.0 - verify v26.5.0 as the latest v26 release in the official Node.js distribution index (released 2026-07-08) |
||
|
|
8e0ed2ebb9 |
release(crates): oxc v0.140.0 (#24453)
### 🚀 Features - |
||
|
|
a82a400c15 |
refactor(rust): use stabilized ptr::as_ref_unchecked (#24381)
Rust 1.95 stabilized [`<*const T>::as_ref_unchecked`](https://doc.rust-lang.org/std/primitive.pointer.html#method.as_ref_unchecked) (MSRV bumped in #24359). This replaces the `ptr.as_ref().unwrap_unchecked()` idiom at its 5 call sites: - `oxc_parser` `lexer/source.rs` `read()`/`read2()` — the doc comment there explains the idiom exists because producing a `&u8` carries `nonnull`/`dereferenceable` LLVM metadata that a raw deref does not (7% lexer speedup). `as_ref_unchecked` is implemented as exactly that reference production (`&*self`), so codegen is unchanged — it just skips the `Option` detour. Comment updated to name the new method. - `oxc_codegen` `lib.rs` — `</script` search loop. - `oxc_ast_visit` `utf8_to_utf16/translation.rs` — chunk read. - `oxc_ast` `generated/ast_kind.rs` `AstKind::ty` — changed via its generator in `tasks/ast_tools`, regenerated with `just ast`. Verified: `cargo test` for the touched crates, and parser/semantic/codegen conformance (test262 / babel / typescript / misc) with **zero snapshot diffs**. CodSpeed should show this perf-neutral. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
69b2dfc181 |
release(crates): oxc v0.139.0 (#24218)
### 🚀 Features - |
||
|
|
c59f2fe031 |
perf(rust): return impl ExactSizeIterator from slice-backed accessors (#24144)
These slice/`Vec`/`IndexVec`-backed accessors are already `ExactSizeIterator` internally (`slice::iter().map(..)` / `.copied()` / `.rev()`, `iter_enumerated()`, `HashMap::values()`, `(0..len).map(..)`) — the `impl Iterator` return type just erased it.
Widening to `impl ExactSizeIterator` lets callers pre-size collections from the known length:
```rust
let mut v = Vec::with_capacity(scoping.symbol_ids().len());
v.extend(scoping.symbol_ids().filter_map(..));
```
instead of growing a `Vec` through reallocs, and lets callers call `.len()` directly.
Backward-compatible: `ExactSizeIterator: Iterator`, so existing callers are unaffected.
Two ordering details make it compile:
- The `multi_index_vec!` `iter_ids` (`(0..len).map(..)`) is widened first so the bound propagates to `SymbolTable`/`ScopeTable`, which is what lets `symbol_ids`/`scope_descendants_from_root` widen.
- The delegating `TraverseCtx::ancestors` is widened after its `TraverseAncestry::ancestors` source — an opaque `impl Iterator` only re-exposes `ExactSizeIterator` if the inner signature declares it.
Widened accessors:
- **oxc_semantic**: `Scoping::{symbol_names, resolved_references, symbol_ids, scope_descendants_from_root, iter_bindings, iter_bindings_in}`, `AstNodes::{iter, iter_enumerated}`, `ClassTable::{iter_enumerated, iter_private_identifiers}`, `multi_index_vec!` `iter_ids`
- **oxc_traverse** / **oxc_minifier**: `TraverseAncestry::ancestors`, `TraverseCtx::ancestors`
- **oxc_formatter_core**: `SourceText::{bytes_from, bytes_to}`
- **oxc_codegen**: `Codegen::current_class_ids`
|
||
|
|
5d6d04a708 |
perf(codegen): SWAR-skip boring byte runs in sourcemap line/column scan (#24023)
## What `update_generated_line_and_column` (in `oxc_codegen`'s sourcemap builder) scans every emitted byte once to track the generated line/column for source-map tokens. Profiling `Codegen::build` with source maps enabled shows it is the **single hottest function** (~20% of self-time) — it walks the output byte-by-byte with a `match` per byte. The vast majority of output bytes are "boring": ASCII that is neither a line break nor part of a Unicode char, and that don't change the line count or the last-line ASCII flag. This skips runs of them **8 bytes at a time** with a branchless SWAR test — the classic zero-byte-in-a-word trick for `\n`/`\r` plus a high-bit test for non-ASCII — and only falls back to the (unchanged) per-byte logic at the interesting bytes. ## Why it's correct The fast-forward **only advances the cursor**; it never mutates line/column/ASCII state. It skips exactly the bytes the old scalar path already handled with `idx += 1` and no state change, so it is a provably-equivalent transformation and the emitted source map is byte-identical. - SWAR predicate exhaustively checked against a brute-force reference (a false positive only forces a harmless scalar fallback; false negatives — the dangerous case — are ruled out). - New unit tests cover long lines, `\r\n` / lone `\r`, embedded Unicode (`Ö`), and U+2028, **including line breaks straddling the 8-byte word boundary**. - Existing `sourcemap.rs` integration tests + the full `oxc_codegen` suite pass; no `unsafe`, all slice access bounds-guarded. ## Performance Interleaved A/B (sourcemap-enabled codegen, median-of-medians): | file | delta | | --- | --- | | binder.ts | ~-4.5% | | react.development.js | ~-2% | | App.tsx | ~-1 to -3% | | kitchen-sink.tsx (short-token synthetic) | ~flat | The win scales with the amount of long string/comment/line content (≥8 boring bytes per run); there is no effect when source maps are disabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
8bf69a3076 |
release(crates): oxc v0.138.0 (#23934)
### 💥 BREAKING CHANGES - |
||
|
|
3d61dea7e2 |
docs(all): correct capitalization in comments (#23887)
Just fix capitalization in comments. |
||
|
|
f2091b3846 |
feat(ast): unify old and new AstBuilders (#23875)
Part of #23043. Recent PRs have introduced a new `AstBuilder` type, and migrated all Oxc's crates over to it. To allow downstream consumers to incrementally migrate from old `AstBuilder` to new one, unify the two implementations into a single `AstBuilder` type. Remove the `builder_new` Cargo feature and instead add a new feature `disable_old_builder`. When enabled, this feature removes all the methods from `AstBuilder`, and makes `AstBuilder` non-`Copy`. Consumers should migrate, and then enable this feature to ensure later changes don't use the methods which are slated for removal. Enable the `disable_old_builder` feature in all Oxc crates which use `AstBuilder` _in tests_. This ensures we don't go back to using the deprecated methods, while not forcing the feature on consumers (e.g. Rolldown) who use e.g. `oxc_transformer` but also use `AstBuilder` directly. |
||
|
|
15a4778a74 |
test(codegen): switch to new AstBuilder (#23836)
Part of #23043. Migrate `oxc_codegen` over to new `AstBuilder`. It only uses `AstBuilder` in one test, not in the main code. |
||
|
|
cd1fd12bd2 |
feat(codegen): expose Codegen::print_string API (#23785)
## Summary - expose `Codegen::print_string` for printing an escaped JavaScript string literal from `&str` - share the existing string escaping implementation without constructing an AST - disallow template literal output so the API is safe in string-literal-only contexts - add coverage for quote selection, escaping, Unicode, and minified output Part of #23780. **Stack:** - https://github.com/oxc-project/oxc/pull/23786 - https://github.com/oxc-project/oxc/pull/23785 (this PR) - https://github.com/oxc-project/oxc/pull/23782 |
||
|
|
6d56d57535 |
release(crates): oxc v0.137.0 (#23591)
### 💥 BREAKING CHANGES - |
||
|
|
837a395fba |
fix(parser): treat a line comment after ':' as leading, not trailing (#23515)
## Summary A line comment immediately after a `:` on the same line was treated as a *trailing* comment in `should_be_treated_as_trailing_comment`, which anchors it to position `0` and drops it during codegen. The `=` and `(` tokens were already exempted from this rule for the same reason; `:` was not. This surfaced as a transformer **idempotency** failure (caught by [monitor-oxc](https://github.com/oxc-project/monitor-oxc/actions/runs/27659196490/job/81800055426)): lowering optional chaining emits `? void 0 : // comment\n_obj.method()`, so the leading comment of the original expression lands right after the `:`. Codegen kept it on the first pass but dropped it on re-parse, so `transform(transform(x)) != transform(x)`. Treating such a comment as **leading** anchors it to the following operand — preserving it and restoring idempotency. Comments after `:` in switch cases and labeled statements are likewise no longer dropped. (Object-property values and type-annotation colons are unaffected: still printed where they already were, and verified idempotent.) ## Example Input: ```js x = cond ? a : // c b; ``` Before this PR the comment was dropped (`x = cond ? a : b;`); now it is preserved. ## Tests - `leading_comments_after_colon` (`oxc_parser`) — the comment anchors to the following token. - `test_line_comment_after_conditional_colon` (`oxc_codegen`) — exact output + idempotency, including the lowered optional-chaining shape. - Verified idempotent across every `:` context (ternary, object value, switch case, label, TS type annotation); no committed transform-conformance fixture contains the affected pattern, so no snapshots change. |
||
|
|
827248a7fb |
release(crates): oxc v0.136.0 (#23443)
### 💥 BREAKING CHANGES - |
||
|
|
7a24911846 |
perf(codegen)!: borrow sourcemaps from codegen (#23422)
Summary: - Upgrade oxc_sourcemap to 8.0.1. - Return borrowed SourceMap from CodegenReturn. - Support no-sourcemap feature builds used by benchmarks. AI-assisted: yes |
||
|
|
65f9233148 |
refactor(parser): emit all TS1274 checks from parser (#23361)
Move TS1274 variance modifier validation into the parser and remove the duplicate semantic diagnostic path. |
||
|
|
e89729b178 |
perf(codegen): accept one-shot wrap closures (#23265)
## Summary - Change `Codegen::wrap` from `FnMut` to `FnOnce`, matching how the helper is used: every closure passed to `wrap` is invoked exactly once. - Add an assembly comparison note showing the optimized codegen difference before and after the change. ## `Fn`, `FnMut`, and `FnOnce` ? `Fn`, `FnMut`, and `FnOnce` all describe how a closure may be called: - `Fn` is the most restrictive for the closure body: it is callable through `&self`, can be called repeatedly, and cannot require mutable or consuming access to captured state. - `FnMut` is callable through `&mut self`, can be called repeatedly, and may mutate captured state. - `FnOnce` is callable by value, may consume captured state, and is only guaranteed to be callable once. The trait relationship goes from most specific to most general call capability: ```rust Fn: FnMut FnMut: FnOnce ``` So accepting `FnOnce` is the least restrictive bound for a callback that is only invoked once. It still accepts `Fn` and `FnMut` closures, but it also tells the optimizer that `wrap` does not need a reusable mutable closure object. ## Assembly Impact `Codegen::wrap` is an inline generic helper, so there is no stable standalone `wrap` assembly symbol in release output. The impact shows up in monomorphized call sites such as `Class::gen`, `Function::gen`, and expression `gen_expr` closures. Before, several call sites materialized a closure environment on the stack before calling the closure: ```asm strb w9, [sp, #15] add x9, sp, #15 stp x0, x9, [sp, #16] add x0, sp, #16 bl <...>::{{closure}} ``` After the `FnOnce` bound, the same shape can pass the one-shot closure state directly through registers: ```asm and w1, w2, #0xfffffffd mov x2, x19 bl <...>::{{closure}} ``` Some wrapper frames also shrink. For example, representative `Class::gen` / `Function::gen` paths go from a `64` byte frame to a `48` byte frame: ```asm - sub sp, sp, #64 - stp x20, x19, [sp, #32] - stp x29, x30, [sp, #48] + sub sp, sp, #48 + stp x20, x19, [sp, #16] + stp x29, x30, [sp, #32] ``` Several restored-frame return paths also become tail calls: ```asm ldp x29, x30, [sp, #32] ldp x20, x19, [sp, #16] add sp, sp, #48 b <closure or push_slow target> ``` The assembly diff also contains expected local label renumbering noise, such as switch-table suffixes changing from `.318` to `.330`; those are not behavior changes. |