From 208b6d02252dff2bf272329d3e4a4a82e56c52c0 Mon Sep 17 00:00:00 2001 From: Domas Trijonis Date: Thu, 24 Feb 2022 16:41:53 +0100 Subject: [PATCH] feat(eslint-plugin): add extension rule `space-before-blocks` (#1606) (#4184) Co-authored-by: Josh Goldberg --- packages/eslint-plugin/README.md | 1 + .../docs/rules/space-before-blocks.md | 60 ++++ packages/eslint-plugin/src/configs/all.ts | 2 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/space-before-blocks.ts | 90 ++++++ .../src/util/getESLintCoreRule.ts | 1 + .../tests/rules/space-before-blocks.test.ts | 258 ++++++++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 23 ++ packages/utils/src/ast-utils/misc.ts | 4 +- 9 files changed, 439 insertions(+), 2 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/space-before-blocks.md create mode 100644 packages/eslint-plugin/src/rules/space-before-blocks.ts create mode 100644 packages/eslint-plugin/tests/rules/space-before-blocks.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index ec14afa8c90..34209e621cb 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -231,6 +231,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 00000000000..52f8a121df6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/space-before-blocks.md @@ -0,0 +1,60 @@ +# `space-before-blocks` + +Enforces consistent spacing 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 8b63b936b4a..03188b72464 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -157,6 +157,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 3249559746a..c2f90319e38 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -112,6 +112,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'; @@ -237,6 +238,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 00000000000..5bf827df96c --- /dev/null +++ b/packages/eslint-plugin/src/rules/space-before-blocks.ts @@ -0,0 +1,90 @@ +import { TSESTree } from '@typescript-eslint/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.isSpaceBetweenTokens( + precedingToken, + node as TSESTree.Token, + ); + + 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 f61bdb0c309..5ba9ae36965 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 00000000000..7e1338c6fc5 --- /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 d53ef43a747..fa7d5934b26 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -842,6 +842,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/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/utils'; diff --git a/packages/utils/src/ast-utils/misc.ts b/packages/utils/src/ast-utils/misc.ts index 923424d9f92..72a7898c31c 100644 --- a/packages/utils/src/ast-utils/misc.ts +++ b/packages/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; }