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

Issue:
`block-spacing`: stop using this rule for typescript types
### 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 reports the same bug to avoid creating a duplicate.

### Describe the bug

I need eslint to consider this code as valid:
```
const myFunc = (item: {foo: number}) => {
  console.log(item)
}
```
I am using the default rules of the eslint-stylistic plugin with the additional rule `'@stylistic/object-curly-spacing': ['error', 'never']` since I dont want spaces around curly braces. In this case the `block-spacing` rule considers the type declaration `{foo: number}` as a block and wants to insert whitespace. I cannot insert whitespace because the `object-curly-spacing` rule forbids it. What can I do in such a case if I want the plugin to consider that code as valid?

### Reproduction

https://stackblitz.com/edit/github-ky9dmn?file=src%2Findex.ts

### Contributes

- [ ] I am willing to submit a PR to fix this issue
- [ ] I am willing to submit a PR with failing tests
`type-annotation-newline`: New rule for enforce spacing inside of type annotation objects
### Clear and concise description of the problem

There are a few newline/spacing related rules:
* array-bracket-newline/array-bracket-spacing
* object-curly-newline/object-curly-spacing

This new rule would be the companion to type-annotation-spacing and provide basically the same functionality as object-curly-newline but for type annotations. Thank you for considering the proposal and I wouldn't intend to submit a PR for the issue.

### Suggested solution

In rule [type-annotation-newline] we could provide following implementation..

Incorrect
```
let a = {
  foo: 'bar'
} as { foo: string };

let Component = ({
  foo
}: { foo: string }): JSX.Element => {
  ...
}
```

Correct
```
let a = {
  foo: 'bar'
} as {
  foo: string
};

let Component = ({
  foo
}: {
  foo: string
}): JSX.Element => {
  ...
}
```

### Alternative

_No response_

### 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:
feat!: handle TS object-like nodes in `object-curly-spacing` instead of `block-spacing`
<!-- 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

- Stop handling `TSInterfaceBody`, `TSTypeLiteral`, `TSEnumDeclaration` in `block-spacing`
- Handling `TSInterfaceBody`, `TSEnumBody` in `object-curly-spacing`

<!-- Please insert your description here and provide especially info about the "what" this PR is solving -->

### Linked Issues

part of #755 
resolves #415 
close #752, close #656

### Additional context

<!-- e.g. is there anything you'd like reviewers to focus on? -->


Patch:
diff --git a/packages/eslint-plugin/rules/block-spacing/block-spacing._js_.test.ts b/packages/eslint-plugin/rules/block-spacing/block-spacing.test.ts
similarity index 100%
rename from packages/eslint-plugin/rules/block-spacing/block-spacing._js_.test.ts
rename to packages/eslint-plugin/rules/block-spacing/block-spacing.test.ts
diff --git a/packages/eslint-plugin/rules/block-spacing/block-spacing.ts b/packages/eslint-plugin/rules/block-spacing/block-spacing.ts
index 7974fea8..9eb989de 100644
--- a/packages/eslint-plugin/rules/block-spacing/block-spacing.ts
+++ b/packages/eslint-plugin/rules/block-spacing/block-spacing.ts
@@ -4,7 +4,7 @@ import { createRule } from '#utils/create-rule'
 import { AST_TOKEN_TYPES } from '@typescript-eslint/utils'
 import { isTokenOnSameLine } from '@typescript-eslint/utils/ast-utils'
 
