fix(formatter): keep parens around TSInstantiationExpression before ! and export (#27626)

Should keep parens around `(make<T>)!()`, otherwise parse error.
This commit is contained in:
leaysgur committed 2026-10-13 03:45:18 +00:00
1 parent 82ecc43053
commit 21dee96d67
5 files changed
+109 -25

No files matched your search

+26
View File
@@ -779,3 +779,29 @@ Prettier keeps an end-of-line comment there (`foo // c` + `(a)`, the same as our
Its function declarations keep the same-line block comment outside (`function foo /* c */(a) {}`).
Before `<`, Prettier glues an own-line comment back onto the callee's line (`foo// c` + `<T>(a)`); we keep it own-line.
Before an optional call's `?.`, Prettier moves an own-line comment into the arguments as well; we keep it own-line.
## instantiation-expression-parens
- Why: semantics (Prettier's output no longer parses; prettier/prettier#20177)
- Pin: `tests/fixtures/ts/parenthesis/instantiation-expression-parens.ts`
```ts
// input
const a = (make<T>)!;
export default (class {})<string>;
(class {})<string>;
// ours
const a = (make<T>)!;
export default (class {}<string>);
(class {})<string>;
// prettier
const a = make<T>!;
export default class {}<string>;
class {}<string>;
```
Prettier drops the parens around an instantiation expression before `!`,
and the ones that keep a leading `class`/`function` from starting a declaration or statement.
Fixed in Prettier `main` by prettier/prettier#20177; we follow that output.
@@ -960,14 +960,18 @@ fn class_extends_needs_parens_through_non_null(expression: &Expression<'_>) -> b
impl NeedsParentheses<'_> for AstNode<'_, TSInstantiationExpression<'_>> {
fn needs_parentheses(&self, _f: &JsFormatter<'_, '_>) -> bool {
let expr = match self.parent() {
AstNodes::StaticMemberExpression(expr) => &expr.object,
AstNodes::ComputedMemberExpression(expr) => &expr.object,
AstNodes::PrivateFieldExpression(expr) => &expr.object,
_ => return false,
};
self.span == expr.span()
match self.parent() {
AstNodes::StaticMemberExpression(expr) => self.span == expr.object.span(),
AstNodes::ComputedMemberExpression(expr) => self.span == expr.object.span(),
AstNodes::PrivateFieldExpression(expr) => self.span == expr.object.span(),
// `(f<T>)!`, since `f<T>!` does not parse
AstNodes::TSNonNullExpression(_) => true,
// `export default (class {}<T>)`
AstNodes::ExportDefaultDeclaration(_) => {
export_default_leftmost_needs_parens(self.expression())
}
_ => false,
}
}
}
@@ -1151,7 +1155,8 @@ fn is_first_in_statement(
| AstNodes::ChainExpression(_)
| AstNodes::TSAsExpression(_)
| AstNodes::TSSatisfiesExpression(_)
| AstNodes::TSNonNullExpression(_) => {}
| AstNodes::TSNonNullExpression(_)
| AstNodes::TSInstantiationExpression(_) => {}
AstNodes::SequenceExpression(sequence) => {
if sequence.expressions.first().unwrap().span() != current_span {
break;
@@ -1228,28 +1233,29 @@ fn ts_as_or_satisfies_needs_parens(
| AstNodes::LogicalExpression(_)
| AstNodes::BinaryExpression(_) => true,
// `export default (function foo() {} as bar)` and `export default (class {} as bar)`.
// Without parens, the leading `function`/`class` token makes `export default` parse the RHS as a declaration,
// detaching the cast.
// The leftmost expression is checked (not just `inner`) so chained casts and member chains are covered too,
// e.g. `export default (function () {} as A as B)` and `export default (function () {}.bar as A)`.
//
// When the leftmost function/class already wraps itself,
// the outer cast doesn't need its own parens, or we'd emit `((function () {})() as A)`.
// Mirrors `Function::needs_parentheses`.
AstNodes::ExportDefaultDeclaration(_) => {
let leftmost = ExpressionLeftSide::leftmost(inner);
matches!(
leftmost.as_ref(),
Expression::FunctionExpression(_) | Expression::ClassExpression(_)
) && !matches!(leftmost.parent(), AstNodes::TaggedTemplateExpression(_))
&& !leftmost.is_call_like_callee()
}
AstNodes::ExportDefaultDeclaration(_) => export_default_leftmost_needs_parens(inner),
_ => {
type_cast_like_needs_parens(span, parent)
}
}
}
/// Whether a wrapper directly under `export default` needs parens because its leftmost expression is a function/class.
/// Without parens, the leading `function`/`class` token makes `export default` parse the RHS as a declaration,
/// detaching the wrapper (e.g. a cast or type arguments).
/// The leftmost expression is checked (not just `expr`) so chained wrappers and member chains are covered too,
/// e.g. `export default (function () {} as A as B)` and `export default (function () {}.bar as A)`.
///
/// When the leftmost function/class already wraps itself,
/// the wrapper doesn't need its own parens, or we'd emit `((function () {})() as A)`.
/// Mirrors `Function::needs_parentheses`.
fn export_default_leftmost_needs_parens(expr: &AstNode<'_, Expression<'_>>) -> bool {
let leftmost = ExpressionLeftSide::leftmost(expr);
matches!(leftmost.as_ref(), Expression::FunctionExpression(_) | Expression::ClassExpression(_))
&& !matches!(leftmost.parent(), AstNodes::TaggedTemplateExpression(_))
&& !leftmost.is_call_like_callee()
}
fn is_class_extends(span: Span, parent: &AstNodes<'_>) -> bool {
if let AstNodes::Class(c) = parent {
return c.heritage().is_some_and(|heritage| heritage.expression().span() == span);
@@ -55,6 +55,7 @@ impl<'a, 'b> ExpressionLeftSide<'a, 'b> {
AstNodes::TSAsExpression(expr) => Some(expr.expression().into()),
AstNodes::TSSatisfiesExpression(expr) => Some(expr.expression().into()),
AstNodes::TSNonNullExpression(expr) => Some(expr.expression().into()),
AstNodes::TSInstantiationExpression(expr) => Some(expr.expression().into()),
AstNodes::AssignmentExpression(expr) => Some(Self::AssignmentTarget(expr.left())),
AstNodes::UpdateExpression(expr) => {
if expr.prefix {
@@ -0,0 +1,9 @@
// Every line deviates from Prettier (DIVERGENCES.md#instantiation-expression-parens).
const a = (make<T>)!;
(make<T>)!.b;
(make<T>)!();
export default (class {})<string>;
export default (function () {})<string>;
export default (class {})<string> as A;
(class {})<string>;
(function () {})<string>;
@@ -0,0 +1,42 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// Every line deviates from Prettier (DIVERGENCES.md#instantiation-expression-parens).
const a = (make<T>)!;
(make<T>)!.b;
(make<T>)!();
export default (class {})<string>;
export default (function () {})<string>;
export default (class {})<string> as A;
(class {})<string>;
(function () {})<string>;
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// Every line deviates from Prettier (DIVERGENCES.md#instantiation-expression-parens).
const a = (make<T>)!;
(make<T>)!.b;
(make<T>)!();
export default (class {}<string>);
export default (function () {}<string>);
export default (class {}<string> as A);
(class {})<string>;
(function () {})<string>;
-------------------
{ printWidth: 100 }
-------------------
// Every line deviates from Prettier (DIVERGENCES.md#instantiation-expression-parens).
const a = (make<T>)!;
(make<T>)!.b;
(make<T>)!();
export default (class {}<string>);
export default (function () {}<string>);
export default (class {}<string> as A);
(class {})<string>;
(function () {})<string>;
===================== End =====================