fix(formatter): keep comments after TS this_param (#25459)

Fixes #25410

Syntactically, this `this` is written inside the function parameters.
However, in the AST, `this_param` and `params` are adjacent fields, and since the inclusion relationship of the spans is broken, exception handling is required.
This commit is contained in:
leaysgur committed 2026-08-10 02:52:32 +00:00
1 parent e3683f29d8
commit b63ecccb1f
3 files changed
+171 -7

No files matched your search

+32 -7
View File
@@ -8,7 +8,7 @@ use crate::{
formatter::{
JsFormatter,
prelude::*,
trivia::{FormatLeadingComments, FormatTrailingComments},
trivia::{FormatLeadingComments, FormatTrailingComments, format_trailing_comments},
},
options::{FormatTrailingCommas, TrailingSeparator},
utils::call_expression::{is_angular_test_wrapper, is_test_call_expression},
@@ -17,6 +17,12 @@ use crate::{
use super::FormatWrite;
/// NOTE: In the AST, `this_param` is a sibling field of `FormalParameters`,
/// but the `FormalParameters` span covers the whole parens INCLUDING `this`.
///
/// Span windows assuming source-ordered, non-overlapping fields are inverted here:
/// the generated `following_span_start` for `this_param` (= params span start) points before it,
/// so the generated trailing-comment pass never captures anything (see [`Parameter::This`]).
pub fn get_this_param<'a>(parent: &AstNodes<'a>) -> Option<&'a AstNode<'a, TSThisParameter<'a>>> {
match parent {
AstNodes::Function(func) => func.this_param(),
@@ -167,7 +173,10 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSThisParameter<'a>> {
}
enum Parameter<'a, 'b> {
This(&'b AstNode<'a, TSThisParameter<'a>>),
This {
param: &'b AstNode<'a, TSThisParameter<'a>>,
list: &'b AstNode<'a, FormalParameters<'a>>,
},
Formal(&'b AstNode<'a, FormalParameter<'a>>),
Rest(&'b AstNode<'a, FormalParameterRest<'a>>),
}
@@ -175,7 +184,7 @@ enum Parameter<'a, 'b> {
impl GetSpan for Parameter<'_, '_> {
fn span(&self) -> Span {
match self {
Self::This(param) => param.span(),
Self::This { param, .. } => param.span(),
Self::Formal(param) => param.span(),
Self::Rest(e) => e.span(),
}
@@ -185,7 +194,19 @@ impl GetSpan for Parameter<'_, '_> {
impl<'a> Format<'a, JsFormatContext<'a>> for Parameter<'a, '_> {
fn fmt(&self, f: &mut JsFormatter<'_, 'a>) {
match self {
Self::This(param) => param.fmt(f),
Self::This { param, list } => {
param.fmt(f);
// The generated trailing pass captures nothing here (inverted window, see `get_this_param`).
// Capture like any other parameter: up to the next one, or dangling inside the parens when `this` is last
// (`AST_NODE_WITHOUT_FOLLOWING_NODE_LIST` rule in `ast_nodes.rs` generator gives the list's own last child).
let following_span_start = list
.items
.first()
.map(|p| p.span.start)
.or_else(|| list.rest.as_deref().map(|r| r.span.start))
.unwrap_or(0);
format_trailing_comments(list.span(), param.span(), following_span_start).fmt(f);
}
Self::Formal(param) => param.fmt(f),
Self::Rest(e) => e.fmt(f),
}
@@ -193,14 +214,18 @@ impl<'a> Format<'a, JsFormatContext<'a>> for Parameter<'a, '_> {
}
struct FormalParametersIter<'a, 'b> {
this: Option<&'b AstNode<'a, TSThisParameter<'a>>>,
this: Option<Parameter<'a, 'b>>,
params: AstNodeIterator<'a, FormalParameter<'a>>,
rest: Option<&'b AstNode<'a, FormalParameterRest<'a>>>,
}
impl<'a, 'b> From<&'b ParameterList<'a, 'b>> for FormalParametersIter<'a, 'b> {
fn from(value: &'b ParameterList<'a, 'b>) -> Self {
Self { this: value.this, params: value.list.items().iter(), rest: value.list.rest() }
Self {
this: value.this.map(|param| Parameter::This { param, list: value.list }),
params: value.list.items().iter(),
rest: value.list.rest(),
}
}
}
@@ -208,7 +233,7 @@ impl<'a, 'b> Iterator for FormalParametersIter<'a, 'b> {
type Item = Parameter<'a, 'b>;
fn next(&mut self) -> Option<Self::Item> {
self.this.take().map(Parameter::This).or_else(|| {
self.this.take().or_else(|| {
self.params
.next()
.map(Parameter::Formal)
@@ -0,0 +1,31 @@
// Comments after a `this` parameter stay inside the parens (issue #25410)
interface X {
ownLine<T>(
this: void
// own-line comment
): void;
sameLine(
this: void // same-line comment
): void;
block(
this: void
/* block comment */
): void;
beforeNext(
this: void, // trailing this
a: string
): void;
}
type F = (
this: void
// fn type
) => void;
declare function g(
this: void,
...rest: number[]
// after rest
): void;
function h(
this: void
// own line
) {}
@@ -0,0 +1,108 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// Comments after a `this` parameter stay inside the parens (issue #25410)
interface X {
ownLine<T>(
this: void
// own-line comment
): void;
sameLine(
this: void // same-line comment
): void;
block(
this: void
/* block comment */
): void;
beforeNext(
this: void, // trailing this
a: string
): void;
}
type F = (
this: void
// fn type
) => void;
declare function g(
this: void,
...rest: number[]
// after rest
): void;
function h(
this: void
// own line
) {}
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// Comments after a `this` parameter stay inside the parens (issue #25410)
interface X {
ownLine<T>(
this: void,
// own-line comment
): void;
sameLine(
this: void, // same-line comment
): void;
block(
this: void,
/* block comment */
): void;
beforeNext(
this: void, // trailing this
a: string,
): void;
}
type F = (
this: void,
// fn type
) => void;
declare function g(
this: void,
...rest: number[]
// after rest
): void;
function h(
this: void,
// own line
) {}
-------------------
{ printWidth: 100 }
-------------------
// Comments after a `this` parameter stay inside the parens (issue #25410)
interface X {
ownLine<T>(
this: void,
// own-line comment
): void;
sameLine(
this: void, // same-line comment
): void;
block(
this: void,
/* block comment */
): void;
beforeNext(
this: void, // trailing this
a: string,
): void;
}
type F = (
this: void,
// fn type
) => void;
declare function g(
this: void,
...rest: number[]
// after rest
): void;
function h(
this: void,
// own line
) {}
===================== End =====================