diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md
index 47012b81b8fc..16b4649fa7b2 100644
--- a/packages/eslint-plugin/README.md
+++ b/packages/eslint-plugin/README.md
@@ -229,6 +229,7 @@ In these cases, we create what we call an extension rule; a rule within our plug
| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :white_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | |
+| [`@typescript-eslint/space-before-blocks`](./docs/rules/space-before-blocks.md) | Enforces consistent spacing before blocks | | :wrench: | |
| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | |
| [`@typescript-eslint/space-infix-ops`](./docs/rules/space-infix-ops.md) | This rule is aimed at ensuring there are spaces around infix operators. | | :wrench: | |
diff --git a/packages/eslint-plugin/docs/rules/space-before-blocks.md b/packages/eslint-plugin/docs/rules/space-before-blocks.md
new file mode 100644
index 000000000000..6f09a0f5568b
--- /dev/null
+++ b/packages/eslint-plugin/docs/rules/space-before-blocks.md
@@ -0,0 +1,58 @@
+# Enforces consistent spacing before blocks (`space-before-blocks`)
+
+## Rule Details
+
+This rule extends the base [`eslint/space-before-blocks`](https://eslint.org/docs/rules/space-before-blocks) rule.
+It adds support for interfaces and enums:
+
+### ❌ Incorrect
+
+```ts
+enum Breakpoint{
+ Large, Medium;
+}
+
+interface State{
+ currentBreakpoint: Breakpoint;
+}
+```
+
+### ✅ Correct
+
+```ts
+enum Breakpoint {
+ Large, Medium;
+}
+
+interface State {
+ currentBreakpoint: Breakpoint;
+}
+```
+
+In case a more specific options object is passed these blocks will follow `classes` configuration option.
+
+## How to Use
+
+```jsonc
+{
+ // note you must disable the base rule as it can report incorrect errors
+ "space-before-blocks": "off",
+ "@typescript-eslint/space-before-blocks": ["error"]
+}
+```
+
+## Options
+
+See [`eslint/space-before-blocks` options](https://eslint.org/docs/rules/space-before-blocks#options).
+
+
+
+Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/space-before-blocks.md)
+
+
+
+## Attributes
+
+- [ ] ✅ Recommended
+- [x] 🔧 Fixable
+- [ ] 💭 Requires type information
diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts
index 538be53ce583..dd5df10fb7f5 100644
--- a/packages/eslint-plugin/src/configs/all.ts
+++ b/packages/eslint-plugin/src/configs/all.ts
@@ -155,6 +155,8 @@ export = {
'@typescript-eslint/space-before-function-paren': 'error',
'space-infix-ops': 'off',
'@typescript-eslint/space-infix-ops': 'error',
+ 'space-before-blocks': 'off',
+ '@typescript-eslint/space-before-blocks': 'error',
'@typescript-eslint/strict-boolean-expressions': 'error',
'@typescript-eslint/switch-exhaustiveness-check': 'error',
'@typescript-eslint/triple-slash-reference': 'error',
diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts
index 83e76f0a23d0..0ff454868edd 100644
--- a/packages/eslint-plugin/src/rules/index.ts
+++ b/packages/eslint-plugin/src/rules/index.ts
@@ -110,6 +110,7 @@ import restrictTemplateExpressions from './restrict-template-expressions';
import returnAwait from './return-await';
import semi from './semi';
import sortTypeUnionIntersectionMembers from './sort-type-union-intersection-members';
+import spaceBeforeBlocks from './space-before-blocks';
import spaceBeforeFunctionParen from './space-before-function-paren';
import spaceInfixOps from './space-infix-ops';
import strictBooleanExpressions from './strict-boolean-expressions';
@@ -233,6 +234,7 @@ export default {
'return-await': returnAwait,
semi: semi,
'sort-type-union-intersection-members': sortTypeUnionIntersectionMembers,
+ 'space-before-blocks': spaceBeforeBlocks,
'space-before-function-paren': spaceBeforeFunctionParen,
'space-infix-ops': spaceInfixOps,
'strict-boolean-expressions': strictBooleanExpressions,
diff --git a/packages/eslint-plugin/src/rules/space-before-blocks.ts b/packages/eslint-plugin/src/rules/space-before-blocks.ts
new file mode 100644
index 000000000000..3c080391d430
--- /dev/null
+++ b/packages/eslint-plugin/src/rules/space-before-blocks.ts
@@ -0,0 +1,87 @@
+import { TSESTree } from '@typescript-eslint/experimental-utils';
+import { getESLintCoreRule } from '../util/getESLintCoreRule';
+import * as util from '../util';
+
+const baseRule = getESLintCoreRule('space-before-blocks');
+
+export type Options = util.InferOptionsTypeFromRule;
+export type MessageIds = util.InferMessageIdsTypeFromRule;
+
+export default util.createRule({
+ name: 'space-before-blocks',
+ meta: {
+ type: 'layout',
+ docs: {
+ description: 'Enforces consistent spacing before blocks',
+ recommended: false,
+ extendsBaseRule: true,
+ },
+ fixable: baseRule.meta.fixable,
+ hasSuggestions: baseRule.meta.hasSuggestions,
+ schema: baseRule.meta.schema,
+ messages: {
+ // @ts-expect-error -- we report on this messageId so we need to ensure it's there in case ESLint changes in future
+ unexpectedSpace: 'Unexpected space before opening brace.',
+ // @ts-expect-error -- we report on this messageId so we need to ensure it's there in case ESLint changes in future
+ missingSpace: 'Missing space before opening brace.',
+ ...baseRule.meta.messages,
+ },
+ },
+ defaultOptions: ['always'],
+ create(context) {
+ const rules = baseRule.create(context);
+ const config = context.options[0];
+ const sourceCode = context.getSourceCode();
+
+ let requireSpace = true;
+
+ if (typeof config === 'object') {
+ requireSpace = config.classes === 'always';
+ } else if (config === 'never') {
+ requireSpace = false;
+ }
+
+ function checkPrecedingSpace(
+ node: TSESTree.Token | TSESTree.TSInterfaceBody,
+ ): void {
+ const precedingToken = sourceCode.getTokenBefore(node);
+ if (precedingToken && util.isTokenOnSameLine(precedingToken, node)) {
+ const hasSpace = sourceCode.isSpaceBetween?.(precedingToken, node);
+
+ if (requireSpace && !hasSpace) {
+ context.report({
+ node,
+ messageId: 'missingSpace',
+ fix(fixer) {
+ return fixer.insertTextBefore(node, ' ');
+ },
+ });
+ } else if (!requireSpace && hasSpace) {
+ context.report({
+ node,
+ messageId: 'unexpectedSpace',
+ fix(fixer) {
+ return fixer.removeRange([
+ precedingToken.range[1],
+ node.range[0],
+ ]);
+ },
+ });
+ }
+ }
+ }
+
+ function checkSpaceAfterEnum(node: TSESTree.TSEnumDeclaration): void {
+ const punctuator = sourceCode.getTokenAfter(node.id);
+ if (punctuator) {
+ checkPrecedingSpace(punctuator);
+ }
+ }
+
+ return {
+ ...rules,
+ TSEnumDeclaration: checkSpaceAfterEnum,
+ TSInterfaceBody: checkPrecedingSpace,
+ };
+ },
+});
diff --git a/packages/eslint-plugin/src/util/getESLintCoreRule.ts b/packages/eslint-plugin/src/util/getESLintCoreRule.ts
index 7c2427c8fcb9..92852db118aa 100644
--- a/packages/eslint-plugin/src/util/getESLintCoreRule.ts
+++ b/packages/eslint-plugin/src/util/getESLintCoreRule.ts
@@ -33,6 +33,7 @@ interface RuleMap {
'prefer-const': typeof import('eslint/lib/rules/prefer-const');
quotes: typeof import('eslint/lib/rules/quotes');
semi: typeof import('eslint/lib/rules/semi');
+ 'space-before-blocks': typeof import('eslint/lib/rules/space-before-blocks');
'space-infix-ops': typeof import('eslint/lib/rules/space-infix-ops');
strict: typeof import('eslint/lib/rules/strict');
}
diff --git a/packages/eslint-plugin/tests/rules/space-before-blocks.test.ts b/packages/eslint-plugin/tests/rules/space-before-blocks.test.ts
new file mode 100644
index 000000000000..7e1338c6fc5d
--- /dev/null
+++ b/packages/eslint-plugin/tests/rules/space-before-blocks.test.ts
@@ -0,0 +1,258 @@
+/* eslint-disable eslint-comments/no-use */
+// this rule tests spacing, which prettier will want to fix and break the tests
+/* eslint "@typescript-eslint/internal/plugin-test-formatting": ["error", { formatWithPrettier: false }] */
+/* eslint-enable eslint-comments/no-use */
+
+import rule from '../../src/rules/space-before-blocks';
+import { RuleTester } from '../RuleTester';
+
+const ruleTester = new RuleTester({
+ parser: '@typescript-eslint/parser',
+});
+
+ruleTester.run('space-before-blocks', rule, {
+ valid: [
+ {
+ code: `
+ enum Test{
+ KEY1 = 2,
+ }
+ `,
+ options: ['never'],
+ },
+ {
+ code: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ options: ['never'],
+ },
+ {
+ code: `
+ enum Test {
+ KEY1 = 2,
+ }
+ `,
+ options: ['always'],
+ },
+ {
+ code: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ options: ['always'],
+ },
+ {
+ code: `
+ enum Test{
+ KEY1 = 2,
+ }
+ `,
+ options: [{ classes: 'never' }],
+ },
+ {
+ code: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ options: [{ classes: 'never' }],
+ },
+ {
+ code: `
+ enum Test {
+ KEY1 = 2,
+ }
+ `,
+ options: [{ classes: 'always' }],
+ },
+ {
+ code: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ options: [{ classes: 'always' }],
+ },
+ {
+ code: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ options: [{ classes: 'off' }],
+ },
+ ],
+ invalid: [
+ {
+ code: `
+ enum Test{
+ A = 2,
+ B = 1,
+ }
+ `,
+ output: `
+ enum Test {
+ A = 2,
+ B = 1,
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSpace',
+ column: 18,
+ line: 2,
+ },
+ ],
+ options: ['always'],
+ },
+ {
+ code: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ output: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSpace',
+ column: 23,
+ line: 2,
+ },
+ ],
+ options: ['always'],
+ },
+ {
+ code: `
+ enum Test{
+ A = 2,
+ B = 1,
+ }
+ `,
+ output: `
+ enum Test {
+ A = 2,
+ B = 1,
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSpace',
+ column: 18,
+ line: 2,
+ },
+ ],
+ options: [{ classes: 'always' }],
+ },
+ {
+ code: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ output: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'missingSpace',
+ column: 23,
+ line: 2,
+ },
+ ],
+ options: [{ classes: 'always' }],
+ },
+ {
+ code: `
+ enum Test {
+ A = 2,
+ B = 1,
+ }
+ `,
+ output: `
+ enum Test{
+ A = 2,
+ B = 1,
+ }
+ `,
+ errors: [
+ {
+ messageId: 'unexpectedSpace',
+ column: 19,
+ line: 2,
+ },
+ ],
+ options: ['never'],
+ },
+ {
+ code: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ output: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'unexpectedSpace',
+ column: 24,
+ line: 2,
+ },
+ ],
+ options: ['never'],
+ },
+ {
+ code: `
+ enum Test {
+ A = 2,
+ B = 1,
+ }
+ `,
+ output: `
+ enum Test{
+ A = 2,
+ B = 1,
+ }
+ `,
+ errors: [
+ {
+ messageId: 'unexpectedSpace',
+ column: 19,
+ line: 2,
+ },
+ ],
+ options: [{ classes: 'never' }],
+ },
+ {
+ code: `
+ interface Test {
+ prop1: number;
+ }
+ `,
+ output: `
+ interface Test{
+ prop1: number;
+ }
+ `,
+ errors: [
+ {
+ messageId: 'unexpectedSpace',
+ column: 24,
+ line: 2,
+ },
+ ],
+ options: [{ classes: 'never' }],
+ },
+ ],
+});
diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts
index 44b679281fbd..9db018641508 100644
--- a/packages/eslint-plugin/typings/eslint-rules.d.ts
+++ b/packages/eslint-plugin/typings/eslint-rules.d.ts
@@ -827,6 +827,29 @@ declare module 'eslint/lib/rules/space-infix-ops' {
export = rule;
}
+declare module 'eslint/lib/rules/space-before-blocks' {
+ import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
+
+ const rule: TSESLint.RuleModule<
+ 'missingSpace' | 'unexpectedSpace',
+ [
+ | 'always'
+ | 'never'
+ | {
+ classes?: 'always' | 'never' | 'off';
+ functions?: 'always' | 'never' | 'off';
+ keywords?: 'always' | 'never' | 'off';
+ },
+ ],
+ {
+ BlockStatement(node: TSESTree.BlockStatement): void;
+ ClassBody(node: TSESTree.ClassBody): void;
+ SwitchStatement(node: TSESTree.SwitchStatement): void;
+ }
+ >;
+ export = rule;
+}
+
declare module 'eslint/lib/rules/prefer-const' {
import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
diff --git a/packages/experimental-utils/src/ast-utils/misc.ts b/packages/experimental-utils/src/ast-utils/misc.ts
index 923424d9f928..72a7898c31c0 100644
--- a/packages/experimental-utils/src/ast-utils/misc.ts
+++ b/packages/experimental-utils/src/ast-utils/misc.ts
@@ -6,8 +6,8 @@ const LINEBREAK_MATCHER = /\r\n|[\r\n\u2028\u2029]/;
* Determines whether two adjacent tokens are on the same line
*/
function isTokenOnSameLine(
- left: TSESTree.Token,
- right: TSESTree.Token,
+ left: TSESTree.Node | TSESTree.Token,
+ right: TSESTree.Node | TSESTree.Token,
): boolean {
return left.loc.end.line === right.loc.start.line;
}