fix(formatter): break mapped type brackets (#25297)

Follow up #25296, same cases but for mapped type.
This commit is contained in:
leaysgur committed 2026-08-05 04:25:24 +00:00
1 parent 4e6f3f1356
commit ef1d04be86
6 files changed
+235 -26

No files matched your search

+53 -23
View File
@@ -2,7 +2,11 @@ use oxc_ast::ast::{TSMappedType, TSMappedTypeModifierOperator};
use crate::{
ast_nodes::AstNode,
formatter::{prelude::*, trivia::FormatLeadingComments},
format_args,
formatter::{
prelude::*,
trivia::{DanglingIndentMode, FormatDanglingComments},
},
print::semicolon::OptionalSemicolon,
utils::suppressed::FormatSuppressedNode,
write,
@@ -32,13 +36,42 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSMappedType<'a>> {
f.source_text().has_line_terminator_after_skipping_comments(self.span.start + 1);
let format_inner = format_with(|f| {
if should_expand {
let comments = if f.comments().has_leading_own_line_comment(self.key.span.start) {
f.context().comments().comments_before(self.key.span.start)
// Comments between `{` and `[` are the mapped type's dangling comments, joined by hard line breaks.
// After the last one: a hard line break when it is a line comment or the source breaks after it,
// otherwise a group collapsing to a space when the member fits on the line.
// Only the last comment goes inside that group: the ones before it always hard-break.
let comments = f.context().comments().comments_before_character(self.span.start, b'[');
if let Some((last, rest)) = comments.split_last() {
if last.is_line() || last.followed_by_newline() {
write!(
f,
[
FormatDanglingComments::Comments {
comments,
indent: DanglingIndentMode::None
},
hard_line_break()
]
);
} else {
f.context().comments().comments_before_character(self.span.start, b'[')
};
write!(f, FormatLeadingComments::Comments(comments));
write!(
f,
[
FormatDanglingComments::Comments {
comments: rest,
indent: DanglingIndentMode::None
},
(!rest.is_empty()).then_some(hard_line_break()),
group(&format_args!(
FormatDanglingComments::Comments {
comments: std::slice::from_ref(last),
indent: DanglingIndentMode::None
},
soft_line_break_or_space()
))
]
);
}
}
if let Some(readonly) = self.readonly() {
@@ -50,28 +83,25 @@ impl<'a> FormatWrite<'a> for AstNode<'a, TSMappedType<'a>> {
write!(f, [prefix, "readonly", space()]);
}
let format_inner_inner = format_with(|f| {
write!(f, "[");
write!(f, key);
write!(f, [space(), "in", space(), constraint]);
let format_key_in_constraint = format_with(|f| {
write!(f, [key, space(), "in", space(), constraint]);
if let Some(name_type) = &name_type {
write!(f, [space(), "as", space(), name_type]);
}
key.format_trailing_comments(f);
write!(f, "]");
if let Some(optional) = self.optional() {
write!(
f,
match optional {
TSMappedTypeModifierOperator::True => "?",
TSMappedTypeModifierOperator::Plus => "+?",
TSMappedTypeModifierOperator::Minus => "-?",
}
);
}
});
write!(f, [group(&format_inner_inner)]);
write!(f, ["[", group(&soft_block_indent(&format_key_in_constraint)), "]"]);
if let Some(optional) = self.optional() {
write!(
f,
match optional {
TSMappedTypeModifierOperator::True => "?",
TSMappedTypeModifierOperator::Plus => "+?",
TSMappedTypeModifierOperator::Minus => "-?",
}
);
}
if let Some(type_annotation) = &self.type_annotation() {
write!(f, [":", space(), type_annotation]);
}
@@ -4,3 +4,26 @@ type _Test = {
// BinaryOperatorToText, then this line will error with "Type 'T' cannot
// be used to index type 'BinaryOperatorToText'."
};
// issue #25286 (sibling): a leading comment on the key breaks the bracket
// group and stays inside the brackets, above the key
type _Key = {
[
// comment on the key
K in keyof T
]: T[K];
};
// dangling comments hoist before `readonly` (matches Prettier, which treats
// everything between `{` and `[` as dangling on the mapped type)
type _Readonly = { readonly /* c */ [K in T]: X };
// adjacent multiline JSDoc comments stay nestled, like Prettier
type _Nestled = {
/**
* a
*//**
* b
*/
[K in T]: X;
};
@@ -9,6 +9,29 @@ type _Test = {
// be used to index type 'BinaryOperatorToText'."
};
// issue #25286 (sibling): a leading comment on the key breaks the bracket
// group and stays inside the brackets, above the key
type _Key = {
[
// comment on the key
K in keyof T
]: T[K];
};
// dangling comments hoist before `readonly` (matches Prettier, which treats
// everything between `{` and `[` as dangling on the mapped type)
type _Readonly = { readonly /* c */ [K in T]: X };
// adjacent multiline JSDoc comments stay nestled, like Prettier
type _Nestled = {
/**
* a
*//**
* b
*/
[K in T]: X;
};
==================== Output ====================
------------------
{ printWidth: 80 }
@@ -20,6 +43,29 @@ type _Test = {
// be used to index type 'BinaryOperatorToText'."
};
// issue #25286 (sibling): a leading comment on the key breaks the bracket
// group and stays inside the brackets, above the key
type _Key = {
[
// comment on the key
K in keyof T
]: T[K];
};
// dangling comments hoist before `readonly` (matches Prettier, which treats
// everything between `{` and `[` as dangling on the mapped type)
type _Readonly = { /* c */ readonly [K in T]: X };
// adjacent multiline JSDoc comments stay nestled, like Prettier
type _Nestled = {
/**
* a
*//**
* b
*/
[K in T]: X;
};
-------------------
{ printWidth: 100 }
-------------------
@@ -30,4 +76,27 @@ type _Test = {
// be used to index type 'BinaryOperatorToText'."
};
// issue #25286 (sibling): a leading comment on the key breaks the bracket
// group and stays inside the brackets, above the key
type _Key = {
[
// comment on the key
K in keyof T
]: T[K];
};
// dangling comments hoist before `readonly` (matches Prettier, which treats
// everything between `{` and `[` as dangling on the mapped type)
type _Readonly = { /* c */ readonly [K in T]: X };
// adjacent multiline JSDoc comments stay nestled, like Prettier
type _Nestled = {
/**
* a
*//**
* b
*/
[K in T]: X;
};
===================== End =====================
@@ -0,0 +1,9 @@
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<KeyNameHere & string>]: X;
};
type B = {
+readonly [VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere]-?: SomeValue;
};
type C = { [K in keyof T]: T[K] };
@@ -0,0 +1,80 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<KeyNameHere & string>]: X;
};
type B = {
+readonly [VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere]-?: SomeValue;
};
type C = { [K in keyof T]: T[K] };
==================== Output ====================
----------------------------------------
{ bracketSpacing: true, printWidth: 80 }
----------------------------------------
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[
KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<
KeyNameHere & string
>
]: X;
};
type B = {
+readonly [
VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere
]-?: SomeValue;
};
type C = { [K in keyof T]: T[K] };
-----------------------------------------
{ bracketSpacing: true, printWidth: 100 }
-----------------------------------------
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<KeyNameHere & string>]: X;
};
type B = {
+readonly [VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere]-?: SomeValue;
};
type C = { [K in keyof T]: T[K] };
-----------------------------------------
{ bracketSpacing: false, printWidth: 80 }
-----------------------------------------
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[
KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<
KeyNameHere & string
>
]: X;
};
type B = {
+readonly [
VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere
]-?: SomeValue;
};
type C = {[K in keyof T]: T[K]};
------------------------------------------
{ bracketSpacing: false, printWidth: 100 }
------------------------------------------
// issue #25286 (sibling): the mapped type key gets its own bracket group,
// so an overlong key breaks after `[` instead of overflowing printWidth
type A = {
[KeyNameHere in keyof SomeVeryLongGenericTypeNameHere as Uppercase<KeyNameHere & string>]: X;
};
type B = {
+readonly [VeryLongKeyName in keyof SomeExtremelyLongSourceTypeNameGoesHere]-?: SomeValue;
};
type C = {[K in keyof T]: T[K]};
===================== End =====================
@@ -1,4 +1,4 @@
ts compatibility: 622/659 (94.39%), 15 files skipped
ts compatibility: 624/659 (94.69%), 15 files skipped
# Failed
@@ -9,7 +9,6 @@ ts compatibility: 622/659 (94.39%), 15 files skipped
| typescript/as/comments/17407.ts | 💥 | 47.06% |
| typescript/call/callee-comments.ts | 💥 | 75.00% |
| typescript/cast/18406.ts | 💥 | 84.21% |
| typescript/comments/mapped-types.ts | 💥 | 85.71% |
| typescript/comments/method_types.ts | 💥 | 82.05% |
| typescript/comments/type-parameters.ts | 💥 | 81.82% |
| typescript/comments/first-argument/first-argument.ts | 💥 | 81.16% |
@@ -23,7 +22,6 @@ ts compatibility: 622/659 (94.39%), 15 files skipped
| typescript/property-signature/consistent-with-flow/comments.ts | 💥 | 80.00% |
| typescript/property-signature/consistent-with-flow/union.ts | 💥 | 80.00% |
| typescript/template-literals/member-expression.ts | 💥 | 65.12% |
| typescript/type-parameters-arguments/10732.ts | 💥 | 66.67% |
| typescript/type-parameters-arguments/18041.ts | 💥 | 91.43% |
| typescript/type-parameters-arguments/18604.ts | 💥 | 60.00% |
| typescript/type-parameters-arguments/19505.ts | 💥 | 61.54% |