Repository: prettier/prettier (JavaScript)

Issue:
Missing parens for `ClassExpression` with decorators
**Prettier 2.5.1**
[Playground link](https://prettier.io/playground/#N4Igxg9gdgLgprEAuEAKAAgM1QSgARgA2AhgM6l7AC+OA3CADQgQAOMAltKcqMQE58IAdwAK-BNxTFCQ4gE9uTAEZ9iYANZwYAZWIBbOABl2UOMkzTScZao1btLNSYDmyGHwCu1kHD1K4ACYBgYbEUM4exM5wAGIQfHrEMBzhyCDEHjAQjCAAFjB6hADquezwpI5gcNoSZewAbmVyaWDkOSZWfDAiqs6J5pbeAFakAB7aLoRwAIoeEPADhFZMjnydaUrE-oQ5LHwmMEXsATC5yAAcAAwrglZFqixpe3Cd9WZMAI5z8D2skumkAC0pkCgRyfDgX3YEJ6UX6SAsS28Vj07DcnmRkxm3zMCMGTBgWyOJzOSAATATVOxCC4AMIQPTwnykACsOQ8VgAKltJIjliB6l4AJJQYKwbRgfZsACCou0MDkU0WVioVCAA)
<!-- prettier-ignore -->
```sh
--parser babel
```

**Input:**
<!-- prettier-ignore -->
```jsx
(@f() class {});
```

**Output:**
<!-- prettier-ignore -->
```jsx
@f()
class {};

```

**Second Output:**
<!-- prettier-ignore -->
```jsx
SyntaxError: A class name is required. (2:7)
  1 | @f()
> 2 | class {};
    |       ^
  3 |
```

**Expected behavior:**

Should keep parens

----

Found this during #12241, when fixing this, should add [this test](https://github.com/prettier/prettier/pull/12241/commits/84d52514b3922912b24fe9f5826936543709c386) back.



Hints:

Patch:
diff --git a/src/language-js/needs-parens.js b/src/language-js/needs-parens.js
--- a/src/language-js/needs-parens.js
+++ b/src/language-js/needs-parens.js
@@ -1,6 +1,7 @@
 "use strict";
 
 const getLast = require("../utils/get-last.js");
+const isNonEmptyArray = require("../utils/is-non-empty-array.js");
 const {
   getFunctionParameters,
   getLeftSidePathName,
@@ -667,6 +668,10 @@ function needsParens(path, options) {
       }
 
     case "ClassExpression":
+      if (isNonEmptyArray(node.decorators)) {
+        return true;
+      }
+
       switch (parent.type) {
         case "NewExpression":
           return name === "callee";
diff --git a/src/language-js/printer-estree.js b/src/language-js/printer-estree.js
--- a/src/language-js/printer-estree.js
+++ b/src/language-js/printer-estree.js
@@ -106,19 +106,44 @@ function genericPrint(path, options, print, args) {
     return printed;
   }
 
+  let parts = [printed];
+
   const printedDecorators = printDecorators(path, options, print);
-  // Nodes with decorators can't have parentheses and don't need leading semicolons
+  const isClassExpressionWithDecorators =
+    node.type === "ClassExpression" && printedDecorators;
+  // Nodes (except `ClassExpression`) with decorators can't have parentheses and don't need leading semicolons
   if (printedDecorators) {
-    return group([...printedDecorators, printed]);
+    parts = [...printedDecorators, printed];
+
+    if (!isClassExpressionWithDecorators) {
+      return group(parts);
+    }
   }
 
   const needsParens = pathNeedsParens(path, options);
 
   if (!needsParens) {
-    return args && args.needsSemi ? [";", printed] : printed;
+    if (args && args.needsSemi) {
+      parts.unshift(";");
+    }
+
+    // In member-chain print, it add `label` to the doc, if we return array here it will be broken
+    if (parts.length === 1 && parts[0] === printed) {
+      return printed;
+    }
+
+    return parts;
+  }
+
+  if (isClassExpressionWithDecorators) {
+    parts = [indent([line, ...parts])];
   }
 
-  const parts = [args && args.needsSemi ? ";(" : "(", printed];
+  parts.unshift("(");
+
+  if (args && args.needsSemi) {
+    parts.unshift(";");
+  }
 
   if (hasFlowShorthandAnnotationComment(node)) {
     const [comment] = node.trailingComments;
@@ -126,6 +151,10 @@ function genericPrint(path, options, print, args) {
     comment.printed = true;
   }
 
+  if (isClassExpressionWithDecorators) {
+    parts.push(line);
+  }
+
   parts.push(")");
 
   return parts;


Test patch:
diff --git a/tests/format/js/decorators/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/decorators/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/js/decorators/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/js/decorators/__snapshots__/jsfmt.spec.js.snap
@@ -51,16 +51,20 @@ export class Bar {}
 export default class Baz {}
 
 const foo =
-  @deco
-  class {
-    //
-  };
+  (
+    @deco
+    class {
+      //
+    }
+  );
 
 const bar =
-  @deco
-  class {
-    //
-  };
+  (
+    @deco
+    class {
+      //
+    }
+  );
 
 ================================================================================
 `;
diff --git a/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap b/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,543 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`arguments.js [acorn] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js [espree] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js [flow] format 1`] = `
+"Unexpected token \`@\` (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js [typescript] format 1`] = `
+"Argument expression expected. (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js - {"semi":false} [acorn] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js - {"semi":false} [espree] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js - {"semi":false} [flow] format 1`] = `
+"Unexpected token \`@\` (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js - {"semi":false} [typescript] format 1`] = `
+"Argument expression expected. (1:13)
+> 1 | console.log(@deco class Foo {})
+    |             ^
+  2 | console.log(@deco class {})
+  3 |"
+`;
+
+exports[`arguments.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+console.log(@deco class Foo {})
+console.log(@deco class {})
+
+=====================================output=====================================
+console.log(
+  (
+    @deco
+    class Foo {}
+  )
+)
+console.log(
+  (
+    @deco
+    class {}
+  )
+)
+
+================================================================================
+`;
+
+exports[`arguments.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+console.log(@deco class Foo {})
+console.log(@deco class {})
+
+=====================================output=====================================
+console.log(
+  (
+    @deco
+    class Foo {}
+  )
+);
+console.log(
+  (
+    @deco
+    class {}
+  )
+);
+
+================================================================================
+`;
+
+exports[`class-expression.js [acorn] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js [espree] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js [flow] format 1`] = `
+"Unexpected token \`@\` (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js [typescript] format 1`] = `
+"Expression expected. (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js - {"semi":false} [acorn] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js - {"semi":false} [espree] format 1`] = `
+"Unexpected character '@' (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js - {"semi":false} [flow] format 1`] = `
+"Unexpected token \`@\` (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js - {"semi":false} [typescript] format 1`] = `
+"Expression expected. (1:13)
+> 1 | const a1 = (@deco class Foo {});
+    |             ^
+  2 | const a2 = (@deco class {});
+  3 |
+  4 | (@deco class Foo {});"
+`;
+
+exports[`class-expression.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+const a1 = (@deco class Foo {});
+const a2 = (@deco class {});
+
+(@deco class Foo {});
+(@deco class {});
+
+const b1 = []
+;(@deco class Foo {})
+
+const b2 = []
+;(@deco class {})
+
+// This is not a \`ClassExpression\` but \`ClassDeclaration\`
+@deco class Foo {}
+
+=====================================output=====================================
+const a1 =
+  (
+    @deco
+    class Foo {}
+  )
+const a2 =
+  (
+    @deco
+    class {}
+  )
+
+;(
+  @deco
+  class Foo {}
+)
+;(
+  @deco
+  class {}
+)
+
+const b1 = []
+;(
+  @deco
+  class Foo {}
+)
+
+const b2 = []
+;(
+  @deco
+  class {}
+)
+
+// This is not a \`ClassExpression\` but \`ClassDeclaration\`
+@deco
+class Foo {}
+
+================================================================================
+`;
+
+exports[`class-expression.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+const a1 = (@deco class Foo {});
+const a2 = (@deco class {});
+
+(@deco class Foo {});
+(@deco class {});
+
+const b1 = []
+;(@deco class Foo {})
+
+const b2 = []
+;(@deco class {})
+
+// This is not a \`ClassExpression\` but \`ClassDeclaration\`
+@deco class Foo {}
+
+=====================================output=====================================
+const a1 =
+  (
+    @deco
+    class Foo {}
+  );
+const a2 =
+  (
+    @deco
+    class {}
+  );
+
+(
+  @deco
+  class Foo {}
+);
+(
+  @deco
+  class {}
+);
+
+const b1 = [];
+(
+  @deco
+  class Foo {}
+);
+
+const b2 = [];
+(
+  @deco
+  class {}
+);
+
+// This is not a \`ClassExpression\` but \`ClassDeclaration\`
+@deco
+class Foo {}
+
+================================================================================
+`;
+
+exports[`member-expression.js [acorn] format 1`] = `
+"Unexpected character '@' (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js [espree] format 1`] = `
+"Unexpected character '@' (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js [flow] format 1`] = `
+"Unexpected token \`@\` (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js [typescript] format 1`] = `
+"Expression expected. (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js - {"semi":false} [acorn] format 1`] = `
+"Unexpected character '@' (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js - {"semi":false} [espree] format 1`] = `
+"Unexpected character '@' (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js - {"semi":false} [flow] format 1`] = `
+"Unexpected token \`@\` (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js - {"semi":false} [typescript] format 1`] = `
+"Expression expected. (1:2)
+> 1 | (@deco class Foo {}).name;
+    |  ^
+  2 | (@deco class {}).name;
+  3 |"
+`;
+
+exports[`member-expression.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+(@deco class Foo {}).name;
+(@deco class {}).name;
+
+=====================================output=====================================
+;((
+  @deco
+  class Foo {}
+).name)
+;((
+  @deco
+  class {}
+).name)
+
+================================================================================
+`;
+
+exports[`member-expression.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+(@deco class Foo {}).name;
+(@deco class {}).name;
+
+=====================================output=====================================
+((
+  @deco
+  class Foo {}
+).name);
+((
+  @deco
+  class {}
+).name);
+
+================================================================================
+`;
+
+exports[`super-class.js [acorn] format 1`] = `
+"Unexpected character '@' (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js [espree] format 1`] = `
+"Unexpected character '@' (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js [flow] format 1`] = `
+"Unexpected token \`@\` (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js [typescript] format 1`] = `
+"Expression expected. (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js - {"semi":false} [acorn] format 1`] = `
+"Unexpected character '@' (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js - {"semi":false} [espree] format 1`] = `
+"Unexpected character '@' (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js - {"semi":false} [flow] format 1`] = `
+"Unexpected token \`@\` (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js - {"semi":false} [typescript] format 1`] = `
+"Expression expected. (1:20)
+> 1 | class Foo extends (@deco class Foo {}){}
+    |                    ^
+  2 |
+  3 | class Foo extends (@deco class {}){}
+  4 |"
+`;
+
+exports[`super-class.js - {"semi":false} format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+semi: false
+                                                                                | printWidth
+=====================================input======================================
+class Foo extends (@deco class Foo {}){}
+
+class Foo extends (@deco class {}){}
+
+=====================================output=====================================
+class Foo extends (
+  @deco
+  class Foo {}
+) {}
+
+class Foo extends (
+  @deco
+  class {}
+) {}
+
+================================================================================
+`;
+
+exports[`super-class.js format 1`] = `
+====================================options=====================================
+parsers: ["babel", "flow", "typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+class Foo extends (@deco class Foo {}){}
+
+class Foo extends (@deco class {}){}
+
+=====================================output=====================================
+class Foo extends (
+  @deco
+  class Foo {}
+) {}
+
+class Foo extends (
+  @deco
+  class {}
+) {}
+
+================================================================================
+`;
diff --git a/tests/format/js/decorators/class-expression/arguments.js b/tests/format/js/decorators/class-expression/arguments.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/arguments.js
@@ -0,0 +1,2 @@
+console.log(@deco class Foo {})
+console.log(@deco class {})
diff --git a/tests/format/js/decorators/class-expression/class-expression.js b/tests/format/js/decorators/class-expression/class-expression.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/class-expression.js
@@ -0,0 +1,14 @@
+const a1 = (@deco class Foo {});
+const a2 = (@deco class {});
+
+(@deco class Foo {});
+(@deco class {});
+
+const b1 = []
+;(@deco class Foo {})
+
+const b2 = []
+;(@deco class {})
+
+// This is not a `ClassExpression` but `ClassDeclaration`
+@deco class Foo {}
diff --git a/tests/format/js/decorators/class-expression/jsfmt.spec.js b/tests/format/js/decorators/class-expression/jsfmt.spec.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/jsfmt.spec.js
@@ -0,0 +1,9 @@
+const errors = {
+  flow: true,
+  typescript: true,
+  acorn: true,
+  espree: true,
+};
+
+run_spec(__dirname, ["babel", "flow", "typescript"], { errors });
+run_spec(__dirname, ["babel", "flow", "typescript"], { semi: false, errors });
diff --git a/tests/format/js/decorators/class-expression/member-expression.js b/tests/format/js/decorators/class-expression/member-expression.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/member-expression.js
@@ -0,0 +1,2 @@
+(@deco class Foo {}).name;
+(@deco class {}).name;
diff --git a/tests/format/js/decorators/class-expression/super-class.js b/tests/format/js/decorators/class-expression/super-class.js
new file mode 100644
--- /dev/null
+++ b/tests/format/js/decorators/class-expression/super-class.js
@@ -0,0 +1,3 @@
+class Foo extends (@deco class Foo {}){}
+
+class Foo extends (@deco class {}){}
diff --git a/tests/format/typescript/type-arguments-bit-shift-left-like/4.ts b/tests/format/typescript/type-arguments-bit-shift-left-like/4.ts
new file mode 100644
--- /dev/null
+++ b/tests/format/typescript/type-arguments-bit-shift-left-like/4.ts
@@ -0,0 +1 @@
+(@f<<T>(v: T) => void>() class {});
diff --git a/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap b/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap
--- a/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/format/typescript/type-arguments-bit-shift-left-like/__snapshots__/jsfmt.spec.js.snap
@@ -49,6 +49,30 @@ exports[`3.ts [typescript] format 1`] = `
   2 |"
 `;
 
+exports[`4.ts [babel-ts] format 1`] = `
+====================================options=====================================
+parsers: ["typescript"]
+printWidth: 80
+                                                                                | printWidth
+=====================================input======================================
+(@f<<T>(v: T) => void>() class {});
+
+=====================================output=====================================
+(
+  @f<<T>(v: T) => void>()
+  class {}
+);
+
+================================================================================
+`;
+
+exports[`4.ts [typescript] format 1`] = `
+"Expression expected. (1:2)
+> 1 | (@f<<T>(v: T) => void>() class {});
+    |  ^
+  2 |"
+`;
+
 exports[`5.tsx [babel-ts] format 1`] = `
 ====================================options=====================================
 parsers: ["typescript"]
diff --git a/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js b/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js
--- a/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js
+++ b/tests/format/typescript/type-arguments-bit-shift-left-like/jsfmt.spec.js
@@ -1,3 +1,3 @@
 run_spec(__dirname, ["typescript"], {
-  errors: { typescript: ["3.ts", "5.tsx"] },
+  errors: { typescript: ["3.ts", "4.ts", "5.tsx"] },
 });