Repository: eslint-stylistic/eslint-stylistic
Pull request #873 (2025-07-08 16:27:52)

feat(no-extra-parens): support part of TS Nodes
<!-- DO NOT IGNORE THE TEMPLATE!

Thank you for contributing!

Before submitting the PR, please make sure you do the following:

- Read the [Contributing Guide](https://eslint.style/contribute/guide).
- Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
- Ideally, include relevant tests that fail without this PR but pass with it.

-->

### Description

`TSStringKeyword` only checks the `string` keyword, it's not enough obviously.

This PR adds support for:
- [x] `TSTypeAnnotation`
	``` ts
	const x: number = 1;
	//     ^^^^^^^^
	
	function foo(x: number): number { return x; }
	//            ^^^^^^^^ ^^^^^^^^
	```
- [x] `TSTypeAliasDeclaration`
- [x] `TSEnumMember`
- [x] `TSArrayType`
- [x] `TSIntersectionType`
- [x] `TSUnionType`

<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->

### Linked Issues

### Additional context

<!-- e.g. is there anything you'd like reviewers to focus on? -->


codecov[bot]: ## [Bundle](https://app.codecov.io/gh/eslint-stylistic/eslint-stylistic/pull/873?dropdown=bundle) Report

Changes will increase total bundle size by 31 bytes (0.0%) :arrow_up:. This is within the [configured](https://docs.codecov.com/docs/javascript-bundle-analysis#main-features) threshold :white_check_mark:

<details><summary>Detailed changes</summary>

| Bundle name | Size | Change |
| ----------- | ---- | ------ |
| @stylistic/eslint-plugin-esm | 771.19kB | 31 bytes (0.0%) :arrow_up: |

</details>

### Affected Assets, Files, and Routes:

<details>
<summary>view changes for bundle: @stylistic/eslint-plugin-esm</summary>

#### **Assets Changed:**
| Asset Name | Size Change | Total Size | Change (%) |
| ---------- | ----------- | ---------- | ---------- |
| ```rules/no-extra-parens.js``` | 31 bytes | 42.62kB | 0.07%  |


**Files in** **```rules/no-extra-parens.js```**:

- ```./rules/no-extra-parens/no-extra-parens.ts``` → Total Size: **42.28kB**





</details>


Patch:
diff --git a/packages/eslint-plugin/rules/multiline-comment-style/README.md b/packages/eslint-plugin/rules/multiline-comment-style/README.md
index b2a20be9..98d38682 100644
--- a/packages/eslint-plugin/rules/multiline-comment-style/README.md
+++ b/packages/eslint-plugin/rules/multiline-comment-style/README.md
@@ -11,16 +11,64 @@ Many style guides require a particular style for comments that span multiple lin
 
 This rule aims to enforce a particular style for multiline comments.
 
+Other than regular JavaScript comments, the following types of comments are recognized by this rule:
+
+1. **[JSDoc comments](https://jsdoc.app/)**
+
+   These are comments which start with a `/**` sequence. They are used to explain the functionality of functions, classes, methods, and other code elements, making the code self-documenting.
+
+   Example:
+
+   ```js
+   /**
+    * Represents a book.
+    * @constructor
+    * @param {string} title - The title of the book.
+    * @param {string} author - The author of the book.
+    */
+   ```
+
+2. **Exclamation comments**
+
+   These are comments which start with a `/*!` sequence. They are conventionally used in JavaScript to indicate a _preserved comment_ or _important comment_. An exclamation comment signals its consumer that the comment should be preserved during processing.
+
+   Exclamation comments are often used for including copyright information, attribution, or other important metadata that needs to remain in the code.
+
+   Example:
+
+   ```js
+   /*!
+    * My JavaScript Library
+    * Copyright (c) 2025 John Doe
+    *
+    * Licensed under the MIT license.
+    */
+   ```
+
+3. **Directive comments**
+
+   These are special comments that convey instructions or metadata to tools like linters, compilers (e.g., TypeScript), or build systems. They are not standard JavaScript comments meant for human readability, but rather specific syntax understood by particular tools to alter their behavior.
+
+   Example:
+
+   ```js
+   // @ts-ignore
+   or
+   /* eslint-disable */
+   ```
+
 ## Options
 
 This rule has a string option, which can have one of the following values:
 
 - `"starred-block"` (default): Disallows consecutive line comments in favor of block comments. Additionally, requires block comments to have an aligned `*` character before each line.
-- `"bare-block"`: Disallows consecutive line comments in favor of block comments, and disallows block comments from having a `"*"` character before each line. This option ignores JSDoc comments.
-- `"separate-lines"`: Disallows block comments in favor of consecutive line comments. By default, this option ignores JSDoc comments. To also apply this rule to JSDoc comments, set the `checkJSDoc` option to `true`.
+- `"bare-block"`: Disallows consecutive line comments in favor of block comments, and disallows block comments from having a `"*"` character before each line. This option ignores JSDoc and Exclamation comments.
+- `"separate-lines"`: Disallows block comments in favor of consecutive line comments. By default, this option ignores JSDoc and Exclamation comments. To also apply this rule to JSDoc comments and or Exclamation comments, set the `checkJSDoc` and or `checkExclamation` option to `true`.
 
 The rule always ignores directive comments such as `/* eslint-disable */`.
 
+## Examples
+
 Examples of **incorrect** code for this rule with the default `"starred-block"` option:
 
 ::: incorrect
@@ -179,6 +227,24 @@ foo();
 
 :::
 
+Examples of **incorrect** code for this rule with the `"separate-lines"` option and `checkExclamation` set to `true`:
+
+::: incorrect
+
+```js
+
+/* eslint @stylistic/multiline-comment-style: ["error", "separate-lines", { "checkExclamation": true }] */
+
+/*!
+ * I am an exclamation comment
+ * and I'm not allowed
+ */
+foo();
+
+```
+
+:::
+
 ## When Not To Use It
 
 If you don't want to enforce a particular style for multiline comments, you can disable the rule.
diff --git a/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.ts b/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.ts
index 2b03ad9e..5869e3b6 100644
--- a/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.ts
+++ b/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.ts
@@ -43,6 +43,9 @@ export default createRule<RuleOptions, MessageIds>({
                 checkJSDoc: {
                   type: 'boolean',
                 },
+                checkExclamation: {
+                  type: 'boolean',
+                },
               },
               additionalProperties: false,
             },
@@ -67,6 +70,7 @@ export default createRule<RuleOptions, MessageIds>({
     const option = context.options[0] || 'starred-block'
     const params = context.options[1] || {}
     const checkJSDoc = !!params.checkJSDoc
+    const checkExclamation = !!params.checkExclamation
 
     // ----------------------------------------------------------------------
     // Helpers
@@ -112,6 +116,22 @@ export default createRule<RuleOptions, MessageIds>({
         && isWhiteSpaces(lines.at(-1)!)
     }
 
+    /**
+     * Checks if a comment group is in exclamation form.
+     * @param firstComment A group of comments, containing either multiple line comments or a single block comment.
+     * @returns Whether or not the comment group is in exclamation form.
+     */
+    function isExclamationComment([firstComment]: Token[]): boolean {
+      if (firstComment.type !== 'Block')
+        return false
+
+      const lines = firstComment.value.split(LINEBREAK_MATCHER)
+
+      return /^!\s*$/u.test(lines[0])
+        && lines.slice(1, -1).every(line => /^\s* /u.test(line))
+        && isWhiteSpaces(lines.at(-1)!)
+    }
+
     /**
      * Processes a comment group that is currently in separate-line form, calculating the offset for each line.
      * @param commentGroup A group of comments containing multiple line comments.
@@ -275,8 +295,8 @@ export default createRule<RuleOptions, MessageIds>({
           const expectedLeadingWhitespace = getInitialOffset(firstComment)
           const expectedLinePrefix = `${expectedLeadingWhitespace} *`
 
-          if (!/^\*?\s*$/u.test(lines[0])) {
-            const start = firstComment.value.startsWith('*') ? firstComment.range[0] + 1 : firstComment.range[0]
+          if (!/^[*!]?\s*$/u.test(lines[0])) {
+            const start = /^[*!]/.test(firstComment.value) ? firstComment.range[0] + 1 : firstComment.range[0]
 
             context.report({
               loc: {
@@ -352,13 +372,14 @@ export default createRule<RuleOptions, MessageIds>({
         const [firstComment] = commentGroup
 
         const isJSDoc = isJSDocComment(commentGroup)
+        const isExclamation = isExclamationComment(commentGroup)
 
-        if (firstComment.type !== 'Block' || (!checkJSDoc && isJSDoc))
+        if (firstComment.type !== 'Block' || (!checkJSDoc && isJSDoc) || (!checkExclamation && isExclamation))
           return
 
         let commentLines = getCommentLines(commentGroup)
 
-        if (isJSDoc)
+        if (isJSDoc || isExclamation)
           commentLines = commentLines.slice(1, commentLines.length - 1)
 
         const tokenAfter = sourceCode.getTokenAfter(firstComment, { includeComments: true })
@@ -378,7 +399,7 @@ export default createRule<RuleOptions, MessageIds>({
         })
       },
       'bare-block': function (commentGroup: Token[]) {
-        if (isJSDocComment(commentGroup))
+        if (isJSDocComment(commentGroup) || isExclamationComment(commentGroup))
           return
 
         const [firstComment] = commentGroup
diff --git a/packages/eslint-plugin/rules/multiline-comment-style/types.d.ts b/packages/eslint-plugin/rules/multiline-comment-style/types.d.ts
index 00975c6f..a854b1ca 100644
--- a/packages/eslint-plugin/rules/multiline-comment-style/types.d.ts
+++ b/packages/eslint-plugin/rules/multiline-comment-style/types.d.ts
@@ -11,6 +11,7 @@ export type MultilineCommentStyleSchema0
       'separate-lines',
       {
         checkJSDoc?: boolean
+        checkExclamation?: boolean
       },
     ]
 
diff --git a/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens.ts b/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens.ts
index 0defd51e..66d5eed7 100644
--- a/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens.ts
+++ b/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens.ts
@@ -1480,12 +1480,37 @@ export default createRule<RuleOptions, MessageIds>({
           report(node.argument)
         }
       },
-      TSStringKeyword(node) {
-        if (hasExcessParens(node)) {
-          report({
-            ...node,
-            type: AST_NODE_TYPES.FunctionExpression as any,
-          })
+      TSArrayType(node) {
+        if (hasExcessParensWithPrecedence(node.elementType, precedence(node)))
+          report(node.elementType)
+      },
+      TSIntersectionType(node) {
+        node.types.forEach((type) => {
+          if (hasExcessParensWithPrecedence(type, precedence(node)))
+            report(type)
+        })
+      },
+      TSUnionType(node) {
+        node.types.forEach((type) => {
+          if (hasExcessParensWithPrecedence(type, precedence(node)))
+            report(type)
+        })
+      },
+      TSTypeAnnotation(node) {
+        if (hasExcessParens(node.typeAnnotation)) {
+          report(node.typeAnnotation)
+        }
+      },
+      TSTypeAliasDeclaration(node) {
+        if (hasExcessParens(node.typeAnnotation)) {
+          report(node.typeAnnotation)
+        }
+      },
+      TSEnumMember(node) {
+        if (!node.initializer)
+          return
+        if (hasExcessParens(node.initializer)) {
+          report(node.initializer)
         }
       },
     }
diff --git a/packages/shared/utils/ast/general.ts b/packages/shared/utils/ast/general.ts
index d10065e9..b8a85198 100644
--- a/packages/shared/utils/ast/general.ts
+++ b/packages/shared/utils/ast/general.ts
@@ -424,6 +424,7 @@ export function getPrecedence(node: ASTNode) {
       return 1
 
     case 'ConditionalExpression':
+    case 'TSConditionalType':
       return 3
 
     case 'LogicalExpression':
@@ -478,6 +479,11 @@ export function getPrecedence(node: ASTNode) {
 
       /* falls through */
 
+    case 'TSUnionType':
+      return 6
+    case 'TSIntersectionType':
+      return 8
+
     case 'UnaryExpression':
     case 'AwaitExpression':
       return 16
@@ -493,6 +499,10 @@ export function getPrecedence(node: ASTNode) {
     case 'NewExpression':
       return 19
 
+    case 'TSImportType':
+    case 'TSArrayType':
+      return 20
+
     default:
       if (node.type in eslintVisitorKeys)
         return 20


Test patch:
diff --git a/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.test.ts b/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.test.ts
index ac24fbb2..6a53f07c 100644
--- a/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.test.ts
+++ b/packages/eslint-plugin/rules/multiline-comment-style/multiline-comment-style.test.ts
@@ -22,6 +22,15 @@ run<RuleOptions, MessageIds>({
              * a JSDoc comment
              */
         `,
+    `
+            /*!
+             * this is
+             * an exclamation comment
+             */
+        `,
+    `
+            /*! this is a single line exclamation comment */
+        `,
     `
             /* eslint semi: [
               "error"
@@ -234,6 +243,33 @@ run<RuleOptions, MessageIds>({
             `,
       options: ['bare-block'],
     },
+    {
+      code: `
+                /*!
+                 * This is
+                 * an exclamation comment
+                 */
+            `,
+      options: ['separate-lines'],
+    },
+    {
+      code: `
+                /*!
+                 * This is
+                 * an exclamation comment
+                 */
+            `,
+      options: ['starred-block'],
+    },
+    {
+      code: `
+                /*!
+                 * This is
+                 * an exclamation comment
+                 */
+            `,
+      options: ['bare-block'],
+    },
     {
       code: `
                 /* This is
@@ -318,6 +354,39 @@ run<RuleOptions, MessageIds>({
             `,
       options: ['separate-lines'],
     },
+    {
+      code: `
+                /*!
+                 *    Exclamation blocks
+                 *  are
+                 *   ignored
+                 * !
+                 */
+            `,
+      options: ['bare-block'],
+    },
+    {
+      code: `
+                /*!
+                 *    Exclamation blocks
+                 *  are
+                 *   ignored
+                 * !
+                 */
+            `,
+      options: ['starred-block'],
+    },
+    {
+      code: `
+                /*!
+                 *    Exclamation blocks
+                 *  are
+                 *   ignored
+                 * !
+                 */
+            `,
+      options: ['separate-lines'],
+    },
     {
       code: `
                 /*
@@ -451,6 +520,34 @@ run<RuleOptions, MessageIds>({
             `,
       errors: [{ messageId: 'startNewline', line: 2 }],
     },
+    {
+      code: `
+                /*! this Exclamation comment
+                 * is missing a newline
+                 * at the start
+                 */
+            `,
+      output: `
+                /*!
+                 * this Exclamation comment
+                 * is missing a newline
+                 * at the start
+                 */
+            `,
+      errors: [{ messageId: 'startNewline', line: 2 }],
+    },
+    {
+      code: `
+                /*! this Exclamation comment is missing a newline at the start
+                 */
+            `,
+      output: `
+                /*!
+                 * this Exclamation comment is missing a newline at the start
+                 */
+            `,
+      errors: [{ messageId: 'startNewline', line: 2 }],
+    },
     {
       code: `
                 /*
@@ -632,6 +729,20 @@ run<RuleOptions, MessageIds>({
       options: ['separate-lines', { checkJSDoc: true }],
       errors: [{ messageId: 'expectedLines', line: 2 }],
     },
+    {
+      code: `
+                /*!
+                 * Exclamation
+                 * Comment
+                 */
+            `,
+      output: `
+                // Exclamation
+                // Comment
+            `,
+      options: ['separate-lines', { checkExclamation: true }],
+      errors: [{ messageId: 'expectedLines', line: 2 }],
+    },
     {
       code: `
                 /* foo
diff --git a/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens._ts_.test.ts b/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens._ts_.test.ts
index 5a53bacf..0d1bc541 100644
--- a/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens._ts_.test.ts
+++ b/packages/eslint-plugin/rules/no-extra-parens/no-extra-parens._ts_.test.ts
@@ -358,6 +358,9 @@ run<RuleOptions, MessageIds>({
     {
       code: 'const x = (1 satisfies number).toFixed();',
     },
+
+    `type Foo = string & (number | 'bar')`,
+    `type Foo = (a extends string ? 'bar' : number)[]`,
   ],
 
   invalid: [
@@ -484,5 +487,65 @@ run<RuleOptions, MessageIds>({
       output: 'class A{ accessor [(foo)] = 1 }',
       errors: [{ messageId: 'unexpected' }],
     },
+    {
+      code: 'function foo(x: (number)): (boolean) {}',
+      output: 'function foo(x: number): boolean {}',
+      errors: [
+        { messageId: 'unexpected' },
+        { messageId: 'unexpected' },
+      ],
+    },
+    {
+      code: $`
+        type Foo = ({
+          a: <T>(x: T) => any
+        })
+      `,
+      output: $`
+        type Foo = {
+          a: <T>(x: T) => any
+        }
+      `,
+      errors: [{ messageId: 'unexpected' }],
+    },
+    {
+      code: $`
+        enum Foo {
+          A,
+          B = ("x"),
+        }
+      `,
+      output: $`
+        enum Foo {
+          A,
+          B = "x",
+        }
+      `,
+      errors: [
+        { messageId: 'unexpected' },
+      ],
+    },
+    {
+      code: `type Foo = (string & number) | 'bar'`,
+      output: `type Foo = string & number | 'bar'`,
+      errors: [
+        { messageId: 'unexpected' },
+      ],
+    },
+    {
+      code: 'type Foo = ((string | number))[]',
+      output: 'type Foo = (string | number)[]',
+      errors: [
+        { messageId: 'unexpected' },
+      ],
+    },
+    {
+      code: `type Foo = ((import('x')))[]`,
+      output: `type Foo = import('x')[]`,
+      recursive: Number.POSITIVE_INFINITY,
+      errors: [
+        { messageId: 'unexpected' },
+      ],
+    },
   ],
 })