-type SupportedNodes = Tree.BlockStatement | Tree.StaticBlock | Tree.SwitchStatement | Tree.TSInterfaceBody | Tree.TSTypeLiteral | Tree.TSEnumDeclaration
+type SupportedNodes = Tree.BlockStatement | Tree.StaticBlock | Tree.SwitchStatement
 
 export default createRule<RuleOptions, MessageIds>({
   name: 'block-spacing',
@@ -145,14 +145,6 @@ export default createRule<RuleOptions, MessageIds>({
       BlockStatement: checkSpacingInsideBraces,
       StaticBlock: checkSpacingInsideBraces,
       SwitchStatement: checkSpacingInsideBraces,
-
-      // This code worked "out of the box" for interface and type literal
-      // Enums were very close to match as well, the only reason they are not is that was that enums don't have a body node in the parser
-      // So the opening brace punctuator starts in the middle of the node - `getFirstToken` in
-      // the base rule did not filter for the first opening brace punctuator
-      TSInterfaceBody: checkSpacingInsideBraces,
-      TSTypeLiteral: checkSpacingInsideBraces,
-      TSEnumDeclaration: checkSpacingInsideBraces,
     }
   },
 })
diff --git a/packages/eslint-plugin/rules/object-curly-spacing/README.md b/packages/eslint-plugin/rules/object-curly-spacing/README.md
index 31faa4f8..8858ed9b 100644
--- a/packages/eslint-plugin/rules/object-curly-spacing/README.md
+++ b/packages/eslint-plugin/rules/object-curly-spacing/README.md
@@ -26,6 +26,15 @@ var { x, y } = y;
 // import/export declarations (EcmaScript 6)
 import { foo } from "bar";
 export { foo };
+
+// type literals
+type Foo = { bar: string };
+
+// interface
+interface Foo = { bar: string };
+
+// enum
+enum Foo = { Bar };
 ```
 
 ## Rule Details
@@ -63,6 +72,10 @@ var obj = { baz: {'foo': 'qux'}, bar};
 var obj = {baz: { 'foo': 'qux'}, bar};
 var {x } = y;
 import { foo } from 'bar';
+export { foo };
+type Foo = { bar: string };
+interface Foo = { bar: string };
+enum Foo = { Bar };
 ```
 
 :::
