diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 37a27ca9511f..48e1cca75aa6 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -217,6 +217,7 @@ In these cases, we create what we call an extension rule; a rule within our plug | [`@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-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-infix-ops.md b/packages/eslint-plugin/docs/rules/space-infix-ops.md new file mode 100644 index 000000000000..da36f25726e2 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/space-infix-ops.md @@ -0,0 +1,26 @@ +# This rule is aimed at ensuring there are spaces around infix operators. (`space-infix-ops`) + +This rule extends the base [`eslint/space-infix-ops`](https://eslint.org/docs/rules/space-infix-ops) rule. + +It also add support for enum members + +```ts +enum MyEnum { + KEY = 'value', +} +``` + +## How to use + +```jsonc +{ + "space-infix-ops": "off", + "@typescript-eslint/space-infix-ops": ["error", { "int32Hint": false }] +} +``` + +## Options + +See [`eslint/space-infix-ops` options](https://eslint.org/docs/rules/space-infix-ops#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/space-infix-ops.md) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 84b4e93fc90c..17dc532dafcf 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -132,6 +132,8 @@ export = { '@typescript-eslint/semi': 'error', 'space-before-function-paren': 'off', '@typescript-eslint/space-before-function-paren': 'error', + 'space-infix-ops': 'off', + '@typescript-eslint/space-infix-ops': '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 8b5049e25022..1b9b062b3998 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -14,6 +14,7 @@ import consistentTypeDefinitions from './consistent-type-definitions'; import consistentTypeImports from './consistent-type-imports'; import defaultParamLast from './default-param-last'; import dotNotation from './dot-notation'; +import enumMembersSpacing from './space-infix-ops'; import explicitFunctionReturnType from './explicit-function-return-type'; import explicitMemberAccessibility from './explicit-member-accessibility'; import explicitModuleBoundaryTypes from './explicit-module-boundary-types'; @@ -122,6 +123,7 @@ export default { 'consistent-type-imports': consistentTypeImports, 'default-param-last': defaultParamLast, 'dot-notation': dotNotation, + 'space-infix-ops': enumMembersSpacing, 'explicit-function-return-type': explicitFunctionReturnType, 'explicit-member-accessibility': explicitMemberAccessibility, 'explicit-module-boundary-types': explicitModuleBoundaryTypes, diff --git a/packages/eslint-plugin/src/rules/space-infix-ops.ts b/packages/eslint-plugin/src/rules/space-infix-ops.ts new file mode 100644 index 000000000000..2211324492f7 --- /dev/null +++ b/packages/eslint-plugin/src/rules/space-infix-ops.ts @@ -0,0 +1,100 @@ +import { + AST_TOKEN_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/space-infix-ops'; +import * as util from '../util'; + +export type Options = util.InferOptionsTypeFromRule; +export type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'space-infix-ops', + meta: { + type: 'layout', + docs: { + description: + 'This rule is aimed at ensuring there are spaces around infix operators.', + category: 'Stylistic Issues', + recommended: false, + extendsBaseRule: true, + }, + fixable: baseRule.meta.fixable, + schema: baseRule.meta.schema, + messages: baseRule.meta.messages, + }, + defaultOptions: [ + { + int32Hint: false, + }, + ], + create(context) { + const rules = baseRule.create(context); + const sourceCode = context.getSourceCode(); + + /** + * Check if it has an assignment char and report if it's faulty + * @param node The node to report + */ + function checkForAssignmentSpace(node: TSESTree.TSEnumMember): void { + if (!node.initializer) { + return; + } + + const leftNode = sourceCode.getTokenByRangeStart(node.id.range[0])!; + const rightNode = sourceCode.getTokenByRangeStart( + node.initializer.range[0], + )!; + + if (!rightNode) { + return; + } + + const operator = sourceCode.getFirstTokenBetween( + leftNode, + rightNode, + token => + token.type === AST_TOKEN_TYPES.Punctuator && token.value === '=', + ); + const prev = sourceCode.getTokenBefore(operator!); + const next = sourceCode.getTokenAfter(operator!); + + if ( + operator && + (!sourceCode.isSpaceBetweenTokens(prev!, operator) || + !sourceCode.isSpaceBetweenTokens(operator, next!)) + ) { + context.report({ + node: node, + loc: operator.loc, + messageId: 'missingSpace', + data: { + operator: operator.value, + }, + fix(fixer) { + const previousToken = sourceCode.getTokenBefore(operator); + const afterToken = sourceCode.getTokenAfter(operator); + let fixString = ''; + + if (operator.range[0] - previousToken!.range[1] === 0) { + fixString = ' '; + } + + fixString += operator.value; + + if (afterToken!.range[0] - operator.range[1] === 0) { + fixString += ' '; + } + + return fixer.replaceText(operator, fixString); + }, + }); + } + } + + return { + ...rules, + TSEnumMember: checkForAssignmentSpace, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/space-infix-ops.test.ts b/packages/eslint-plugin/tests/rules/space-infix-ops.test.ts new file mode 100644 index 000000000000..0c90443ca4fc --- /dev/null +++ b/packages/eslint-plugin/tests/rules/space-infix-ops.test.ts @@ -0,0 +1,102 @@ +/* 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-infix-ops'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('space-infix-ops', rule, { + valid: [ + { + code: ` + enum Test { + KEY1 = 2, + } + `, + }, + { + code: ` + enum Test { + KEY1 = "value", + } + `, + }, + { + code: ` + enum Test { + KEY1, + } + `, + }, + ], + invalid: [ + { + code: ` + enum Test { + A= 2, + B = 1, + } + `, + output: ` + enum Test { + A = 2, + B = 1, + } + `, + errors: [ + { + messageId: 'missingSpace', + column: 12, + line: 3, + }, + ], + }, + { + code: ` + enum Test { + KEY1= "value1", + KEY2 = "value2", + } + `, + output: ` + enum Test { + KEY1 = "value1", + KEY2 = "value2", + } + `, + errors: [ + { + messageId: 'missingSpace', + column: 15, + line: 3, + }, + ], + }, + { + code: ` + enum Test { + A =2, + B = 1, + } + `, + output: ` + enum Test { + A = 2, + B = 1, + } + `, + errors: [ + { + messageId: 'missingSpace', + column: 13, + line: 3, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 39d49db7328e..0a43256cbaed 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -762,3 +762,25 @@ declare module 'eslint/lib/rules/comma-dangle' { >; export = rule; } + +declare module 'eslint/lib/rules/space-infix-ops' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'missingSpace', + [ + { + int32Hint: boolean; + }, + ], + { + AssignmentExpression(node: TSESTree.AssignmentExpression): void; + AssignmentPattern(node: TSESTree.AssignmentPattern): void; + BinaryExpression(node: TSESTree.BinaryExpression): void; + LogicalExpression(node: TSESTree.LogicalExpression): void; + ConditionalExpression(node: TSESTree.ConditionalExpression): void; + VariableDeclarator(node: TSESTree.VariableDeclarator): void; + } + >; + export = rule; +}