From 7df3395070cb08630c8f433b87b8723c0fcd9e19 Mon Sep 17 00:00:00 2001 From: Dominique Richard Date: Sat, 19 Sep 2020 00:26:28 -0400 Subject: [PATCH] feat(eslint-plugin): [space-infix-ops] extention rule --- packages/eslint-plugin/README.md | 1 + .../docs/rules/space-infix-ops.md | 18 +++ packages/eslint-plugin/src/configs/all.ts | 2 + packages/eslint-plugin/src/rules/index.ts | 2 + .../src/rules/space-infix-ops.ts | 153 ++++++++++++++++++ .../tests/rules/space-infix-ops.test.ts | 98 +++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 22 +++ 7 files changed, 296 insertions(+) create mode 100644 packages/eslint-plugin/docs/rules/space-infix-ops.md create mode 100644 packages/eslint-plugin/src/rules/space-infix-ops.ts create mode 100644 packages/eslint-plugin/tests/rules/space-infix-ops.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 63b5b6e7f321..c1b47e2f3fe7 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -216,6 +216,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..9360c8dee009 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/space-infix-ops.md @@ -0,0 +1,18 @@ +# 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. + +## 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/semi.md) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index 7e457d7aa1f5..510720d9027c 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 fa8dba93ed1d..396b4f6eefbc 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -13,6 +13,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'; @@ -120,6 +121,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..63e5692ddd76 --- /dev/null +++ b/packages/eslint-plugin/src/rules/space-infix-ops.ts @@ -0,0 +1,153 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/space-infix-ops'; +import { TSEnumMember } from '../../../parser/node_modules/@typescript-eslint/types/dist/ts-estree'; +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(); + + /** + * Find the first non space specified char + * @param {TSESTree.token} left The token to the left + * @param {TSESTree.Token} right The token to the right + * @param {string} op The token to find + * @returns {TSESTree.Token|null} + * @private + */ + function getFirstNonSpacedToken( + left: TSESTree.Token, + right: TSESTree.Token, + op: string, + ): TSESTree.Token | null { + const operator = sourceCode.getFirstTokenBetween( + left, + right, + token => token.value === op, + ); + const prev = sourceCode.getTokenBefore(operator!); + const next = sourceCode.getTokenAfter(operator!); + + if ( + !sourceCode.isSpaceBetweenTokens(prev!, operator!) || + !sourceCode.isSpaceBetweenTokens(operator!, next!) + ) { + return operator; + } + + return null; + } + + /** + * Reports an AST node as a rule violation + * @param {TSESTree.Node} mainNode The node to report + * @param {TSESTree.Token} culpritToken The token which has a problem + * @returns {void} + * @private + */ + function report( + mainNode: TSESTree.Node, + culpritToken: TSESTree.Token, + ): void { + context.report({ + node: mainNode, + loc: culpritToken.loc, + messageId: 'missingSpace', + data: { + operator: culpritToken.value, + }, + fix(fixer) { + const previousToken = sourceCode.getTokenBefore(culpritToken); + const afterToken = sourceCode.getTokenAfter(culpritToken); + let fixString = ''; + + if (culpritToken.range[0] - previousToken!.range[1] === 0) { + fixString = ' '; + } + + fixString += culpritToken.value; + + if (afterToken!.range[0] - culpritToken.range[1] === 0) { + fixString += ' '; + } + + return fixer.replaceText(culpritToken, fixString); + }, + }); + } + + /** + * Check if it has an assignment char and report if it's faulty + * @param {TSESTree.Node} node The node to report + * @returns {void} + * @private + */ + function checkForAssignmentSpace(node: TSEnumMember): void { + if (!node.initializer) { + return; + } + + const leftNode = sourceCode.getTokenByRangeStart( + sourceCode.getIndexFromLoc(node.id.loc.start), + )!; + const rightNode = sourceCode.getTokenByRangeStart( + sourceCode.getIndexFromLoc(node.initializer.loc.start), + )!; + + if (!rightNode) { + return; + } + + const operator = '='; + + const nonSpacedNode = getFirstNonSpacedToken( + leftNode, + rightNode, + operator, + ); + + if (nonSpacedNode) { + report(node, nonSpacedNode); + } + } + + const nodesToCheck = [AST_NODE_TYPES.TSEnumMember].reduce< + TSESLint.RuleListener + >((acc, node) => { + acc[node as string] = checkForAssignmentSpace; + return acc; + }, {}); + + return { + ...rules, + ...nodesToCheck, + }; + }, +}); 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..1a35d5ba7467 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/space-infix-ops.test.ts @@ -0,0 +1,98 @@ +/* eslint-disable @typescript-eslint/internal/plugin-test-formatting */ +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 7cabdf8a3c97..48289200f921 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -729,3 +729,25 @@ declare module 'eslint/lib/rules/no-loss-of-precision' { >; 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; +}