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..62c6a982f451 --- /dev/null +++ b/packages/eslint-plugin/src/rules/space-before-blocks.ts @@ -0,0 +1,147 @@ +import { + AST_NODE_TYPES, + 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: 'enforce consistent spacing before blocks.', + recommended: false, + extendsBaseRule: true, + }, + fixable: baseRule.meta.fixable, + hasSuggestions: baseRule.meta.hasSuggestions, + schema: [ + { + oneOf: [ + { + enum: ['always', 'never'], + }, + { + type: 'object', + properties: { + keywords: { + enum: ['always', 'never', 'off'], + }, + functions: { + enum: ['always', 'never', 'off'], + }, + classes: { + enum: ['always', 'never', 'off'], + }, + enums: { + enum: ['always', 'never', 'off'], + }, + interfaces: { + enum: ['always', 'never', 'off'], + }, + }, + additionalProperties: false, + }, + ], + }, + ], + 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 alwaysEnums = true, + alwaysInterfaces = true, + neverEnums = false, + neverInterfaces = false; + + if (typeof config === 'object') { + alwaysEnums = config.enums === 'always'; + alwaysInterfaces = config.interfaces === 'always'; + neverEnums = config.enums === 'never'; + neverInterfaces = config.interfaces === 'never'; + } else if (config === 'never') { + alwaysEnums = false; + alwaysInterfaces = false; + neverEnums = true; + neverInterfaces = true; + } + + function checkPrecedingSpace( + node: + | TSESTree.Token + | TSESTree.TSInterfaceBody + | TSESTree.PunctuatorToken, + ) { + const precedingToken = sourceCode.getTokenBefore(node); + if ( + precedingToken && + util.isTokenOnSameLine(precedingToken, node as TSESTree.Token) + ) { + const hasSpace = sourceCode.isSpaceBetweenTokens( + precedingToken, + node as TSESTree.Token, + ); + let requireSpace: boolean; + let requireNoSpace: boolean; + + if (node.type === AST_NODE_TYPES.TSInterfaceBody) { + requireSpace = alwaysInterfaces; + requireNoSpace = neverInterfaces; + } else { + requireSpace = alwaysEnums; + requireNoSpace = neverEnums; + } + + if (requireSpace && !hasSpace) { + context.report({ + node, + messageId: 'missingSpace', + fix(fixer) { + return fixer.insertTextBefore(node, ' '); + }, + }); + } else if (requireNoSpace && hasSpace) { + context.report({ + node, + messageId: 'unexpectedSpace', + fix(fixer) { + return fixer.removeRange([ + precedingToken.range[1], + node.range[0], + ]); + }, + }); + } + } + } + + function checkSpaceAfterEnum(node: TSESTree.TSEnumDeclaration) { + 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 1701b4ef1d76..0e298dcd90f6 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..8a5d3681b0e4 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/space-before-blocks.test.ts @@ -0,0 +1,250 @@ +/* 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: [{ enums: 'never' }], + }, + { + code: ` + interface Test{ + prop1: number; + } + `, + options: [{ interfaces: 'never' }], + }, + { + code: ` + enum Test { + KEY1 = 2, + } + `, + options: [{ enums: 'always' }], + }, + { + code: ` + interface Test { + prop1: number; + } + `, + options: [{ interfaces: 'always' }], + }, + ], + 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: [{ enums: 'always' }], + }, + { + code: ` + interface Test{ + prop1: number; + } + `, + output: ` + interface Test { + prop1: number; + } + `, + errors: [ + { + messageId: 'missingSpace', + column: 23, + line: 2, + }, + ], + options: [{ interfaces: '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: [{ enums: 'never' }], + }, + { + code: ` + interface Test { + prop1: number; + } + `, + output: ` + interface Test{ + prop1: number; + } + `, + errors: [ + { + messageId: 'unexpectedSpace', + column: 24, + line: 2, + }, + ], + options: [{ interfaces: 'never' }], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 44b679281fbd..2cf8cf4afdd3 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -827,6 +827,31 @@ 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'; + enums?: 'always' | 'never'; + functions?: 'always' | 'never'; + interfaces?: 'always' | 'never'; + keywords?: 'always' | 'never'; + }, + ], + { + 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';