Repository: eslint-stylistic/eslint-stylistic (ts)

Issue:
`keyword-spacing`: add "catch" special treatment
### Clear and concise description of the problem

The current rule, setup with the rule parameters:
```
        "@stylistic/keyword-spacing": ["warn",
            {before: true, after: false, overrides: {catch: {after: true}, }
         }]
```
cannot cover both valid code:
```
try { . . . }
catch(error) { . . . }
```
and
```
try { . . . }
catch { . . . }
```
it complains on the first one. Setting `after: false` complains on the second one. Currently there is no way to satisfy both codes. I'm forced to put disabling comment in a lot of files of my project.

### Suggested solution

In the `override` section, add a third option `ignore` besides `before` and `after` to suspend the checking for a specific keyword, that, for my understanding, is only "catch".

### Alternative

The current alternative is to use the following parameters:
```
        "@stylistic/keyword-spacing": ["warn",
            {before: true, after: false, overrides: {catch: {after: false}, }
         }]
```
and add disabling comment before all catch without parameters:
```
try { . . . }
// eslint-disable-next-line @stylistic/keyword-spacing
catch { . . . }
```


### Additional context

_No response_

### Validations

- [x] Follow our [Code of Conduct](https://github.com/eslint-stylistic/eslint-stylistic/blob/main/CODE_OF_CONDUCT.md)
- [x] Read the [Contributing Guide](https://eslint.style/contribute/guide).
- [x] Check that there isn't already an issue that request the same feature to avoid creating a duplicate.

### Contributes

- [ ] If this feature request is accepted, I am willing to submit a PR to fix this issue

Pull request description:
feat(space-before-function-paren)!: handle catch clause with param
<!-- 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
> [!CAUTION]
> BREAKING CHANGE

Handling spacing after the `catch` keyword and the following opening parenthesis (only when parameters are present) in `space-before-function-paren` instead of `keyword-spacing`

<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->

### Linked Issues

close #770 
resolves #681 

### Additional context

<!-- e.g. is there anything you'd like reviewers to focus on? -->


Patch:
diff --git a/packages/eslint-plugin/rules/keyword-spacing/README.md b/packages/eslint-plugin/rules/keyword-spacing/README.md
index 3dda81be..9f83e5f0 100644
--- a/packages/eslint-plugin/rules/keyword-spacing/README.md
+++ b/packages/eslint-plugin/rules/keyword-spacing/README.md
@@ -1,6 +1,8 @@
 ---
 title: keyword-spacing
 rule_type: layout
+related_rules:
+  - space-before-function-paren
 ---
 
 # keyword-spacing
@@ -20,7 +22,10 @@ if (foo) {
 
 Of course, you could also have a style guide that disallows spaces around keywords.
 
-However, if you want to enforce the style of spacing between the `function` keyword and the following opening parenthesis, please refer to [space-before-function-paren](space-before-function-paren).
+However, if you want to enforce the style of following spacing cases, please refer to [space-before-function-paren](space-before-function-paren).
+
+- Between the `function` keyword and the following opening parenthesis
+- After the `catch` keyword and the following opening parenthesis (only when parameters are present)
 
 ## Rule Details
 
diff --git a/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing.ts b/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing.ts
index 2848752f..8c083ea0 100644
--- a/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing.ts
+++ b/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing.ts
@@ -467,8 +467,19 @@ export default createRule<RuleOptions, MessageIds>({
       ReturnStatement: checkSpacingAroundFirstToken,
       ThrowStatement: checkSpacingAroundFirstToken,
       TryStatement(node) {
+        // try
         checkSpacingAroundFirstToken(node)
-        checkSpacingAroundFirstToken(node.handler)
+        // catch
+        if (node.handler) {
+          // The space after `catch` handled by the `space-before-function-paren`
+          if (node.handler.param) {
+            checkSpacingBeforeFirstToken(node.handler)
+          }
+          else {
+            checkSpacingAroundFirstToken(node.handler)
+          }
+        }
+        // finally
         checkSpacingAroundTokenBefore(node.finalizer)
       },
 
diff --git a/packages/eslint-plugin/rules/space-before-function-paren/README.md b/packages/eslint-plugin/rules/space-before-function-paren/README.md
index 9df342a8..d76eb438 100644
--- a/packages/eslint-plugin/rules/space-before-function-paren/README.md
+++ b/packages/eslint-plugin/rules/space-before-function-paren/README.md
@@ -40,7 +40,8 @@ This rule has a string option or an object option:
     "space-before-function-paren": ["error", {
         "anonymous": "always",
         "named": "always",
-        "asyncArrow": "always"
+        "asyncArrow": "always",
+        "catch": "always"
     }],
 }
 ```
@@ -56,6 +57,7 @@ Each of the following options can be set to `"always"`, `"never"`, or `"ignore"`
 - `anonymous` is for anonymous function expressions (e.g. `function () {}`).
 - `named` is for named function expressions (e.g. `function foo () {}`).
 - `asyncArrow` is for async arrow function expressions (e.g. `async () => {}`).
+- `catch` is for catch clause with params (e.g. `catch (e) {}`).
 
 ### "always"
 
@@ -92,6 +94,12 @@ var baz = {
 };
 
 var baz = async() => 1
+
+try {
+    // ...
+} catch(e) {
+    // ...
+}
 ```
 
 :::
@@ -129,6 +137,12 @@ var baz = {
 };
 
 var baz = async () => 1
+
+try {
+    // ...
+} catch (e) {
+    // ...
+}
 ```
 
 :::
@@ -168,6 +182,12 @@ var baz = {
 };
 
 var baz = async () => 1
+
+try {
+    // ...
+} catch (e) {
+    // ...
+}
 ```
 
 :::
@@ -205,18 +225,24 @@ var baz = {
 };
 
 var baz = async() => 1
+
+try {
+    // ...
+} catch(e) {
+    // ...
+}
 ```
 
 :::
 
-### `{"anonymous": "always", "named": "never", "asyncArrow": "always"}`
+### `{"anonymous": "always", "named": "never", "asyncArrow": "always", "catch": "always"}`
 
-Examples of **incorrect** code for this rule with the `{"anonymous": "always", "named": "never", "asyncArrow": "always"}` option:
+Examples of **incorrect** code for this rule with the `{"anonymous": "always", "named": "never", "asyncArrow": "always", "catch": "always"}` option:
 
 ::: incorrect
 
 ```js
-/*eslint space-before-function-paren: ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}]*/
+/*eslint space-before-function-paren: ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always", "catch": "always"}]*/
 /*eslint-env es6*/
 
 function foo () {
@@ -240,16 +266,22 @@ var baz = {
 };
 
 var baz = async(a) => await a
+
+try {
+    // ...
+} catch(e) {
+    // ...
+}
 ```
 
 :::
 
-Examples of **correct** code for this rule with the `{"anonymous": "always", "named": "never", "asyncArrow": "always"}` option:
+Examples of **correct** code for this rule with the `{"anonymous": "always", "named": "never", "asyncArrow": "always", "catch": "always"}` option:
 
 ::: correct
 
 ```js
-/*eslint space-before-function-paren: ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}]*/
+/*eslint space-before-function-paren: ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always", "catch": "always"}]*/
 /*eslint-env es6*/
 
 function foo() {
@@ -273,18 +305,24 @@ var baz = {
 };
 
 var baz = async (a) => await a
+
+try {
+    // ...
+} catch (e) {
+    // ...
+}
 ```
 
 :::
 
-### `{"anonymous": "never", "named": "always"}`
+### `{"anonymous": "never", "named": "always", "catch": "never"}`
 
-Examples of **incorrect** code for this rule with the `{"anonymous": "never", "named": "always"}` option:
+Examples of **incorrect** code for this rule with the `{"anonymous": "never", "named": "always", "catch": "never"}` option:
 
 ::: incorrect
 
 ```js
-/*eslint space-before-function-paren: ["error", { "anonymous": "never", "named": "always" }]*/
+/*eslint space-before-function-paren: ["error", { "anonymous": "never", "named": "always", "catch": "never" }]*/
 /*eslint-env es6*/
 
 function foo() {
@@ -306,16 +344,22 @@ var baz = {
         // ...
     }
 };
+
+try {
+    // ...
+} catch (e) {
+    // ...
+}
 ```
 
 :::
 
-Examples of **correct** code for this rule with the `{"anonymous": "never", "named": "always"}` option:
+Examples of **correct** code for this rule with the `{"anonymous": "never", "named": "always", "catch": "never"}` option:
 
 ::: correct
 
 ```js
-/*eslint space-before-function-paren: ["error", { "anonymous": "never", "named": "always" }]*/
+/*eslint space-before-function-paren: ["error", { "anonymous": "never", "named": "always", "catch": "never" }]*/
 /*eslint-env es6*/
 
 function foo () {
@@ -337,18 +381,24 @@ var baz = {
         // ...
     }
 };
+
+try {
+    // ...
+} catch(e) {
+    // ...
+}
 ```
 
 :::
 
-### `{"anonymous": "ignore", "named": "always"}`
+### `{"anonymous": "ignore", "named": "always", "catch": "never"}`
 
-Examples of **incorrect** code for this rule with the `{"anonymous": "ignore", "named": "always"}` option:
+Examples of **incorrect** code for this rule with the `{"anonymous": "ignore", "named": "always", "catch": "never"}` option:
 
 ::: incorrect
 
 ```js
-/*eslint space-before-function-paren: ["error", { "anonymous": "ignore", "named": "always" }]*/
+/*eslint space-before-function-paren: ["error", { "anonymous": "ignore", "named": "always", "catch": "never" }]*/
 /*eslint-env es6*/
 
 function foo() {
@@ -366,16 +416,22 @@ var baz = {
         // ...
     }
 };
+
+try {
+    // ...
+} catch (e) {
+    // ...
+}
 ```
 
 :::
 
-Examples of **correct** code for this rule with the `{"anonymous": "ignore", "named": "always"}` option:
+Examples of **correct** code for this rule with the `{"anonymous": "ignore", "named": "always", "catch": "never"}` option:
 
 ::: correct
 
 ```js
-/*eslint space-before-function-paren: ["error", { "anonymous": "ignore", "named": "always" }]*/
+/*eslint space-before-function-paren: ["error", { "anonymous": "ignore", "named": "always", "catch": "never" }]*/
 /*eslint-env es6*/
 
 var bar = function() {
@@ -401,6 +457,12 @@ var baz = {
         // ...
     }
 };
+
+try {
+    // ...
+} catch(e) {
+    // ...
+}
 ```
 
 :::
diff --git a/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren.ts b/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren.ts
index 067a210f..b3c8d0ea 100644
--- a/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren.ts
+++ b/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren.ts
@@ -1,4 +1,4 @@
-import type { Tree } from '#types'
+import type { Token, Tree } from '#types'
 import type { MessageIds, RuleOptions } from './types'
 import { createRule } from '#utils/create-rule'
 import { AST_NODE_TYPES } from '@typescript-eslint/utils'
@@ -36,6 +36,10 @@ export default createRule<RuleOptions, MessageIds>({
                 type: 'string',
                 enum: ['always', 'never', 'ignore'],
               },
+              catch: {
+                type: 'string',
+                enum: ['always', 'never', 'ignore'],
+              },
             },
             additionalProperties: false,
           },
@@ -133,6 +137,9 @@ export default createRule<RuleOptions, MessageIds>({
       if (functionConfig === 'ignore')
         return
 
+      if (functionConfig === 'always' && node.typeParameters && !node.id)
+        return
+
       let leftToken: Tree.Token
       let rightToken: Tree.Token
       if (node.typeParameters) {
@@ -144,9 +151,24 @@ export default createRule<RuleOptions, MessageIds>({
         leftToken = sourceCode.getTokenBefore(rightToken)!
       }
 
+      checkSpace(node, leftToken, rightToken, functionConfig)
+    }
+
+    function checkSpace(
+      node:
+        | Tree.ArrowFunctionExpression
+        | Tree.FunctionDeclaration
+        | Tree.FunctionExpression
+        | Tree.CatchClause
+        | Tree.TSDeclareFunction
+        | Tree.TSEmptyBodyFunctionExpression,
+      leftToken: Token,
+      rightToken: Token,
+      option: FuncOption,
+    ) {
       const hasSpacing = sourceCode.isSpaceBetween(leftToken, rightToken)
 
-      if (hasSpacing && functionConfig === 'never') {
+      if (hasSpacing && option === 'never') {
         context.report({
           node,
           loc: {
@@ -168,11 +190,7 @@ export default createRule<RuleOptions, MessageIds>({
           },
         })
       }
-      else if (
-        !hasSpacing
-        && functionConfig === 'always'
-        && (!node.typeParameters || node.id)
-      ) {
+      else if (!hasSpacing && option === 'always') {
         context.report({
           node,
           loc: rightToken.loc,
@@ -188,6 +206,20 @@ export default createRule<RuleOptions, MessageIds>({
       FunctionExpression: checkFunction,
       TSEmptyBodyFunctionExpression: checkFunction,
       TSDeclareFunction: checkFunction,
+      CatchClause(node) {
+        if (!node.param)
+          return
+
+        const option = overrideConfig.catch ?? baseConfig
+
+        if (option === 'ignore')
+          return
+
+        const rightToken = sourceCode.getFirstToken(node, isOpeningParenToken)!
+        const leftToken = sourceCode.getTokenBefore(rightToken)!
+
+        checkSpace(node, leftToken, rightToken, option)
+      },
     }
   },
 })
diff --git a/packages/eslint-plugin/rules/space-before-function-paren/types.d.ts b/packages/eslint-plugin/rules/space-before-function-paren/types.d.ts
index c19e0d97..2518392d 100644
--- a/packages/eslint-plugin/rules/space-before-function-paren/types.d.ts
+++ b/packages/eslint-plugin/rules/space-before-function-paren/types.d.ts
@@ -1,6 +1,6 @@
 /* GENERATED, DO NOT EDIT DIRECTLY */
 
-/* @checksum: Kz_Mu_8GLtgwE6ZVqrwQZnwlqOZRD4NAWufYw9AJBDk */
+/* @checksum: B9qHL1NzSVJct-9B-cD09lId05xVPKRdaGuR3vGihsc */
 
 export type SpaceBeforeFunctionParenSchema0
   = | ('always' | 'never')
@@ -8,6 +8,7 @@ export type SpaceBeforeFunctionParenSchema0
       anonymous?: 'always' | 'never' | 'ignore'
       named?: 'always' | 'never' | 'ignore'
       asyncArrow?: 'always' | 'never' | 'ignore'
+      catch?: 'always' | 'never' | 'ignore'
     }
 
 export type SpaceBeforeFunctionParenRuleOptions = [


Test patch:
diff --git a/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing._js_.test.ts b/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing._js_.test.ts
index d387f60c..0d06801e 100644
--- a/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing._js_.test.ts
+++ b/packages/eslint-plugin/rules/keyword-spacing/keyword-spacing._js_.test.ts
@@ -353,6 +353,12 @@ run<RuleOptions, MessageIds>({
     // catch
     // ----------------------------------------------------------------------
 
+    'try {} catch {}',
+    { code: 'try{}catch{}', options: [NEITHER] },
+    { code: 'try{} catch {}', options: [override('catch', BOTH)] },
+    { code: 'try {}catch{}', options: [override('catch', NEITHER)] },
+    'try {}\ncatch {}',
+    { code: 'try{}\ncatch{}', options: [NEITHER] },
     'try {} catch (e) {}',
     { code: 'try{}catch(e) {}', options: [NEITHER] },
     { code: 'try{} catch (e) {}', options: [override('catch', BOTH)] },
@@ -1979,28 +1985,51 @@ run<RuleOptions, MessageIds>({
     // ----------------------------------------------------------------------
 
     {
-      code: 'try {}catch(e) {}',
-      output: 'try {} catch (e) {}',
+      code: 'try {}catch{}',
+      output: 'try {} catch {}',
       errors: expectedBeforeAndAfter('catch'),
     },
     {
-      code: 'try{} catch (e) {}',
-      output: 'try{}catch(e) {}',
+      code: 'try{} catch {}',
+      output: 'try{}catch{}',
       options: [NEITHER],
       errors: unexpectedBeforeAndAfter('catch'),
     },
     {
-      code: 'try{}catch(e) {}',
-      output: 'try{} catch (e) {}',
+      code: 'try{}catch{}',
+      output: 'try{} catch {}',
       options: [override('catch', BOTH)],
       errors: expectedBeforeAndAfter('catch'),
     },
     {
-      code: 'try {} catch (e) {}',
-      output: 'try {}catch(e) {}',
+      code: 'try {} catch {}',
+      output: 'try {}catch{}',
       options: [override('catch', NEITHER)],
       errors: unexpectedBeforeAndAfter('catch'),
     },
+    {
+      code: 'try {}catch(e) {}',
+      output: 'try {} catch(e) {}',
+      errors: expectedBefore('catch'),
+    },
+    {
+      code: 'try{} catch (e) {}',
+      output: 'try{}catch (e) {}',
+      options: [NEITHER],
+      errors: unexpectedBefore('catch'),
+    },
+    {
+      code: 'try{}catch(e) {}',
+      output: 'try{} catch(e) {}',
+      options: [override('catch', BOTH)],
+      errors: expectedBefore('catch'),
+    },
+    {
+      code: 'try {} catch (e) {}',
+      output: 'try {}catch (e) {}',
+      options: [override('catch', NEITHER)],
+      errors: unexpectedBefore('catch'),
+    },
 
     // ----------------------------------------------------------------------
     // class
diff --git a/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren._js_.test.ts b/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren._js_.test.ts
index a9a74d92..3491f59f 100644
--- a/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren._js_.test.ts
+++ b/packages/eslint-plugin/rules/space-before-function-paren/space-before-function-paren._js_.test.ts
@@ -117,6 +117,15 @@ run<RuleOptions, MessageIds>({
     { code: 'async () => 1', parserOptions: { ecmaVersion: 8 } },
     { code: 'async () => 1', options: ['always'], parserOptions: { ecmaVersion: 8 } },
     { code: 'async() => 1', options: ['never'], parserOptions: { ecmaVersion: 8 } },
+
+    // Catch clause
+    { code: 'try {} catch (e) {}' },
+    { code: 'try {} catch (e) {}', options: ['always'] },
+    { code: 'try {} catch(e) {}', options: ['never'] },
+    { code: 'try {} catch (e) {}', options: [{ catch: 'always' }] },
+    { code: 'try {} catch(e) {}', options: [{ catch: 'never' }] },
+    { code: 'try {} catch (e) {}', options: [{ catch: 'ignore' }] },
+    { code: 'try {} catch(e) {}', options: [{ catch: 'ignore' }] },
   ],
 
   invalid: [
@@ -612,5 +621,36 @@ run<RuleOptions, MessageIds>({
       parserOptions: { ecmaVersion: 8 },
       errors: [{ messageId: 'unexpectedSpace', type: 'ArrowFunctionExpression' }],
     },
+
+    // Catch clause
+    {
+      code: 'try {} catch(e) {}',
+      output: 'try {} catch (e) {}',
+      errors: [{ messageId: 'missingSpace', type: 'CatchClause' }],
+    },
+    {
+      code: 'try {} catch(e) {}',
+      output: 'try {} catch (e) {}',
+      options: ['always'],
+      errors: [{ messageId: 'missingSpace', type: 'CatchClause' }],
+    },
+    {
+      code: 'try {} catch (e) {}',
+      output: 'try {} catch(e) {}',
+      options: ['never'],
+      errors: [{ messageId: 'unexpectedSpace', type: 'CatchClause' }],
+    },
+    {
+      code: 'try {} catch(e) {}',
+      output: 'try {} catch (e) {}',
+      options: [{ catch: 'always' }],
+      errors: [{ messageId: 'missingSpace', type: 'CatchClause' }],
+    },
+    {
+      code: 'try {} catch (e) {}',
+      output: 'try {} catch(e) {}',
+      options: [{ catch: 'never' }],
+      errors: [{ messageId: 'unexpectedSpace', type: 'CatchClause' }],
+    },
   ],
 })