fix(formatter): print decorators on rest parameters (#28731)

oxfmt deletes a decorator on a rest parameter. This happens with the
default settings, and oxfmt reports no error.

```ts
class C {
  m(@Dec() ...args: string[]) {}
}

// before
class C {
  m(...args: string[]) {}
}
```

The parser and codegen support these decorators (#18938, #20132), but
the formatter printed only the rest element and its type annotation.
This PR prints the decorators in the same way as for `FormalParameter`.

The new fixture `ts/parameters/rest-decorators.ts` covers single and
multiple decorators, `@(await p)`, a decorator after another parameter,
and long parameter lists. The output matches Prettier 3.9.10 for every
case.

---

AI assistance was used for this change. I reviewed and tested it.

---------

Co-authored-by: leaysgur <6259812+leaysgur@users.noreply.github.com>
This commit is contained in:
YTandleaysgur authored and GitHub committed 2026-11-04 02:08:56 +00:00
1 parent 8432de6b27
commit b8ba9ae45f
4 files changed
+154 -3

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ impl<'a> Format<'a, JsFormatContext<'a>> for AstNode<'_, 'a, ArenaVec<'a, Decora
);
}
// Parameter decorators
AstNodes::FormalParameter(_) => {
AstNodes::FormalParameter(_) | AstNodes::FormalParameterRest(_) => {
write!(f, should_expand_decorators(self, f).then_some(expand_parent()));
}
AstNodes::ExportDeclaration(_) | AstNodes::ExportDefaultDeclaration(_) => {
+11 -2
View File
@@ -1103,8 +1103,17 @@ impl<'a> FormatWrite<'a> for AstNode<'_, 'a, BindingPattern<'a>> {
impl<'a> FormatWrite<'a> for AstNode<'_, 'a, FormalParameterRest<'a>> {
fn write(&self, f: &mut JsFormatter<'_, 'a>) {
write!(f, [self.rest()]);
write!(f, self.type_annotation());
let content = format_with(|f| {
write!(f, [self.rest()]);
write!(f, self.type_annotation());
});
// Decorators are laid out as on `FormalParameter`, an undecorated rest parameter stays ungrouped
let decorators = self.decorators();
if decorators.is_empty() {
write!(f, [content]);
} else {
write!(f, [group(&decorators), group(&content)]);
}
}
}
@@ -0,0 +1,20 @@
// Decorators on a rest parameter are printed like those on other parameters
class C { m(@d ...args: unknown[]) {} }
class C2 { constructor(@Inject(TOKEN) ...deps: any[]) {} }
class C3 { m(@d x, @e ...rest) {} }
class C4 { async rest(@(await p) ...args: unknown[]) {} }
class C5 { m(@d @e ...args) {} }
class C6 {
constructor(
@Inject(VERY_LONG_TOKEN_NAME_FOR_DEPENDENCY_INJECTION) private readonly a: A,
@Inject(ANOTHER_VERY_LONG_TOKEN_NAME) ...rest: unknown[]
) {}
}
class C7 { m(@d
...args) {} }
// Comments around the decorator keep their place
class C8 { m(@d /* c */ ...args) {} }
class C9 { m(@d // c
...args) {} }
class C10 { m(@d ...args // c
) {} }
@@ -0,0 +1,122 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// Decorators on a rest parameter are printed like those on other parameters
class C { m(@d ...args: unknown[]) {} }
class C2 { constructor(@Inject(TOKEN) ...deps: any[]) {} }
class C3 { m(@d x, @e ...rest) {} }
class C4 { async rest(@(await p) ...args: unknown[]) {} }
class C5 { m(@d @e ...args) {} }
class C6 {
constructor(
@Inject(VERY_LONG_TOKEN_NAME_FOR_DEPENDENCY_INJECTION) private readonly a: A,
@Inject(ANOTHER_VERY_LONG_TOKEN_NAME) ...rest: unknown[]
) {}
}
class C7 { m(@d
...args) {} }
// Comments around the decorator keep their place
class C8 { m(@d /* c */ ...args) {} }
class C9 { m(@d // c
...args) {} }
class C10 { m(@d ...args // c
) {} }
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// Decorators on a rest parameter are printed like those on other parameters
class C {
m(@d ...args: unknown[]) {}
}
class C2 {
constructor(@Inject(TOKEN) ...deps: any[]) {}
}
class C3 {
m(@d x, @e ...rest) {}
}
class C4 {
async rest(@(await p) ...args: unknown[]) {}
}
class C5 {
m(@d @e ...args) {}
}
class C6 {
constructor(
@Inject(VERY_LONG_TOKEN_NAME_FOR_DEPENDENCY_INJECTION)
private readonly a: A,
@Inject(ANOTHER_VERY_LONG_TOKEN_NAME) ...rest: unknown[]
) {}
}
class C7 {
m(
@d
...args
) {}
}
// Comments around the decorator keep their place
class C8 {
m(@d /* c */ ...args) {}
}
class C9 {
m(
@d // c
...args
) {}
}
class C10 {
m(
@d ...args // c
) {}
}
-------------------
{ printWidth: 100 }
-------------------
// Decorators on a rest parameter are printed like those on other parameters
class C {
m(@d ...args: unknown[]) {}
}
class C2 {
constructor(@Inject(TOKEN) ...deps: any[]) {}
}
class C3 {
m(@d x, @e ...rest) {}
}
class C4 {
async rest(@(await p) ...args: unknown[]) {}
}
class C5 {
m(@d @e ...args) {}
}
class C6 {
constructor(
@Inject(VERY_LONG_TOKEN_NAME_FOR_DEPENDENCY_INJECTION) private readonly a: A,
@Inject(ANOTHER_VERY_LONG_TOKEN_NAME) ...rest: unknown[]
) {}
}
class C7 {
m(
@d
...args
) {}
}
// Comments around the decorator keep their place
class C8 {
m(@d /* c */ ...args) {}
}
class C9 {
m(
@d // c
...args
) {}
}
class C10 {
m(
@d ...args // c
) {}
}
===================== End =====================