@@ -86,6 +99,10 @@ var obj = {
 var obj = {};
 var {x} = y;
 import {foo} from 'bar';
+export {foo};
+type Foo = {bar: string};
+interface Foo = {bar: string};
+enum Foo = {Bar};
 ```
 
 :::
@@ -109,6 +126,10 @@ var obj = {
   'foo':'bar'};
 var {x} = y;
 import {foo } from 'bar';
+export {foo };
+type Foo = {bar: string };
+interface Foo = {bar: string };
+enum Foo = {Bar };
 ```
 
 :::
@@ -128,6 +149,10 @@ var obj = {
 };
 var { x } = y;
 import { foo } from 'bar';
+export { foo };
+type Foo = { bar: string };
+interface Foo = { bar: string };
+enum Foo = { Bar };
 ```
 
 :::
diff --git a/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing.ts b/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing.ts
index 03fe6682..4409d8a0 100644
--- a/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing.ts
+++ b/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing.ts
@@ -162,22 +162,20 @@ export default createRule<RuleOptions, MessageIds>({
     /**
      * Determines if spacing in curly braces is valid.
      * @param node The AST node to check.
-     * @param first The first token to check (should be the opening brace)
-     * @param second The second token to check (should be first after the opening brace)
-     * @param penultimate The penultimate token to check (should be last before closing brace)
-     * @param last The last token to check (should be closing brace)
+     * @param openingToken The first token to check (should be the opening brace)
+     * @param closingToken The last token to check (should be closing brace)
      */
     function validateBraceSpacing(
-      node: Tree.ObjectExpression | Tree.ObjectPattern | Tree.ImportDeclaration | Tree.ExportNamedDeclaration | Tree.TSMappedType | Tree.TSTypeLiteral,
-      first: Tree.Token,
-      second: Tree.Token,
-      penultimate: Tree.Token,
-      last: Tree.Token,
+      node: ASTNode,
+      openingToken: Tree.Token,
+      closingToken: Tree.Token,
     ): void {
-      if (isTokenOnSameLine(first, second)) {
-        const firstSpaced = sourceCode.isSpaceBetween!(first, second)
+      const tokenAfterOpening = sourceCode.getTokenAfter(openingToken, { includeComments: true })!
+
+      if (isTokenOnSameLine(openingToken, tokenAfterOpening)) {
+        const firstSpaced = sourceCode.isSpaceBetween!(openingToken, tokenAfterOpening)
         const secondType = sourceCode.getNodeByRangeIndex(
-          second.range[0],
+          tokenAfterOpening.range[0],
         )!.type
 
         const openingCurlyBraceMustBeSpaced
@@ -190,25 +188,27 @@ export default createRule<RuleOptions, MessageIds>({
             : options.spaced
 
         if (openingCurlyBraceMustBeSpaced && !firstSpaced)
-          reportRequiredBeginningSpace(node, first)
+          reportRequiredBeginningSpace(node, openingToken)
 
         if (
           !openingCurlyBraceMustBeSpaced
           && firstSpaced
-          && second.type !== AST_TOKEN_TYPES.Line
+          && tokenAfterOpening.type !== AST_TOKEN_TYPES.Line
         ) {
-          reportNoBeginningSpace(node, first)
+          reportNoBeginningSpace(node, openingToken)
         }
       }
 
-      if (isTokenOnSameLine(penultimate, last)) {
+      const tokenBeforeClosing = sourceCode.getTokenBefore(closingToken, { includeComments: true })!
+
+      if (isTokenOnSameLine(tokenBeforeClosing, closingToken)) {
         const shouldCheckPenultimate
           = (options.arraysInObjectsException
-            && isClosingBracketToken(penultimate))
+            && isClosingBracketToken(tokenBeforeClosing))
           || (options.objectsInObjectsException
-            && isClosingBraceToken(penultimate))
+            && isClosingBraceToken(tokenBeforeClosing))
         const penultimateType = shouldCheckPenultimate
-          ? sourceCode.getNodeByRangeIndex(penultimate.range[0])!.type
+          ? sourceCode.getNodeByRangeIndex(tokenBeforeClosing.range[0])!.type
           : undefined
 
         const closingCurlyBraceMustBeSpaced
@@ -232,53 +232,30 @@ export default createRule<RuleOptions, MessageIds>({
             ? !options.spaced
             : options.spaced
 
-        const lastSpaced = sourceCode.isSpaceBetween!(penultimate, last)
+        const lastSpaced = sourceCode.isSpaceBetween!(tokenBeforeClosing, closingToken)
 
         if (closingCurlyBraceMustBeSpaced && !lastSpaced)
-          reportRequiredEndingSpace(node, last)
+          reportRequiredEndingSpace(node, closingToken)
 
         if (!closingCurlyBraceMustBeSpaced && lastSpaced)
-          reportNoEndingSpace(node, last)
+          reportNoEndingSpace(node, closingToken)
       }
     }
 
     /**
-     * Gets '}' token of an object node.
-     *
-     * Because the last token of object patterns might be a type annotation,
-     * this traverses tokens preceded by the last property, then returns the
-     * first '}' token.
-     * @param node The node to get. This node is an
-     *      ObjectExpression or an ObjectPattern. And this node has one or
-     *      more properties.
-     * @returns '}' token.
-     */
-    function getClosingBraceOfObject(
-      node: Tree.ObjectPattern | Tree.ObjectExpression | Tree.TSTypeLiteral,
-    ): Tree.Token | null {
-      const lastProperty = node.type === 'TSTypeLiteral'
-        ? node.members[node.members.length - 1]
-        : node.properties[node.properties.length - 1]
-
-      return sourceCode.getTokenAfter(lastProperty, isClosingBraceToken)
-    }
-
-    /**
-     * Reports a given object node if spacing in curly braces is invalid.
-     * @param node An ObjectExpression or ObjectPattern node to check.
+     * Reports a given object-like node if spacing in curly braces is invalid.
+     * @param node An object-like node to check.
+     * @param properties The properties of the object-like node
      */
-    function checkForObject(node:
-      | Tree.ObjectExpression
-      | Tree.ObjectPattern) {
-      if (node.properties.length === 0)
+    function checkForObjectLike(node: ASTNode, properties: ASTNode[]) {
+      if (properties.length === 0)
         return
 
-      const first = sourceCode.getFirstToken(node)!
-      const last = getClosingBraceOfObject(node)!
-      const second = sourceCode.getTokenAfter(first, { includeComments: true })!
-      const penultimate = sourceCode.getTokenBefore(last, { includeComments: true })!
+      const closeToken = sourceCode.getTokenAfter(properties.at(-1)!, isClosingBraceToken)!
 
-      validateBraceSpacing(node, first, second, penultimate, last)
+      const openingToken = sourceCode.getFirstToken(node)!
+
+      validateBraceSpacing(node, openingToken, closeToken)
     }
 
     /**
@@ -300,10 +277,8 @@ export default createRule<RuleOptions, MessageIds>({
 
       const first = sourceCode.getTokenBefore(firstSpecifier)!
       const last = sourceCode.getTokenAfter(lastSpecifier, isNotCommaToken)!
-      const second = sourceCode.getTokenAfter(first, { includeComments: true })!
-      const penultimate = sourceCode.getTokenBefore(last, { includeComments: true })!
 
-      validateBraceSpacing(node, first, second, penultimate, last)
+      validateBraceSpacing(node, first, last)
     }
 
     /**
@@ -318,47 +293,37 @@ export default createRule<RuleOptions, MessageIds>({
       const lastSpecifier = node.specifiers[node.specifiers.length - 1]
       const first = sourceCode.getTokenBefore(firstSpecifier)!
       const last = sourceCode.getTokenAfter(lastSpecifier, isNotCommaToken)!
-      const second = sourceCode.getTokenAfter(first, { includeComments: true })!
-      const penultimate = sourceCode.getTokenBefore(last, { includeComments: true })!
 
-      validateBraceSpacing(node, first, second, penultimate, last)
+      validateBraceSpacing(node, first, last)
     }
 
     return {
       // var {x} = y;
-      ObjectPattern: checkForObject,
+      ObjectPattern(node) {
+        checkForObjectLike(node, node.properties)
+      },
       // var y = {x: 'y'}
-      ObjectExpression: checkForObject,
+      ObjectExpression(node) {
+        checkForObjectLike(node, node.properties)
+      },
       // import {y} from 'x';
       ImportDeclaration: checkForImport,
       // export {name} from 'yo';
       ExportNamedDeclaration: checkForExport,
-      TSMappedType(node: Tree.TSMappedType): void {
-        const first = sourceCode.getFirstToken(node)!
-        const last = sourceCode.getLastToken(node)!
-        const second = sourceCode.getTokenAfter(first, {
-          includeComments: true,
-        })!
-        const penultimate = sourceCode.getTokenBefore(last, {
-          includeComments: true,
-        })!
-
-        validateBraceSpacing(node, first, second, penultimate, last)
+      TSMappedType(node) {
+        const openingToken = sourceCode.getFirstToken(node)!
+        const closeToken = sourceCode.getLastToken(node)!
+
+        validateBraceSpacing(node, openingToken, closeToken)
+      },
+      TSTypeLiteral(node) {
+        checkForObjectLike(node, node.members)
+      },
+      TSInterfaceBody(node) {
+        checkForObjectLike(node, node.body)
       },
-      TSTypeLiteral(node: Tree.TSTypeLiteral): void {
-        if (node.members.length === 0)
-          return
-
-        const first = sourceCode.getFirstToken(node)!
-        const last = getClosingBraceOfObject(node)!
-        const second = sourceCode.getTokenAfter(first, {
-          includeComments: true,
-        })!
-        const penultimate = sourceCode.getTokenBefore(last, {
-          includeComments: true,
-        })!
-
-        validateBraceSpacing(node, first, second, penultimate, last)
+      TSEnumBody(node) {
+        checkForObjectLike(node, node.members)
       },
     }
   },


Test patch:
diff --git a/packages/eslint-plugin/rules/block-spacing/block-spacing._ts_.test.ts b/packages/eslint-plugin/rules/block-spacing/block-spacing._ts_.test.ts
deleted file mode 100644
index d09ce5c0..00000000
--- a/packages/eslint-plugin/rules/block-spacing/block-spacing._ts_.test.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import type { InvalidTestCase, ValidTestCase } from '#test'
-import type { MessageIds, RuleOptions } from './types'
-import { run } from '#test'
-import { AST_NODE_TYPES } from '@typescript-eslint/utils'
-import rule from './block-spacing'
-
-const options = ['always', 'never'] as const
-const typeDeclarations = [
-  {
-    nodeType: AST_NODE_TYPES.TSInterfaceBody,
-    stringPrefix: 'interface Foo ',
-  },
-  {
-    nodeType: AST_NODE_TYPES.TSTypeLiteral,
-    stringPrefix: 'type Foo = ',
-  },
-  {
-    nodeType: AST_NODE_TYPES.TSEnumDeclaration,
-    stringPrefix: 'enum Foo ',
-  },
-  {
-    nodeType: AST_NODE_TYPES.TSEnumDeclaration,
-    stringPrefix: 'const enum Foo ',
-  },
-]
-const emptyBlocks = ['{}', '{ }']
-const singlePropertyBlocks = ['{bar: true}', '{ bar: true }']
-const blockComment = '/* comment */'
-
-run<RuleOptions, MessageIds>({
-  name: 'block-spacing',
-  rule,
-  valid: [
-    // Empty blocks don't apply
-    ...options.flatMap(option =>
-      typeDeclarations.flatMap<ValidTestCase<RuleOptions>>(typeDec =>
-        emptyBlocks.map<ValidTestCase<RuleOptions>>(blockType => ({
-          code: typeDec.stringPrefix + blockType,
-          options: [option],
-        })),
-      ),
-    ),
-    ...typeDeclarations.flatMap<ValidTestCase<RuleOptions>>(
-      (typeDec) => {
-        const property
-          = typeDec.nodeType === AST_NODE_TYPES.TSEnumDeclaration
-            ? 'bar = 1'
-            : 'bar: true;'
-        return [
-          {
-            code: `${typeDec.stringPrefix}{ /* comment */ ${property} /* comment */ } // always`,
-            options: ['always'],
-          },
-          {
-            code: `${typeDec.stringPrefix}{/* comment */ ${property} /* comment */} // never`,
-            options: ['never'],
-          },
-          {
-            code: `${typeDec.stringPrefix}{ //comment\n ${property}}`,
-            options: ['never'],
-          },
-        ]
-      },
-    ),
-  ],
-  invalid: [
-    ...options.flatMap(option =>
-      typeDeclarations.flatMap((typeDec) => {
-        return singlePropertyBlocks.flatMap<InvalidTestCase<RuleOptions, MessageIds>>(
-          (blockType, blockIndex) => {
-            // These are actually valid, so filter them out
-            if (
-              (option === 'always' && blockType.startsWith('{ '))
-              || (option === 'never' && blockType.startsWith('{bar'))
-            ) {
-              return []
-            }
-
-            const reverseBlockType = singlePropertyBlocks[1 - blockIndex]
-            let code = `${typeDec.stringPrefix}${blockType};  /* ${option} */`
-            let output = `${typeDec.stringPrefix}${reverseBlockType};  /* ${option} */`
-            if (typeDec.nodeType === AST_NODE_TYPES.TSEnumDeclaration) {
-              output = output.replace(':', '=')
-              code = code.replace(':', '=')
-            }
-
-            return {
-              code,
-              options: [option],
-              output,
-              errors: [
-                {
-                  type: typeDec.nodeType,
-                  messageId: option === 'always' ? 'missing' : 'extra',
-                  data: { location: 'after', token: '{' },
-                },
-                {
-                  type: typeDec.nodeType,
-                  messageId: option === 'always' ? 'missing' : 'extra',
-                  data: { location: 'before', token: '}' },
-                },
-              ],
-            }
-          },
-        )
-      }),
-    ),
-    // With block comments
-    ...options.flatMap(option =>
-      typeDeclarations.flatMap <InvalidTestCase<RuleOptions, MessageIds>>((typeDec) => {
-        const property
-          = typeDec.nodeType === AST_NODE_TYPES.TSEnumDeclaration
-            ? 'bar = 1'
-            : 'bar: true;'
-        const alwaysSpace = option === 'always' ? '' : ' '
-        const neverSpace = option === 'always' ? ' ' : ''
-        return [
-          {
-            code: `${typeDec.stringPrefix}{${alwaysSpace}${blockComment}${property}${blockComment}${alwaysSpace}}  /* ${option} */`,
-            output: `${typeDec.stringPrefix}{${neverSpace}${blockComment}${property}${blockComment}${neverSpace}}  /* ${option} */`,
-            options: [option],
-            errors: [
-              {
-                type: typeDec.nodeType,
-                messageId: option === 'always' ? 'missing' : 'extra',
-                data: { location: 'after', token: '{' },
-              },
-              {
-                type: typeDec.nodeType,
-                messageId: option === 'always' ? 'missing' : 'extra',
-                data: { location: 'before', token: '}' },
-              },
-            ],
-          },
-        ]
-      }),
-    ),
-  ],
-},
-)
diff --git a/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing._ts_.test.ts b/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing._ts_.test.ts
index 04731c88..57328dd2 100644
--- a/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing._ts_.test.ts
+++ b/packages/eslint-plugin/rules/object-curly-spacing/object-curly-spacing._ts_.test.ts
@@ -808,6 +808,35 @@ run<RuleOptions, MessageIds>({
       code: 'const x:{[key: string]: [number]}',
       options: ['always', { arraysInObjects: false }],
     },
+
+    // default - TSInterfaceBody
+    {
+      code: 'interface x {f: number}',
+    },
+    // always - TSInterfaceBody
+    {
+      code: 'interface x { f: number }',
+      options: ['always'],
+    },
+    // never - TSInterfaceBody
+    {
+      code: 'interface x {f: number}',
+      options: ['never'],
+    },
+    // default - TSEnumBody
+    {
+      code: 'enum Foo {ONE, TWO,}',
+    },
+    // always - TSEnumBody
+    {
+      code: 'enum Foo { ONE, TWO = 2 }',
+      options: ['always'],
+    },
+    // never - TSEnumBody
+    {
+      code: 'enum Foo {ONE, TWO,}',
+      options: ['never'],
+    },
   ],
 
   invalid: [
@@ -2129,5 +2158,41 @@ run<RuleOptions, MessageIds>({
         { messageId: 'unexpectedSpaceBefore' },
       ],
     },
+    // TSInterfaceBody
+    {
+      code: 'interface x { f: number }',
+      output: 'interface x {f: number}',
+      errors: [
+        { messageId: 'unexpectedSpaceAfter' },
+        { messageId: 'unexpectedSpaceBefore' },
+      ],
+    },
+    {
+      code: 'interface x {f: number}',
+      output: 'interface x { f: number }',
+      options: ['always'],
+      errors: [
+        { messageId: 'requireSpaceAfter' },
+        { messageId: 'requireSpaceBefore' },
+      ],
+    },
+    // TSEnumBody
+    {
+      code: 'enum Foo { ONE, TWO = 2 }',
+      output: 'enum Foo {ONE, TWO = 2}',
+      errors: [
+        { messageId: 'unexpectedSpaceAfter' },
+        { messageId: 'unexpectedSpaceBefore' },
+      ],
+    },
+    {
+      code: 'enum Foo {ONE, TWO,}',
+      output: 'enum Foo { ONE, TWO, }',
+      options: ['always'],
+      errors: [
+        { messageId: 'requireSpaceAfter' },
+        { messageId: 'requireSpaceBefore' },
+      ],
+    },
   ],
 })