fix(estree): emit decorators on FormalParameterRest (#25582)

This PR adds support for emitting `decorators` when serializing an ast to ESTree (both with/without raw transfer).

Given the following code:
```
class C { method(@dec ...args) {} }
```

Before we emitted the following ESTree shape:
```json
"params": [
  {
    "type": "RestElement",
    "decorators": [],
    "argument": {
      "type": "Identifier",
      "decorators": [],
      "name": "args",
      "optional": false,
      "typeAnnotation": null,
    },
    "optional": false,
    "typeAnnotation": null,
    "value": null,
  }
],
```

Now we emit:
```json
"params": [
  {
    "type": "RestElement",
    "decorators": [
      {
        "type": "Decorator",
        "expression": { "type": "Identifier", "name": "dec" }
      }
    ],
    "argument": {
      "type": "Identifier",
      "decorators": [],
      "name": "args",
      "optional": false,
      "typeAnnotation": null,
    },
    "optional": false,
    "typeAnnotation": null,
    "value": null,
  },
],
```

fixes https://github.com/oxc-project/oxc/issues/18981
closes https://github.com/oxc-project/oxc/pull/24736
This commit is contained in:
camc314 committed 2026-08-13 10:43:14 +00:00
1 parent 6e040e494e
commit 0c68b7ff8b
11 files changed
+12 -15

No files matched your search

@@ -2388,6 +2388,7 @@ function deserializeFormalParameters(pos) {
parent: previousParent,
});
rest.argument = deserializeBindingPattern(pos + 56);
rest.decorators = deserializeVecDecorator(pos + 16);
rest.typeAnnotation = deserializeOptionBoxTSTypeAnnotation(pos + 72);
if (rest.typeAnnotation !== null) {
end = rest.typeAnnotation.end;
+1 -1
View File
@@ -673,7 +673,7 @@ export type FunctionType =
export interface FormalParameterRest extends Span {
type: "RestElement";
argument: BindingPattern;
decorators?: [];
decorators?: Array<Decorator>;
optional?: boolean;
typeAnnotation?: TSTypeAnnotation | null;
value?: null;
+1 -1
View File
@@ -1869,7 +1869,7 @@ pub enum FunctionType {
interface FormalParameterRest extends Span {
type: 'RestElement';
argument: BindingPattern;
decorators?: [],
decorators?: Array<Decorator>;
optional?: boolean;
typeAnnotation?: TSTypeAnnotation | null;
value?: null;
+2 -1
View File
@@ -379,6 +379,7 @@ impl ESTree for CatchParameterConverter<'_, '_> {
};
rest.argument = DESER[BindingPattern]( POS_OFFSET<FormalParameterRest>.rest.argument );
if (IS_TS) {
rest.decorators = DESER[Vec<Decorator>](POS_OFFSET<FormalParameterRest>.decorators);
rest.typeAnnotation = DESER[Option<Box<TSTypeAnnotation>>](
POS_OFFSET<FormalParameterRest>.type_annotation
);
@@ -418,7 +419,7 @@ impl ESTree for FormalParameterRest<'_> {
let rest = self;
let mut state = serializer.serialize_struct();
state.serialize_field("type", &JsonSafeString("RestElement"));
state.serialize_ts_field("decorators", &EmptyArray(()));
state.serialize_ts_field("decorators", &rest.decorators);
state.serialize_field("argument", &rest.rest.argument);
state.serialize_ts_field("optional", &false);
state.serialize_ts_field("typeAnnotation", &rest.type_annotation);
@@ -1917,6 +1917,7 @@ function deserializeFormalParameters(pos) {
end: (end = deserializeI32(pos + 44)),
};
rest.argument = deserializeBindingPattern(pos + 56);
rest.decorators = deserializeVecDecorator(pos + 16);
rest.typeAnnotation = deserializeOptionBoxTSTypeAnnotation(pos + 72);
if (rest.typeAnnotation !== null) {
end = rest.typeAnnotation.end;
@@ -2121,6 +2121,7 @@ function deserializeFormalParameters(pos) {
parent: previousParent,
});
rest.argument = deserializeBindingPattern(pos + 56);
rest.decorators = deserializeVecDecorator(pos + 16);
rest.typeAnnotation = deserializeOptionBoxTSTypeAnnotation(pos + 72);
if (rest.typeAnnotation !== null) {
end = rest.typeAnnotation.end;
@@ -2124,6 +2124,7 @@ function deserializeFormalParameters(pos) {
range: [start, end],
};
rest.argument = deserializeBindingPattern(pos + 56);
rest.decorators = deserializeVecDecorator(pos + 16);
rest.typeAnnotation = deserializeOptionBoxTSTypeAnnotation(pos + 72);
if (rest.typeAnnotation !== null) {
end = rest.typeAnnotation.end;
@@ -2327,6 +2327,7 @@ function deserializeFormalParameters(pos) {
parent: previousParent,
});
rest.argument = deserializeBindingPattern(pos + 56);
rest.decorators = deserializeVecDecorator(pos + 16);
rest.typeAnnotation = deserializeOptionBoxTSTypeAnnotation(pos + 72);
if (rest.typeAnnotation !== null) {
end = rest.typeAnnotation.end;
+1 -1
View File
@@ -666,7 +666,7 @@ export type FunctionType =
export interface FormalParameterRest extends Span {
type: "RestElement";
argument: BindingPattern;
decorators?: [];
decorators?: Array<Decorator>;
optional?: boolean;
typeAnnotation?: TSTypeAnnotation | null;
value?: null;
-1
View File
@@ -564,7 +564,6 @@ so that arm can run.
- `import "m";` versus `import {} from "m";`
- An absent `with` clause versus an empty `with {}`
- `assert {...}` versus `with {...}`
- A rest parameter's decorators (a deficiency in `oxc-parser`, fixable, not blocked by ESTree spec)
The conformance harness normalizes the Rust AST down to what ESTree can express before printing,
rather than expecting the JS side to reproduce information it was never given.
+2 -10
View File
@@ -15,8 +15,8 @@ use napi_derive::napi;
use oxc_allocator::Allocator;
use oxc_ast::ast::{
ArrowFunctionExpression, ExportAllDeclaration, ExportFromDeclaration, FormalParameterRest,
Function, ImportDeclaration, WithClauseKeyword,
ArrowFunctionExpression, ExportAllDeclaration, ExportFromDeclaration, Function,
ImportDeclaration, WithClauseKeyword,
};
use oxc_ast_visit::{VisitMut, walk_mut};
use oxc_codegen::{Codegen, CodegenOptions, CommentOptions};
@@ -146,14 +146,6 @@ impl<'a> VisitMut<'a> for Normalize {
walk_mut::walk_arrow_function_expression(self, arrow);
}
/// A rest parameter's decorators are `#[estree(skip)]`ped - ESTree types them as
/// an always-empty array on `RestElement` - so the JS side never sees them.
/// Decorators on the other parameters do come through, and are left alone.
fn visit_formal_parameter_rest(&mut self, rest: &mut FormalParameterRest<'a>) {
rest.decorators.clear();
walk_mut::walk_formal_parameter_rest(self, rest);
}
/// ESTree cannot distinguish `import "m"` from `import {} from "m"` - `specifiers` is `[]` for both.
/// It also cannot distinguish an attribute-less import from one with an empty `with {}` clause -
/// `attributes` is `[]` for both.