mirror of
https://github.com/oxc-project/oxc.git
synced 2026-09-14 19:36:11 +08:00
codex/parser-grouped-expression-allocation
1404 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60fa13878c |
release(crates): oxc v0.149.0 (#26397)
### 💥 BREAKING CHANGES - |
||
|
|
8b35abdfd9 |
fix(semantic): detect duplicate private class elements (#26361)
The duplicate class element check stored only the last element for each name. This allowed a public element to hide an earlier private declaration, and a setter to hide an earlier getter (or vice versa):
```js
class DuplicateField {
#x;
x;
#x;
}
class DuplicateGetter {
get #x() {}
set #x(value) {}
get #x() {}
}
```
Both classes were accepted despite declaring the same private field or getter twice. The same problem affected setter/getter/setter sequences.
Key a single map by both the element name and whether it is private, so public elements cannot replace private declarations. This keeps the namespaces separate while reserving map capacity only once. For each private name, retain separate slots for setters and other declarations. Look for a previous declaration in the matching slot first, then fall back to the other slot. This catches repeated getters and setters while still allowing one getter/setter pair when both have the same static status.
The existing diagnostic logic continues to reject private names shared by static and instance elements, including the TypeScript-specific diagnostic. Public elements continue through the existing public-element duplicate check.
|
||
|
|
457ed57ff2 |
fix(semantic): reject await and yield in rest parameter defaults (#26359)
The parameter check only recognized `FormalParameter`, allowing `await` and `yield` in destructuring defaults inside rest parameters:
```js
async function f(...[x = await 0]) {}
function* g(...[x = yield 0]) {}
```
Include `FormalParameterRest` so both cases report the existing parameter diagnostic. The check still stops at nested functions, where `await` and `yield` can be valid.
|
||
|
|
4b89c7bd55 |
fix(semantic): reject jumps across arrow functions (#26357)
Reject labeled and unlabeled `break` and `continue` statements that cross an arrow-function boundary.
For example:
```ts
while (true) {
(() => {
break;
});
}
```
now reports an illegal break, while jumps targeting loops inside the arrow remain valid.
|
||
|
|
894c8f9cd8 |
release(crates): oxc v0.148.0 (#26198)
### 💥 BREAKING CHANGES - |
||
|
|
a3d33dda7c |
fix(semantic): skip body bindings in type parameters and this param (#26169)
#26099 resolves parameter and signature references before visiting the function body, so they cannot bind to declarations that only exist inside the body. That early pass only ran when the function had parameters or a return type. Type parameter constraints and a `this` parameter type are visited before that check, so a function whose signature has references only in those positions skipped the pass. Its references then resolved after the body was visited and bound to body-local types. ## Example ```ts type U = number; function f<T extends U>() { type U = string; return null! as T; } ``` Before this change, `U` in the constraint resolved to the body-local `type U = string`. TypeScript scopes local types to the function body, so it must resolve to the outer alias. The same happened for `function f(this: U) { type U = string }` and for arrow functions with type parameters. The early pass now runs for every function and arrow function. It returns immediately when the signature produced no references, so the `has_parameter() || return_type.is_some()` condition, which approximated that check, is no longer needed. Also adds a regression test for the compaction in `resolve_references_for_current_scope`: a resolved reference followed by an unresolved one must neither drop nor duplicate entries, and no existing test covered that ordering. Verified with the semantic and transformer unit tests, Clippy, `cargo coverage -- semantic`, and transform conformance; no snapshots changed. AI assistance: Claude Code was used to investigate, implement, and verify this change. The contributor is responsible for the submitted change. |
||
|
|
a09c3e0220 |
refactor(semantic): simplify resolve_references_for_current_scope (#26167)
Follow-up to https://github.com/oxc-project/oxc/pull/26099: make it readable and easy to understand. |
||
|
|
4adfb4c949 |
fix(semantic): validate chained continue labels (#26157)
## Summary - follow chained labels to their terminal statement when validating labeled `continue` - reject label chains that terminate in a non-iteration statement - add a focused negative coverage fixture and update the parser coverage snapshot |
||
|
|
c3dedc9b0c |
fix(semantic): skip function body bindings in parameters (#26099)
Function parameters, TypeScript signatures, and function bodies currently share one Oxc semantic scope. A reference in a parameter can therefore bind to a declaration inside the body, even though JavaScript does not make body declarations visible there. Rolldown may then treat a global as local, skip a configured define, or remove live code.
## Example
```js
export function load(value = typeof window !== "undefined") {
var window;
return value;
}
```
The body `var window` is not visible to the default value. Before this change, Oxc resolved `window` in the default value to that body declaration. It should remain a global reference.
The workaround resolves parameter and signature references before visiting the body, while only parameter bindings are present. If a reference is still unresolved, its `lookup_scope_id` advances to the function's parent. Final resolution resumes there, so declarations added while visiting the body are skipped. References created in the body still begin at the function scope and resolve normally.
```text
Before: parameter reference -> completed function scope -> body binding (wrong)
After: parameter reference -> parameter bindings -> parent scopes
body reference -> completed function scope -> parent scopes
```
Parameter decorators already begin in the enclosing class scope, so they are left for normal final resolution. This emulates separate parameter and body scopes until https://github.com/oxc-project/backlog/issues/176 is implemented.
The corrected semantic data also exposes the transformer case covered by #26105.
Verified with semantic and transformer-plugin tests, Clippy, coverage, allocation snapshots, and diff checks. `just ready` completed workspace checks but stopped at a codegen snapshot recorded with Node.js v26.5.0; the local version is v25.8.1.
Closes #20427
Closes #22783
Downstream reports:
- https://github.com/rolldown/rolldown/issues/10722
- https://github.com/rolldown/rolldown/issues/10779
AI assistance: OpenAI Codex was used to investigate, implement, and test this change. The contributor is responsible for the submitted change.
|
||
|
|
8531b9b2a4 |
fix(linter/plugins): alter method for obtaining mutable Program when sending AST to JS plugins (#26077)
### What's needed for JS plugins When running JS plugins, we need to obtain a mutable copy of the AST (`&mut Program`) in order to alter it (update spans to UTF-16 offsets). But `Semantic` only contains an immutable `&Program` reference, and also its `AstNodes` contains immutable references to _all_ nodes in the AST. So we need to do 2 things: 1. Convert the `&Program` from `Semantic` to `&mut Program`. 2. Get rid of all the active references to AST nodes stored in `AstNodes` so that the `&mut Program` doesn't illegally alias with those references. ### Previous solution and its problems Previously we achieved this with a dodgy hack where we discarded all of `Semantic` (to get rid of the AST node references) and then "laundered" the pointer to the `Program`, upgrading it to write permission, by taking the provenance of the `Allocator`'s cursor pointer (which does have write permission). However, this relied on the `Program` being in the `Allocator`'s current chunk, so that the `Allocator`'s current cursor and the `Program` are within the same allocation (a requirement for the pointer returned from `NonNull::with_addr` being legal to dereference). This assumption was always a bit shaky, but when we enabled React Compiler rules by default in last release, it became clear it was completely untenable. React Compiler rules allocate a lot, and the debug assertions which checked that the `Program` was in current chunk started failing - so many allocations had occured that the `Allocator` had grown a fresh chunk to accomodate them. ### How this PR fixes the problem This PR alters the method by which we obtain the `&mut Program`. It still discards all the references to AST nodes held in `Semantic`, but now performs a bitwise copy of the original `Program` and allocates that copy back into the arena, yielding a `&mut Program`. This avoids the need for pointer "laudering" and removes the requirement for `Program` to be in the current `Allocator` chunk. As noted in the comments, this is still not sound. We have no way to prove that no other references to AST nodes exist, stashed somewhere, which would be an aliasing violation (UB). However, a thorough review of the code shows that we currently do not do this, so there is no actual UB at present. So, in short, this change does not achieve soundness, but it does at least swap definite proven UB for a _potential risk_ of UB which isn't currently triggered. Not great, but better. ### Side effects Previously we nuked the whole of `Semantic`, replacing it all with a new empty copy. Instead this PR operates more "surgically", just removing the AST node and `Comment` references that we _need_ to get rid of to avoid aliasing violations, and leaving the rest of `Semantic` alone. This is much cheaper. |
||
|
|
4e258430cd |
release(crates): oxc v0.147.0 (#26046)
### 🚀 Features - |
||
|
|
2cde1f6671 |
fix(rust): address nightly deprecations (#25998)
Addresses deprecations reported by the latest Rust nightly while preserving Rust 1.98 compatibility. - Rename `unit-bindings` to `unit_bindings`: hyphenated lint names are deprecated in favor of their canonical underscore spelling. - Replace `isize::max_value()` and `usize::max_value()` with `isize::MAX` and `usize::MAX`: the legacy numeric methods are deprecated in favor of associated constants. - Set explicit `MAX_INDEX` values on `define_index_type!`: `oxc_index` otherwise generates `<raw>::max_value() as usize`; the explicit modern constants avoid that deprecated expansion without changing the index limits. - Remove obsolete `clippy::legacy_numeric_constants` expectations: the corrected code no longer triggers that lint, so the expectations would be unfulfilled. - Allow the AST scanner to consume optional `define_index_type!` configuration: it previously parsed only the struct declaration and rejected the added `MAX_INDEX` option. AI-assisted. |
||
|
|
0389c4010e |
release(crates): oxc v0.146.0 (#25912)
### 🚀 Features - |
||
|
|
4a4a1fb2e7 |
release(crates): oxc v0.145.0 (#25865)
### 💥 BREAKING CHANGES - |
||
|
|
fca2e0cfed |
fix(parser): reject initialized lexical declarations in for-in (#25700)
Rejects initialized `let` and `const` declarations in `for...in` during parsing while preserving Annex B's non-strict `var` form. Adds misc coverage for valid and invalid initializer forms. Verified with `just ready` and parser conformance. Closes #25149. AI-assisted. |
||
|
|
059784d96b |
fix(semantic): respect shadowed Infinity and NaN in enum evaluation (#25604)
## Summary Fix enum evaluation to resolve identifiers before treating `Infinity` and `NaN` as unbound globals. |
||
|
|
757f3d4113 |
perf(mangler): share allocated names across reused slots (#25605)
Avoid duplicate semantic-arena allocations when multiple symbols share one mangler slot. Previously, Phase 5 renamed every symbol in a shared slot individually. Each call to `Scoping::set_symbol_name` cloned the same generated `Ident` into the semantic arena, so a slot shared by `N` symbols made `N` allocations and copied the name `N` times. This adds `Scoping::set_symbol_names`, which clones the generated name once into the semantic arena and copies the immutable `Ident` handle to every symbol in that slot. The name must still be cloned once: generated names are backed by the mangler’s temporary allocator, which may be reset after mangling. Sharing that temporary backing would be unsound. ## Before / after For a slot containing `N` symbols and a mangled name of length `L`: | | Arena allocations | Copied bytes | |---|---:|---:| | Before | `N` | `N × L` | | After | `1` | `L` | | Benchmark | Old: one per symbol | New: one per slot | Saved | |---|---:|---:|---:| | `RadixUIAdoptionSection.jsx` | 12 | 12 | 0 | | `react.development.js` | 470 | 143 | 327 | | `App.tsx` | 1,791 | 426 | 1,365 | | `binder.ts` | 930 | 374 | 556 | | `kitchen-sink.tsx` | 5,422 | 1,058 | 4,364 | | `RadixUIAdoptionSection.jsx_keep_names` | 12 | 12 | 0 | |
||
|
|
50201a465e |
chore(semantic): gate cfg example behind feature (#25609)
Before, when running semantic's CFG example, we would get a cryptic message talking about the problem.
```
$ cargo run -p oxc_semantic --example cfg --
Compiling oxc_semantic v0.144.0 (/Users/cameron/.codex/worktrees/6012/oxc/crates/oxc_semantic)
error[E0601]: `main` function not found in crate `cfg`
--> crates/oxc_semantic/examples/cfg.rs:169:2
|
169 | }
| ^ consider adding a `main` function to `crates/oxc_semantic/examples/cfg.rs`
For more information about this error, try `rustc --explain E0601`.
error: could not compile `oxc_semantic` (example "cfg") due to 1 previous error
```
After, we get a clear message explaining how to fix the issue and run the example.
```
$ cargo run -p oxc_semantic --example cfg --
error: target `cfg` in package `oxc_semantic` requires the features: `cfg`
Consider enabling them by passing, e.g., `--features="cfg
```
Docs: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#the-required-features-field
|
||
|
|
509931bd27 |
fix(semantic): classify global references per identifier (#25608)
## Summary
- classify global references from each identifier reference rather than unresolved-name membership
- retain configured disabled-global handling in the linter context
- add semantic and no-promise-executor-return regressions for a shadowed `Promise`
## Before
```js
Promise.resolve(); // unresolved global reference
function f(Promise) {
new Promise((resolve, reject) => 1);
// ^ incorrectly classified as global because another `Promise` is unresolved & inserted in `root_unresolved_references`
}
```
## After
```js
Promise.resolve(); // still an unresolved global reference
function f(Promise) {
new Promise((resolve, reject) => 1);
// ^ correctly classified as the local parameter
}
```
|
||
|
|
de9bf67dee | chore(semantic): remove dead assertion macro arm (#25607) | ||
|
|
cdf1846af2 |
fix(semantic): allow legacy escapes in JSX attributes (#25576)
Quoted JSX attribute values do not use ECMAScript escape sequences. Skip strict-mode legacy escape validation for JSX attributes and cover the behavior in misc conformance. AI-assisted by Codex. |
||
|
|
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. |
||
|
|
dd1430baed | perf(semantic): avoid allocation in class overload check (#25440) | ||
|
|
0e36e21594 |
refactor(semantic): remove NodeFlags::HasYield (#25399)
## Summary - Remove `NodeFlags::HasYield` and its accessor now that `require-yield` detects yields locally. - Delete the semantic-builder state that existed only to populate the flag. - Remove `AstNodeStore::current_node_flags`, which has no remaining writers or non-empty values. |
||
|
|
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
|
||
|
|
c3918302a8 |
refactor(parser): check multiple for-loop declarations (#25344)
Moves the validation that rejects multiple declarations in `for...in` and `for...of` loop heads from semantic analysis into the parser. Part of #25149. |
||
|
|
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
|
||
|
|
6be314f226 |
feat(ast)!: remove duplicated VariableDeclarator::kind (#25319)
## Summary
- Removes `VariableDeclarator::kind`.
- Makes `VariableDeclaration::kind` the single source of truth for every declarator in its declaration list.
- Makes contradictory parent and child declaration kinds unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(skip)]
pub kind: VariableDeclarationKind,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
to:
```rust
pub struct VariableDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub kind: VariableDeclarationKind,
pub declarations: Vec<'a, VariableDeclarator<'a>>,
#[ts]
pub declare: bool,
}
pub struct VariableDeclarator<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
#[estree(via = VariableDeclaratorId)]
pub id: BindingPattern<'a>,
#[ts]
#[estree(skip)]
pub type_annotation: Option<Box<'a, TSTypeAnnotation<'a>>>,
pub init: Option<Expression<'a>>,
#[ts]
pub definite: bool,
}
```
## Why?
A variable declaration has one declaration kind shared by every declarator in its declaration list:
```js
const first = 1, second = 2;
```
Previously, the AST stored that kind on both the parent declaration and every child declarator:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Const, ... },
VariableDeclarator { kind: Const, ... },
],
}
```
This allowed contradictory ASTs to be constructed:
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { kind: Var, ... },
],
}
```
Different consumers read different copies:
- Code generation used `VariableDeclaration::kind`.
- Semantic binding used `VariableDeclarator::kind`.
- Lint rules, transforms, and minifier passes used a mixture of both.
As a result, one AST could be emitted as `const` while its binding was treated as function-scoped `var`.
Transforms also had to keep the copies synchronized manually. For example, const-to-let normalization updated the parent and then looped through every declarator to update each child. Explicit resource management performed similar paired mutations for `using` declarations.
Keeping the kind only on `VariableDeclaration` removes this synchronization requirement and prevents consumers from observing conflicting declaration semantics.
## Declaration forms
The declaration kind now belongs exclusively to the node that owns the declaration list:
```js
let value;
```
```text
VariableDeclaration {
kind: Let,
declarations: [
VariableDeclarator { id: value, ... },
],
}
```
Multiple declarators share the same parent kind:
```js
const first = 1, second = 2;
```
```text
VariableDeclaration {
kind: Const,
declarations: [
VariableDeclarator { id: first, ... },
VariableDeclarator { id: second, ... },
],
}
```
## How to migrate
Code that previously read the kind from a declarator:
```rust
if declarator.kind.is_const() {
// ...
}
```
should now read it from the containing declaration:
```rust
if declaration.kind.is_const() {
// ...
}
```
Consumers operating through an AST node store should retrieve the parent `VariableDeclaration`:
```rust
let AstKind::VariableDeclaration(declaration) =
nodes.parent_kind(declarator.node_id())
else {
unreachable!();
};
let kind = declaration.kind;
```
Common migrations:
| Previous representation | New representation |
| --- | --- |
| `declarator.kind` | Containing `declaration.kind` |
| Match `VariableDeclarator::kind` during semantic binding | Read the parent `VariableDeclaration` from ancestry |
| Pass `kind` to `VariableDeclarator::new` | Remove the `kind` argument |
| Update parent and every child kind | Update only `VariableDeclaration::kind` |
| Recover kind after detaching a declarator | Capture or pass the parent kind before detaching it |
| Inspect a symbol’s declarator kind in lint rules | Resolve its parent declaration through the node store |
When constructing declarators, use the generated kindless `VariableDeclarator` builders and place them inside a `VariableDeclaration` carrying the required kind.
fixes https://github.com/oxc-project/backlog/issues/237
|
||
|
|
44fd320324 |
feat(ast)!: split TS external modules & Namespace Declarations (#25284)
## Summary
- Splits `TSModuleDeclaration` into two dedicated declaration nodes:
- `TSExternalModuleDeclaration` for string-literal modules and module augmentations.
- `TSNamespaceDeclaration` for identifier-based `module` and `namespace` declarations.
- Removes `TSModuleDeclaration`, `TSModuleDeclarationName`, `TSModuleDeclarationBody`, and `TSModuleDeclarationKind`.
- Makes invalid combinations of names, bodies, and declaration kinds unrepresentable.
- Ensures only namespace declarations introduce a normal binding.
- Updates all AST consumers, generators, traversal, formatting, code generation, semantic analysis, lint rules, transforms, and serialization.
- Keeps normal and eager-raw ESTree output compatible: both declarations remain `TSModuleDeclaration` nodes.
- Allows experimental lazy raw deserialization to expose the two new node types directly.
## Breaking Change
The Rust AST changes from:
```rust
pub enum Declaration<'a> {
VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
FunctionDeclaration(Box<'a, Function<'a>>) = 33,
ClassDeclaration(Box<'a, Class<'a>>) = 34,
TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 38,
TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 39,
TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 40,
}
pub struct TSModuleDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: TSModuleDeclarationName<'a>,
pub body: Option<TSModuleDeclarationBody<'a>>,
pub kind: TSModuleDeclarationKind,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
```
to:
```rust
pub enum Declaration<'a> {
VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
FunctionDeclaration(Box<'a, Function<'a>>) = 33,
ClassDeclaration(Box<'a, Class<'a>>) = 34,
TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
TSExternalModuleDeclaration(Box<'a, TSExternalModuleDeclaration<'a>>) = 38,
TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 39,
TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 40,
TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 41,
}
pub struct TSExternalModuleDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: StringLiteral<'a>,
pub body: Option<Box<'a, TSModuleBlock<'a>>>,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub struct TSNamespaceDeclaration<'a> {
pub node_id: Cell<NodeId>,
pub span: Span,
pub id: BindingIdentifier<'a>,
pub body: TSNamespaceDeclarationBody<'a>,
pub kind: TSNamespaceDeclarationKind,
pub declare: bool,
pub scope_id: Cell<Option<ScopeId>>,
}
pub enum TSNamespaceDeclarationBody<'a> {
TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 0,
TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
}
pub enum TSNamespaceDeclarationKind {
Module = 0,
Namespace = 1,
}
```
The corresponding inherited `Statement` variants, `AstKind` variants, builders, and visitor methods have also changed.
## Why?
`TSModuleDeclaration` previously represented two declaration families with different syntax and semantics:
```ts
declare module "foo" {}
declare module "*.css";
module Foo {}
namespace Foo {}
namespace Foo.Bar {}
```
The shared representation allowed invalid states such as:
- A namespace with a string-literal name.
- An external module with an identifier name.
- An identifier namespace without a body.
- A dotted external module.
- An external module containing another module declaration as its body.
- A declaration whose `kind` disagreed with its name.
Consumers also had to inspect `id` before determining whether the node introduced a binding or represented an external module.
The two declaration families have stronger independent invariants:
### External modules
- Always have a string-literal name.
- Never introduce a normal binding identifier.
- May omit their body.
- Cannot use dotted names.
- Use module-body import and export rules.
### Namespace declarations
- Always have a binding identifier.
- Always have a body.
- May contain nested namespace declarations for dotted names.
- Explicitly preserve the `module` or `namespace` spelling.
- Introduce a namespace/module symbol.
- Use namespace-body restrictions.
Dedicated nodes encode these differences directly.
## Declaration forms
External modules use `TSExternalModuleDeclaration`:
```ts
declare module "foo" {}
declare module "*.css";
module "foo" {}
```
Identifier-based declarations use `TSNamespaceDeclaration`:
```ts
module Foo {}
namespace Foo {}
```
Dotted namespaces are represented as nested namespace declarations:
```ts
namespace Foo.Bar {}
```
```rust
TSNamespaceDeclaration {
id: Foo,
body: TSNamespaceDeclarationBody::TSNamespaceDeclaration(
TSNamespaceDeclaration {
id: Bar,
body: TSNamespaceDeclarationBody::TSModuleBlock(...),
// ...
}
),
// ...
}
```
Only `TSExternalModuleDeclaration::body` is optional.
## ESTree compatibility
Normal serialization and eager raw deserialization continue to emit the existing ESTree shape:
```ts
interface TSModuleDeclaration {
type: "TSModuleDeclaration";
id: BindingIdentifier | StringLiteral | TSQualifiedName;
body: TSModuleBlock | null;
kind: "module" | "namespace";
declare: boolean;
global: false;
}
```
Dotted namespaces continue to serialize as a single `TSModuleDeclaration` with a `TSQualifiedName`.
Experimental lazy raw deserialization is not compatibility-preserving. It now exposes `TSExternalModuleDeclaration` and `TSNamespaceDeclaration` directly, with separate constructors, layouts, and visitor type IDs.
## How to migrate
Code that previously matched all forms through one variant:
```rust
if let Declaration::TSModuleDeclaration(declaration) = declaration {
// ...
}
```
should now match the required declaration family:
```rust
match declaration {
Declaration::TSExternalModuleDeclaration(declaration) => {
// String-literal external module
}
Declaration::TSNamespaceDeclaration(declaration) => {
// Identifier-based module or namespace
}
_ => {}
}
```
The same applies to `Statement` and `AstKind`:
```rust
Statement::TSExternalModuleDeclaration(declaration)
Statement::TSNamespaceDeclaration(declaration)
AstKind::TSExternalModuleDeclaration(declaration)
AstKind::TSNamespaceDeclaration(declaration)
```
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `TSModuleDeclarationName::StringLiteral(id)` | `TSExternalModuleDeclaration::id` |
| `TSModuleDeclarationName::Identifier(id)` | `TSNamespaceDeclaration::id` |
| `body: None` | Only valid on `TSExternalModuleDeclaration` |
| `TSModuleDeclarationBody::TSModuleBlock` | External `body` or `TSNamespaceDeclarationBody::TSModuleBlock` |
| `TSModuleDeclarationBody::TSModuleDeclaration` | `TSNamespaceDeclarationBody::TSNamespaceDeclaration` |
| `TSModuleDeclarationKind::Module` | External module, or `TSNamespaceDeclarationKind::Module` |
| `TSModuleDeclarationKind::Namespace` | `TSNamespaceDeclarationKind::Namespace` |
| Inspect `id` to determine binding behavior | Match `TSNamespaceDeclaration` |
| `declaration.id()` for string modules | Now returns `None` |
| `visit_ts_module_declaration` | `visit_ts_external_module_declaration` or `visit_ts_namespace_declaration` |
When constructing nodes, use the generated `TSExternalModuleDeclaration` or `TSNamespaceDeclaration` builders rather than recreating the previous shared representation.
fixes https://github.com/oxc-project/backlog/issues/218
|
||
|
|
0c9b980cb1 |
fix(semantic): report module declaration diagnostics (#25285)
## Summary - report TS1234 for function-local ambient module declarations - preserve TS1235 for function-local namespace and internal module declarations - report TS2435 for ambient modules nested in namespaces while allowing module augmentation inside ambient external modules ## Babel Conformance Snapshot Parse Failure This intentionally introduces one known Babel conformance false positive: `typescript/scope/redeclaration-in-nested-module/input.ts`. The Babel harness supplies `sourceType: "module"`, so Oxc reports TS2435 for its nested ambient module. This affects one unique fixture in both the parser and semantic Babel summaries. This change aligns with TS, and TS expects this error. |
||
|
|
45a17c25d1 |
release(crates): oxc v0.143.0 (#25250)
### 💥 BREAKING CHANGES - |
||
|
|
4120da068c |
perf(semantic)!: return borrowed JSDoc so the parse cache works (#25186)
Generated with Claude Code, Opus 5. Reviewed and tested by me to confirm
the finding here is correct and the fix works as intended.
## Problem
`JSDoc` parses lazily into a `OnceCell`, but `JSDocFinder` handed out
clones:
```rust
pub fn get_all_by_span<'b>(&'b self, span: Span) -> Option<Vec<JSDoc<'a>>> {
self.attached.get(&span.start).cloned()
}
```
Every caller parsed into its own clone and then dropped it, so the
`OnceCell` held in the map was never the one that got initialized. Each
subsequent lookup cloned that still-uninitialized cell and parsed again.
(`OnceCell::clone` does copy an initialized value — the problem is that
the cell in the map never reaches that state, because the write always
lands in a temporary.) A JSDoc comment was therefore parsed once per
*rule* that asked for it, rather than once overall.
Confirmed with a temporary counter in `parse_jsdoc`: **5 rule accesses
to a single JSDoc comment produced 5 calls to `parse_jsdoc`.**
## Fix
Return borrows (`Option<&[JSDoc]>` / `Option<&JSDoc>`) so the cache
lives in the map and is shared across callers. All 22 `jsdoc/*` rule
files were already iteration-based and needed no changes; the only
rule-side edits are 5 now-redundant borrows that clippy autofixed.
## Results
vscode `src` (8006 files, 862k lines, 36k JSDoc comments), `oxlint
--threads 1` with all 22 jsdoc
rules enabled:
| | mean |
|---|---|
| before | 767.4 ms |
| after | 761.6 ms |
**5.8 ms / 0.75%**, stable across independent 12-run and 40-run
hyperfine sets. Diagnostics are byte-identical: 13113 warnings before
and after.
The wall-clock win is small, but the change is algorithmically strictly
better with no downside: parses per comment go from O(rules that touch
it) to 1. A symbolicated profile puts ~67 ms (5.1% of worker time) in
JSDoc-containing stacks, of which ~12.5 ms is the parse itself — this PR
removes the redundant share of that.
## Notes
- **Breaking:** `get_one_by_node`, `get_all_by_node` and
`get_all_by_span` now return borrows instead of owned values.
- Sound with respect to threading: `iter_all()` already handed out
`&JSDoc`, so shared cells were not new; `OnceCell` makes `Semantic`
`!Sync`, so the compiler already forbids sharing `&Semantic` across
threads, and the newly-shared cell writes cannot race.
- Only affects runs with the `jsdoc` plugin explicitly enabled — it is
not in oxlint's default plugin set. The always-on `JSDocBuilder` path is
unaffected (I measured it separately at 690.0 ms -> 690.1 ms when
stubbed out entirely, i.e. free).
- We could remove the `#[derive(Clone)]` from JSDoc as nothing uses it
here anymore, but it'd also be a breaking change, so I didn't do it yet.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
|
||
|
|
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
|
||
|
|
04b854044f |
test(semantic): cover JSDoc re-exports (#25085)
## Summary
- add semantic JSDoc coverage for export { x } from "m"
|
||
|
|
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 - |
||
|
|
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 - |
||
|
|
47ddedf011 |
fix(semantic): allow arguments in ambient declarations (#24758)
## Summary - allow `eval` and `arguments` as direct bindings in ambient declarations - continue reporting strict-mode errors for destructured ambient bindings - add focused coverage for declared functions, classes, object patterns, and array patterns ## Why The semantic checker previously identified allowed bindings by walking to a `TSDeclareFunction`. That missed other ambient declarations, including declared class constructors and methods, and produced diagnostics for TypeScript cases that require no runtime code generation. The check now uses the existing ambient-context state and rejects only destructuring binding shapes. |
||
|
|
ddacf617f1 |
feat(semantic): add context notes for strict assignments (#24728)
## Summary
Adds context notes to `Cannot assign to '{x0}' in strict mode` diagnostics, explaining whether strict mode comes from an explicit directive, class body, or module.
|
||
|
|
76ec104498 |
fix(semantic): resolve import-equals namespaces (#22696)
Fixes `import =` semantic reference resolution so internal module references are resolved as namespace lookups instead of ordinary value reads. TypeScript treats the RHS of an internal `import =` specially. In the checker, `getTargetOfImportEqualsDeclaration` delegates internal module references to `getSymbolOfPartOfRightHandSideOfImportEquals`, which resolves bare RHS names and intermediate qualified names with `SymbolFlags.Namespace` rather than value meaning: - `getTargetOfImportEqualsDeclaration`: https://github.com/microsoft/TypeScript/blob/f350b52331494b68c90ab02e2b6d0828d2a22a74/src/compiler/checker.ts#L3708-L3733 - `getSymbolOfPartOfRightHandSideOfImportEquals`: https://github.com/microsoft/TypeScript/blob/f350b52331494b68c90ab02e2b6d0828d2a22a74/src/compiler/checker.ts#L4480-L4499 - `getSymbolOfNameOrPropertyAccessExpression` also routes names on the RHS of import/export assignments through that same helper: https://github.com/microsoft/TypeScript/blob/f350b52331494b68c90ab02e2b6d0828d2a22a74/src/compiler/checker.ts#L50014-L50019 Before this change, Oxc could count plain values as references from `import =`: ```ts const x = 1; import foo = x; ``` Before: ```text x references: 1 ``` After: ```text x references: 0 ``` That matches TypeScript’s behavior because `x` is only a value symbol, not a namespace-capable symbol. The same applies to qualified names: ```ts const x = { y: 1 }; import foo = x.y; ``` Before: ```text x references: 1 ``` After: ```text x references: 0 ``` But namespace-capable declarations still resolve: ```ts namespace x {} import foo = x; ``` After: ```text x references: 1 ``` and: ```ts namespace x { export namespace y {} } import foo = x.y; ``` After: ```text x references: 1 ``` This updates the semantic snapshots to reflect the corrected reference/declaration matching. |
||
|
|
cc3d17f063 |
feat(semantic): add context notes for reserved keyword diagnostic (#24715)
## Summary
Reserved-keyword diagnostics now explain the language context that makes an identifier invalid, rather than only reporting that the keyword is reserved.
## Details
- Add context-aware notes for strict mode, classes, and modules.
- Add a dedicated note for `await` in module code.
- Update parser-conformance snapshots for the expanded diagnostics.
Before:
```text
× The keyword 'package' is reserved
```
After, for strict-mode code:
```text
× The keyword 'package' is reserved
note: This identifier is reserved in strict mode code
```
After, for class code:
```js
class Example {
method() {
const static = 1;
}
}
```
```text
× The keyword 'static' is reserved
note: Classes are always strict mode code
```
After, for module code:
```js
export const await = 1;
```
```text
× The keyword 'await' is reserved
note: The identifier `await` is reserved in module code
|
||
|
|
b14d52e647 |
refactor(parser): move require after optional param check to parser (#24600)
## Summary - report TS1016 while parsing formal parameter lists - preserve initialized-parameter and rest-parameter behavior - remove the equivalent semantic check and diagnostic - update parser and semantic coverage snapshots Follows #24596, which has now merged. |
||
|
|
f01b460811 |
refactor(parser): move abstract private check to the parser (#24596)
## Summary - report TS18019 for abstract private methods during parsing - reuse the parser diagnostic already used for abstract private properties - remove the equivalent semantic check and diagnostic Stacked on #24595. |
||
|
|
7cd106daab |
refactor(parser): move abstract constructor check to the parser (#24595)
## Summary - report TS1242 for abstract constructors in the parser - remove the equivalent semantic diagnostic - update TypeScript parser coverage snapshots ## Why? This check needs to be moved to the parser, as the data is not retained after we change the Constructor ast type. |
||
|
|
ba65790029 |
perf(semantic, allocator): branchless clone_in for semantic IDs (#24564)
Follow-on after #24422. Make cloning semantic IDs (`NodeId`, `ScopeId` etc) use branchless arithmetic. * Cloning `NodeId` now just uses a single OR operation to take into account whether to preserve the existing ID (`clone_in_with_semantic_ids`) or substitute a dummy ID (`clone_in`). * Cloning `Cell<Option<ScopeId>>` is 2 instructions (NOT + AND) on x86_64, or just 1 on aarch64 (NOT-AND). https://godbolt.org/z/1KWdc5hnb The mechanism is a type `CloneInSemanticIds` which is used as `with_semantic_ids` instead of a `bool`: ```rs #[repr(u32)] pub enum CloneInSemanticIds { With = 0, Without = u32::MAX, } ``` This is 4 bytes, rather than `bool`'s 1 byte, but that makes no difference - either way it takes 1 register to pass between functions. The choice of discriminants is what enables the cheap arithmetic. This is even smaller in terms of binary size than the previous solution of marking `Cell::clone_in_impl` as `#[inline(never)]` (OR instruction is smaller than function call), and it's more performant as it loses the function call overhead. It also keeps `Cell`'s blanket implementation of `CloneIn` generalized. Between #24422 and this PR, merging `CloneIn` with/without semantic IDs into a single path has gained us a large binary size reduction, and lost us nothing - or may even have gained us a marginal perf improvement. No visible change on our benchmarks, as we don't use `CloneIn` much, but it may well be a measurable improvement for Rolldown, which clones whole ASTs. |
||
|
|
589415c2b8 |
chore(semantic): remove stale hash set TODO (#24572)
## Summary - remove the stale TODO about revisiting strict-mode identifier matching - retain the existing direct match for the ten reserved identifier names ## Rationale The prerequisites mentioned by the TODO have landed: identifier names now use `Ident` and carry precomputed hashes. I compared the current match with an `IdentHashSet` implementation that accepts `Ident` directly. On ARM64, the match compiles to a string-length decision tree with inline byte/word comparisons. The hash-set version uses the precomputed hash and hashbrown's SwissTable probe, but also adds `LazyLock` access, a larger stack frame (368 bytes versus 272 bytes), and substantially more register saves in `check_identifier`. ### Benchmark methodology To avoid sequential-baseline and thermal-order artifacts, I built and preserved separate optimized binaries for the match and hash-set implementations, restored the worktree to the match, and ran alternating ABBA comparisons. Each run used 100 samples, a 5-second warmup, 15-20 seconds of measurement, and a 1% Criterion noise threshold. The benchmark inputs are especially favorable to the hash set: they contained 39 to 25,013 checked identifiers and effectively 100% of the lookups missed the reserved-name set. Results: - `RadixUIAdoptionSection.jsx` (39 identifiers): hash set 2.08% slower in the forward comparison; match 1.82% faster in the reverse comparison. - `kitchen-sink.tsx` (25,013 identifiers): hash set 0.81% faster forward and 0.39% faster in the reverse comparison; both were within the 1% noise threshold. - `binder.ts` (5,861 identifiers): hash set 0.88% slower forward; match 0.33% faster in reverse, but the reverse confidence interval crossed zero. The original 2% regression therefore reproduces when benchmark order is reversed. The hash set can show a sub-1% gain on the largest miss-heavy input, but it does not establish a general end-to-end improvement and adds a reproducible small-input regression plus more initialization and generated-code complexity. The direct match remains preferable, and the TODO no longer has a useful follow-up action. |
||
|
|
f297654a68 |
refactor(allocator, ast_tools): generate a single CloneIn traversal via a runtime flag (#24422)
## Summary
The `CloneIn` derive generated **two near-identical traversals over the whole AST** — `clone_in` (resets semantic ids to their default) and `clone_in_with_semantic_ids` (preserves them). Almost every node carries an id field, so the two bodies diverge nearly everywhere and the pair roughly doubled the clone codegen.
This threads *"preserve semantic ids?"* as a **runtime `bool`** through a **single** traversal:
- **`CloneIn::clone_in_impl(&self, with_semantic_ids: bool, allocator)` is the one required method.** `clone_in` / `clone_in_with_semantic_ids` are `#[inline(always)]` default wrappers that call it with `false` / `true`. Hand-written impls provide only `clone_in_impl`.
- The **derive emits a single `clone_in_impl` body per type** — no per-type `clone_in` / `clone_in_with_semantic_ids` wrappers.
- Semantic ids live in `Cell<NodeId>` / `Cell<Option<{Scope,Symbol,Reference}Id>>`. **`Cell<T: Copy + Default>::clone_in_impl` threads the flag**: preserve (`Cell::new(self.get())`) when `with_semantic_ids`, otherwise reset to the default (`Cell::new(T::default())` = `NodeId::DUMMY` / `None`). So the derive emits a plain `clone_in_impl(&self.node_id, …)` per id field — no per-field `if/else`.
- **`Cell::clone_in_impl` is `#[inline(never)]`** so the (for some consumers dead) preserve branch isn't inlined at every id-field site — see Results.
## Results
Measured `f736d87e77` → `db9f2ab41a` on this branch (release, stripped, fat-LTO):
| | before | after | Δ |
|---|--:|--:|--:|
| `oxc_ast` generated `CloneIn` (source lines) | 7,180 | 5,581 | **−1,599 (−22%)** |
| oxlint binary | 14,192,816 B | 14,126,656 B | **−66,160 B (−64.6 KiB)** |
| napi transform cdylib (`--features allocator`) | 3,633,312 B | 3,633,312 B | **0 (neutral)** |
The two binaries move differently because they clone differently:
- **oxlint / rolldown clone *with* semantic ids** (react compiler on `TSType`; whole-`Program` clones). They already carried chunks of both traversals; merging dedups them → **−64.6 KiB**.
- **napi transform / parse clone *without* ids** — `oxc_transformer` never calls `clone_in_with_semantic_ids`, so before this PR `--gc-sections` dropped that whole half. The merged traversal would force the preserve branch live at every id field (~+16 KiB); `#[inline(never)]` on the `Cell` leaf outlines it to one function per `Cell<T>`, keeping these binaries **flat**.
## Behavior & perf
Behavior is identical by construction — `clone_in` resets ids (`Cell::new(None)` / `Cell::new(DUMMY)`), `clone_in_with_semantic_ids` preserves them. The runtime flag is loop-invariant and well predicted; the `#[inline(never)]` leaf adds one outlined call per id-cell on the clone path (expected negligible — worth a benchmark spot-check). Generated files were produced by `just ast`.
---
🤖 AI-assisted (Claude Code), reviewed and measured by the author.
|