From b3390ef6310e83d6d79de3d93f29382cb17f7691 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sat, 26 Sep 2020 22:54:33 +0200 Subject: [PATCH 01/25] feat(eslint-plugin): add no-void-expr --- packages/eslint-plugin/README.md | 1 + .../docs/rules/no-void-expression.md | 25 ++++++ packages/eslint-plugin/src/configs/all.ts | 1 + packages/eslint-plugin/src/rules/index.ts | 30 ++++--- .../src/rules/no-void-expression.ts | 89 +++++++++++++++++++ .../tests/rules/no-void-expression.test.ts | 51 +++++++++++ 6 files changed, 184 insertions(+), 13 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-void-expression.md create mode 100644 packages/eslint-plugin/src/rules/no-void-expression.ts create mode 100644 packages/eslint-plugin/tests/rules/no-void-expression.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 2f6425bf902..fb880a4af1d 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -148,6 +148,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | +| [`@typescript-eslint/no-void-expression`](./docs/rules/no-void-expression.md) | Requires expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md new file mode 100644 index 00000000000..d91fe5245c0 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -0,0 +1,25 @@ +# Requires expressions of type void to appear in statement position (`no-void-expression`) + +TODO + +## Examples + +Examples of **incorrect** code for this rule: + +```ts +// TODO +``` + +Examples of **correct** code for this rule: + +```tsx +// TODO +``` + +## When Not To Use It + +TODO + +## Related to + +- TSLint: ['no-void-expression'](https://palantir.github.io/tslint/rules/no-void-expression/) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index ec298406498..484cc9a016f 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -108,6 +108,7 @@ export = { 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-var-requires': 'error', + '@typescript-eslint/no-void-expression': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/prefer-for-of': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 587b79d3017..a9322e92626 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -1,3 +1,5 @@ +/* eslint-disable eslint-comments/no-use */ +/* eslint sort-keys: error */ import adjacentOverloadSignatures from './adjacent-overload-signatures'; import arrayType from './array-type'; import awaitThenable from './await-thenable'; @@ -8,7 +10,6 @@ import braceStyle from './brace-style'; import classLiteralPropertyStyle from './class-literal-property-style'; import commaDangle from './comma-dangle'; import commaSpacing from './comma-spacing'; -import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion'; import consistentIndexedObjectStyle from './consistent-indexed-object-style'; import consistentTypeAssertions from './consistent-type-assertions'; import consistentTypeDefinitions from './consistent-type-definitions'; @@ -29,25 +30,26 @@ import methodSignatureStyle from './method-signature-style'; import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; import noBaseToString from './no-base-to-string'; +import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion'; import noDupeClassMembers from './no-dupe-class-members'; +import noDuplicateImports from './no-duplicate-imports'; import noDynamicDelete from './no-dynamic-delete'; import noEmptyFunction from './no-empty-function'; import noEmptyInterface from './no-empty-interface'; import noExplicitAny from './no-explicit-any'; -import noImplicitAnyCatch from './no-implicit-any-catch'; -import noExtraneousClass from './no-extraneous-class'; import noExtraNonNullAssertion from './no-extra-non-null-assertion'; import noExtraParens from './no-extra-parens'; import noExtraSemi from './no-extra-semi'; +import noExtraneousClass from './no-extraneous-class'; import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; -import preferLiteralEnumMember from './prefer-literal-enum-member'; +import noImplicitAnyCatch from './no-implicit-any-catch'; import noImpliedEval from './no-implied-eval'; import noInferrableTypes from './no-inferrable-types'; import noInvalidThis from './no-invalid-this'; import noInvalidVoidType from './no-invalid-void-type'; -import noLossOfPrecision from './no-loss-of-precision'; import noLoopFunc from './no-loop-func'; +import noLossOfPrecision from './no-loss-of-precision'; import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; @@ -76,11 +78,13 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; +import noVoidExpression from './no-void-expression'; import preferAsConst from './prefer-as-const'; import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; import preferFunctionType from './prefer-function-type'; import preferIncludes from './prefer-includes'; +import preferLiteralEnumMember from './prefer-literal-enum-member'; import preferNamespaceKeyword from './prefer-namespace-keyword'; import preferNullishCoalescing from './prefer-nullish-coalescing'; import preferOptionalChain from './prefer-optional-chain'; @@ -106,7 +110,6 @@ import typeAnnotationSpacing from './type-annotation-spacing'; import typedef from './typedef'; import unboundMethod from './unbound-method'; import unifiedSignatures from './unified-signatures'; -import noDuplicateImports from './no-duplicate-imports'; export default { 'adjacent-overload-signatures': adjacentOverloadSignatures, @@ -129,6 +132,7 @@ export default { 'explicit-member-accessibility': explicitMemberAccessibility, 'explicit-module-boundary-types': explicitModuleBoundaryTypes, 'func-call-spacing': funcCallSpacing, + indent: indent, 'init-declarations': initDeclarations, 'keyword-spacing': keywordSpacing, 'lines-between-class-members': linesBetweenClassMembers, @@ -140,6 +144,7 @@ export default { 'no-base-to-string': noBaseToString, 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, 'no-dupe-class-members': noDupeClassMembers, + 'no-duplicate-imports': noDuplicateImports, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, @@ -180,11 +185,12 @@ export default { 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, 'no-unused-expressions': noUnusedExpressions, - 'no-unused-vars-experimental': noUnusedVarsExperimental, 'no-unused-vars': noUnusedVars, + 'no-unused-vars-experimental': noUnusedVarsExperimental, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, + 'no-void-expression': noVoidExpression, 'prefer-as-const': preferAsConst, 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, @@ -194,28 +200,26 @@ export default { 'prefer-namespace-keyword': preferNamespaceKeyword, 'prefer-nullish-coalescing': preferNullishCoalescing, 'prefer-optional-chain': preferOptionalChain, - 'prefer-readonly-parameter-types': preferReadonlyParameterTypes, 'prefer-readonly': preferReadonly, + 'prefer-readonly-parameter-types': preferReadonlyParameterTypes, 'prefer-reduce-type-parameter': preferReduceTypeParameter, 'prefer-regexp-exec': preferRegexpExec, 'prefer-string-starts-ends-with': preferStringStartsEndsWith, 'prefer-ts-expect-error': preferTsExpectError, 'promise-function-async': promiseFunctionAsync, + quotes: quotes, 'require-array-sort-compare': requireArraySortCompare, 'require-await': requireAwait, 'restrict-plus-operands': restrictPlusOperands, 'restrict-template-expressions': restrictTemplateExpressions, 'return-await': returnAwait, + semi: semi, 'space-before-function-paren': spaceBeforeFunctionParen, 'strict-boolean-expressions': strictBooleanExpressions, 'switch-exhaustiveness-check': switchExhaustivenessCheck, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, + typedef: typedef, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, - 'no-duplicate-imports': noDuplicateImports, - indent: indent, - quotes: quotes, - semi: semi, - typedef: typedef, }; diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts new file mode 100644 index 00000000000..6c088d92cf4 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -0,0 +1,89 @@ +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; +import * as util from '../util'; + +export type Options = [ + { + // ignoreArrowShorthand: boolean; + }, +]; + +export type MessageId = 'invalidVoidExpr'; + +export default util.createRule({ + name: 'no-void-expression', + meta: { + docs: { + description: + 'Requires expressions of type void to appear in statement position', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + messages: { + invalidVoidExpr: '', + }, + schema: [ + { + type: 'object', + properties: { + ignoreArrowShorthand: { type: 'boolean' }, + }, + additionalProperties: false, + }, + ], + type: 'problem', + fixable: 'code', + }, + defaultOptions: [ + { + // ignoreArrowShorthand: false, + }, + ], + create(context) { + return { + 'AwaitExpression, CallExpression, TaggedTemplateExpression'( + node: + | TSESTree.AwaitExpression + | TSESTree.CallExpression + | TSESTree.TaggedTemplateExpression, + ): void { + const parserServices = util.getParserServices(context); + const checker = parserServices.program.getTypeChecker(); + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); + const type = util.getConstrainedTypeAtLocation(checker, tsNode); + if (!isParentAllowed(node)) { + if (tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) { + context.report({ + node, + messageId: 'invalidVoidExpr', + }); + } + } + }, + }; + + function isParentAllowed(node: TSESTree.Node): boolean { + if (node.parent == null) { + return true; + } + switch (node.parent.type) { + case AST_NODE_TYPES.ExpressionStatement: + // e.g. `console.log("foo");` + return true; + case AST_NODE_TYPES.LogicalExpression: + // e.g. `x && console.log(x);` + return isParentAllowed(node.parent); + case AST_NODE_TYPES.ConditionalExpression: + // e.g. `cond ? console.log(true) : console.log(false);` + return isParentAllowed(node.parent); + default: + return false; + } + } + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts new file mode 100644 index 00000000000..2550d69d2ee --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -0,0 +1,51 @@ +import rule, { MessageId, Options } from '../../src/rules/no-void-expression'; +import { + batchedSingleLineTests, + getFixturesRootDir, + RuleTester, +} from '../RuleTester'; + +const rootPath = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +ruleTester.run('no-void-expression', rule, { + valid: [ + ...batchedSingleLineTests({ + code: ` + console.log('foo'); + foo && console.log(foo); + foo || console.log(foo); + (foo && console.log(true)) || console.log(false); + `, + }), + ], + + invalid: [ + ...batchedSingleLineTests({ + code: ` + const x = console.log('foo'); + (() => console.log('foo'))(); + return console.log('foo'); + console.error(console.log('foo')); + [console.log('foo')]; + ({ x: console.log('foo') }); + void console.log('foo'); + `, + errors: [ + { messageId: 'invalidVoidExpr', line: 2, column: 11 }, + { messageId: 'invalidVoidExpr', line: 3, column: 16 }, + { messageId: 'invalidVoidExpr', line: 4, column: 16 }, + { messageId: 'invalidVoidExpr', line: 5, column: 23 }, + { messageId: 'invalidVoidExpr', line: 6, column: 10 }, + { messageId: 'invalidVoidExpr', line: 7, column: 15 }, + { messageId: 'invalidVoidExpr', line: 8, column: 14 }, + ], + }), + ], +}); From 9cb2dfa3205b9ce96c1b2011010a8062c9d03df2 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 27 Sep 2020 00:08:10 +0200 Subject: [PATCH 02/25] feat(eslint-plugin): add ignoreArrowShorthand --- .../src/rules/no-void-expression.ts | 28 +++++++++-- .../tests/rules/no-void-expression.test.ts | 48 +++++++++++++++---- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 6c088d92cf4..1e15b8ce498 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -8,11 +8,11 @@ import * as util from '../util'; export type Options = [ { - // ignoreArrowShorthand: boolean; + ignoreArrowShorthand: boolean; }, ]; -export type MessageId = 'invalidVoidExpr'; +export type MessageId = 'invalidVoidExpr' | 'invalidVoidArrowExpr'; export default util.createRule({ name: 'no-void-expression', @@ -25,7 +25,8 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - invalidVoidExpr: '', + invalidVoidExpr: 'TODO', + invalidVoidArrowExpr: 'TODO', }, schema: [ { @@ -41,10 +42,10 @@ export default util.createRule({ }, defaultOptions: [ { - // ignoreArrowShorthand: false, + ignoreArrowShorthand: false, }, ], - create(context) { + create(context, [options]) { return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( node: @@ -52,12 +53,26 @@ export default util.createRule({ | TSESTree.CallExpression | TSESTree.TaggedTemplateExpression, ): void { + const sourceCode = context.getSourceCode(); const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = util.getConstrainedTypeAtLocation(checker, tsNode); + if (!isParentAllowed(node)) { if (tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) { + if (node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) { + // this would be reported by this rule btw. such irony + return context.report({ + node, + messageId: 'invalidVoidArrowExpr', + fix: fixer => { + const nodeText = sourceCode.getText(node); + return fixer.replaceText(node, `{ ${nodeText}; }`); + }, + }); + } + context.report({ node, messageId: 'invalidVoidExpr', @@ -81,6 +96,9 @@ export default util.createRule({ case AST_NODE_TYPES.ConditionalExpression: // e.g. `cond ? console.log(true) : console.log(false);` return isParentAllowed(node.parent); + case AST_NODE_TYPES.ArrowFunctionExpression: + // e.g. `() => console.log("foo")` + return options.ignoreArrowShorthand; default: return false; } diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index 2550d69d2ee..133dabff414 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -2,6 +2,7 @@ import rule, { MessageId, Options } from '../../src/rules/no-void-expression'; import { batchedSingleLineTests, getFixturesRootDir, + noFormat, RuleTester, } from '../RuleTester'; @@ -22,6 +23,18 @@ ruleTester.run('no-void-expression', rule, { foo && console.log(foo); foo || console.log(foo); (foo && console.log(true)) || console.log(false); + foo ? console.log(true) : console.log(false); + `, + }), + + ...batchedSingleLineTests({ + options: [{ ignoreArrowShorthand: true }], + code: ` + () => console.log('foo'); + foo => foo && console.log(foo); + foo => foo || console.log(foo); + foo => (foo && console.log(true)) || console.log(false); + foo => (foo ? console.log(true) : console.log(false)); `, }), ], @@ -30,7 +43,6 @@ ruleTester.run('no-void-expression', rule, { ...batchedSingleLineTests({ code: ` const x = console.log('foo'); - (() => console.log('foo'))(); return console.log('foo'); console.error(console.log('foo')); [console.log('foo')]; @@ -38,14 +50,34 @@ ruleTester.run('no-void-expression', rule, { void console.log('foo'); `, errors: [ - { messageId: 'invalidVoidExpr', line: 2, column: 11 }, - { messageId: 'invalidVoidExpr', line: 3, column: 16 }, - { messageId: 'invalidVoidExpr', line: 4, column: 16 }, - { messageId: 'invalidVoidExpr', line: 5, column: 23 }, - { messageId: 'invalidVoidExpr', line: 6, column: 10 }, - { messageId: 'invalidVoidExpr', line: 7, column: 15 }, - { messageId: 'invalidVoidExpr', line: 8, column: 14 }, + { line: 2, column: 11, messageId: 'invalidVoidExpr' }, + { line: 3, column: 16, messageId: 'invalidVoidExpr' }, + { line: 4, column: 23, messageId: 'invalidVoidExpr' }, + { line: 5, column: 10, messageId: 'invalidVoidExpr' }, + { line: 6, column: 15, messageId: 'invalidVoidExpr' }, + { line: 7, column: 14, messageId: 'invalidVoidExpr' }, ], }), + + { + code: "() => console.log('foo');", + errors: [{ line: 1, column: 7, messageId: 'invalidVoidArrowExpr' }], + output: noFormat`() => { console.log('foo'); };`, + }, + { + code: 'foo => foo && console.log(foo);', + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + }, + { + code: 'foo => foo || console.log(foo);', + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + }, + { + code: 'foo => (foo ? console.log(true) : console.log(false));', + errors: [ + { line: 1, column: 15, messageId: 'invalidVoidExpr' }, + { line: 1, column: 35, messageId: 'invalidVoidExpr' }, + ], + }, ], }); From d66fb009a44ca9d3204fc785d18285cf803d1863 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 27 Sep 2020 01:16:32 +0200 Subject: [PATCH 03/25] feat(eslint-plugin): add ignoreVoidExpr --- .../src/rules/no-void-expression.ts | 41 +++++++++++++----- .../tests/rules/no-void-expression.test.ts | 43 ++++++++++++++++++- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 1e15b8ce498..de070be3a3a 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -8,12 +8,17 @@ import * as util from '../util'; export type Options = [ { - ignoreArrowShorthand: boolean; + ignoreArrowShorthand?: boolean; + ignoreVoidOperator?: boolean; }, ]; export type MessageId = 'invalidVoidExpr' | 'invalidVoidArrowExpr'; +const defaultOptions = { + ignoreArrowShorthand: false, + ignoreVoidOperator: true, +}; export default util.createRule({ name: 'no-void-expression', meta: { @@ -25,14 +30,16 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - invalidVoidExpr: 'TODO', - invalidVoidArrowExpr: 'TODO', + invalidVoidExpr: 'Unexpected void expression used in another expression.', + invalidVoidArrowExpr: + 'Unexpected void expression returned from arrow function.', }, schema: [ { type: 'object', properties: { ignoreArrowShorthand: { type: 'boolean' }, + ignoreVoidOperator: { type: 'boolean' }, }, additionalProperties: false, }, @@ -40,12 +47,9 @@ export default util.createRule({ type: 'problem', fixable: 'code', }, - defaultOptions: [ - { - ignoreArrowShorthand: false, - }, - ], - create(context, [options]) { + defaultOptions: [defaultOptions], + create(context) { + const options = { ...defaultOptions, ...context.options[0] }; return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( node: @@ -61,8 +65,19 @@ export default util.createRule({ if (!isParentAllowed(node)) { if (tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) { - if (node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) { + if (options.ignoreVoidOperator) { // this would be reported by this rule btw. such irony + return context.report({ + node, + messageId: 'invalidVoidExpr', + fix: fixer => { + const nodeText = sourceCode.getText(node); + return fixer.replaceText(node, `void ${nodeText}`); + }, + }); + } + + if (node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) { return context.report({ node, messageId: 'invalidVoidArrowExpr', @@ -99,6 +114,12 @@ export default util.createRule({ case AST_NODE_TYPES.ArrowFunctionExpression: // e.g. `() => console.log("foo")` return options.ignoreArrowShorthand; + case AST_NODE_TYPES.UnaryExpression: + if (node.parent.operator === 'void') { + // e.g. `void console.log("foo")` + return options.ignoreVoidOperator; + } + return false; default: return false; } diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index 133dabff414..bcf3e416c7a 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -18,6 +18,7 @@ const ruleTester = new RuleTester({ ruleTester.run('no-void-expression', rule, { valid: [ ...batchedSingleLineTests({ + options: [{ ignoreVoidOperator: false }], code: ` console.log('foo'); foo && console.log(foo); @@ -27,7 +28,7 @@ ruleTester.run('no-void-expression', rule, { `, }), - ...batchedSingleLineTests({ + ...batchedSingleLineTests({ options: [{ ignoreArrowShorthand: true }], code: ` () => console.log('foo'); @@ -37,10 +38,25 @@ ruleTester.run('no-void-expression', rule, { foo => (foo ? console.log(true) : console.log(false)); `, }), + + ...batchedSingleLineTests({ + code: ` + void console.log('foo'); + void foo && console.log(foo); + void foo || console.log(foo); + void (foo && console.log(true)) || console.log(false); + void (foo ? console.log(true) : console.log(false)); + foo && void console.log(foo); + foo || void console.log(foo); + (foo && void console.log(true)) || void console.log(false); + foo ? void console.log(true) : void console.log(false); + `, + }), ], invalid: [ ...batchedSingleLineTests({ + options: [{ ignoreVoidOperator: false }], code: ` const x = console.log('foo'); return console.log('foo'); @@ -60,24 +76,49 @@ ruleTester.run('no-void-expression', rule, { }), { + options: [{ ignoreVoidOperator: false }], code: "() => console.log('foo');", errors: [{ line: 1, column: 7, messageId: 'invalidVoidArrowExpr' }], output: noFormat`() => { console.log('foo'); };`, }, { + options: [{ ignoreVoidOperator: false }], code: 'foo => foo && console.log(foo);', errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], }, { + options: [{ ignoreVoidOperator: false }], code: 'foo => foo || console.log(foo);', errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], }, { + options: [{ ignoreVoidOperator: false }], code: 'foo => (foo ? console.log(true) : console.log(false));', errors: [ { line: 1, column: 15, messageId: 'invalidVoidExpr' }, { line: 1, column: 35, messageId: 'invalidVoidExpr' }, ], }, + + { + code: "return console.log('foo');", + errors: [{ line: 1, column: 8, messageId: 'invalidVoidExpr' }], + output: "return void console.log('foo');", + }, + { + code: "console.error(console.log('foo'));", + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + output: "console.error(void console.log('foo'));", + }, + { + code: "const x = console.log('foo');", + errors: [{ line: 1, column: 11, messageId: 'invalidVoidExpr' }], + output: "const x = void console.log('foo');", + }, + { + code: "() => console.log('foo');", + errors: [{ line: 1, column: 7, messageId: 'invalidVoidExpr' }], + output: "() => void console.log('foo');", + }, ], }); From 2cedaa66ecd9dc9d59b785b22668b9f8ac1d3ac2 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 27 Sep 2020 21:38:10 +0200 Subject: [PATCH 04/25] feat(eslint-plugin): add more messages and fixes --- .../src/rules/no-void-expression.ts | 273 ++++++++++++++---- .../tests/rules/no-void-expression.test.ts | 142 +++++++-- 2 files changed, 329 insertions(+), 86 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index de070be3a3a..331d13fb0bf 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -1,10 +1,16 @@ import { AST_NODE_TYPES, + TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import * as tsutils from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; +import { + isClosingParenToken, + isOpeningParenToken, + isParenthesized, +} from '../util'; export type Options = [ { @@ -13,12 +19,15 @@ export type Options = [ }, ]; -export type MessageId = 'invalidVoidExpr' | 'invalidVoidArrowExpr'; +export type MessageId = + | 'invalidVoidExpr' + | 'invalidVoidExprWrapVoid' + | 'invalidVoidExprArrow' + | 'invalidVoidExprArrowWrapVoid' + | 'invalidVoidExprReturn' + | 'invalidVoidExprReturnLast' + | 'invalidVoidExprReturnWrapVoid'; -const defaultOptions = { - ignoreArrowShorthand: false, - ignoreVoidOperator: true, -}; export default util.createRule({ name: 'no-void-expression', meta: { @@ -30,9 +39,27 @@ export default util.createRule({ requiresTypeChecking: true, }, messages: { - invalidVoidExpr: 'Unexpected void expression used in another expression.', - invalidVoidArrowExpr: - 'Unexpected void expression returned from arrow function.', + invalidVoidExpr: + 'Placing a void expression inside another expression is forbidden. ' + + "Move it to it's own statement instead.", + invalidVoidExprWrapVoid: + 'Void expressions used inside another expression ' + + 'must be marked explicitly with the `void` operator.', + invalidVoidExprArrow: + 'Returning a void expression from an arrow function shorthand is forbidden. ' + + 'Please add braces to the arrow function.', + invalidVoidExprArrowWrapVoid: + 'Void expressions returned from an arrow function shorthand ' + + 'must be marked explicitly with the `void` operator.', + invalidVoidExprReturn: + 'Returning a void expression from a function is forbidden.' + + 'Please move it before the `return` statement.', + invalidVoidExprReturnLast: + 'Returning a void expression from a function is forbidden.' + + 'Please remove the `return` statement.', + invalidVoidExprReturnWrapVoid: + 'Void expressions returned from a function ' + + 'must be marked explicitly with the `void` operator.', }, schema: [ { @@ -47,9 +74,9 @@ export default util.createRule({ type: 'problem', fixable: 'code', }, - defaultOptions: [defaultOptions], - create(context) { - const options = { ...defaultOptions, ...context.options[0] }; + defaultOptions: [{}], + + create(context, [options]) { return { 'AwaitExpression, CallExpression, TaggedTemplateExpression'( node: @@ -57,72 +84,200 @@ export default util.createRule({ | TSESTree.CallExpression | TSESTree.TaggedTemplateExpression, ): void { - const sourceCode = context.getSourceCode(); const parserServices = util.getParserServices(context); const checker = parserServices.program.getTypeChecker(); const tsNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = util.getConstrainedTypeAtLocation(checker, tsNode); + if (!tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) { + // not a void expression + return; + } - if (!isParentAllowed(node)) { - if (tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) { - if (options.ignoreVoidOperator) { - // this would be reported by this rule btw. such irony - return context.report({ - node, - messageId: 'invalidVoidExpr', - fix: fixer => { - const nodeText = sourceCode.getText(node); - return fixer.replaceText(node, `void ${nodeText}`); - }, - }); - } + const invalidAncestor = findInvalidAncestor(node); + if (invalidAncestor == null) { + // void expression is in valid position + return; + } + + const sourceCode = context.getSourceCode(); + const wrapVoidFix = (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => { + const nodeText = sourceCode.getText(node); + const newNodeText = `void ${nodeText}`; + return fixer.replaceText(node, newNodeText); + }; - if (node.parent?.type === AST_NODE_TYPES.ArrowFunctionExpression) { - return context.report({ - node, - messageId: 'invalidVoidArrowExpr', - fix: fixer => { - const nodeText = sourceCode.getText(node); - return fixer.replaceText(node, `{ ${nodeText}; }`); - }, - }); + // handle arrow function shorthand + if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { + // handle wrapping with `void` + if (options.ignoreVoidOperator) { + return context.report({ + node, + messageId: 'invalidVoidExprArrowWrapVoid', + fix: wrapVoidFix, + }); + } + + // handle wrapping with braces + const arrowFunction = invalidAncestor; + return context.report({ + node, + messageId: 'invalidVoidExprArrow', + fix(fixer) { + const arrowBody = arrowFunction.body; + const arrowBodyText = sourceCode.getText(arrowBody); + isParenthesized; + const newArrowBodyText = `{ ${arrowBodyText}; }`; + if (isParenthesized(arrowBody, sourceCode)) { + const [bodyOpeningParen, bodyClosingParen] = [ + sourceCode.getTokenBefore(arrowBody, isOpeningParenToken)!, + sourceCode.getTokenAfter(arrowBody, isClosingParenToken)!, + ]; + return fixer.replaceTextRange( + [bodyOpeningParen.range[0], bodyClosingParen.range[1]], + newArrowBodyText, + ); + } + return fixer.replaceText(arrowBody, newArrowBodyText); + }, + }); + } + + // handle return statement + if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { + // handle wrapping with `void` + if (options.ignoreVoidOperator) { + return context.report({ + node, + messageId: 'invalidVoidExprReturnWrapVoid', + fix: wrapVoidFix, + }); + } + + // handle moving to it's own statement + const returnStmt = invalidAncestor; + let isTopLevelReturn = false; + let isLastReturn = false; + if (returnStmt.parent?.type === AST_NODE_TYPES.BlockStatement) { + const block = returnStmt.parent; + const blockType = block.parent?.type; + if ( + blockType === AST_NODE_TYPES.FunctionDeclaration || + blockType === AST_NODE_TYPES.FunctionExpression || + blockType === AST_NODE_TYPES.ArrowFunctionExpression + ) { + isTopLevelReturn = true; + } + if (block.body.indexOf(returnStmt) === block.body.length - 1) { + isLastReturn = true; } + } - context.report({ + // remove the `return` keyword + if (isTopLevelReturn && isLastReturn) { + return context.report({ node, - messageId: 'invalidVoidExpr', + messageId: 'invalidVoidExprReturnLast', + fix(fixer) { + const returnValue = returnStmt.argument!; + const returnValueText = sourceCode.getText(returnValue); + const newReturnStmtText = `${returnValueText};`; + return fixer.replaceText(returnStmt, newReturnStmtText); + }, }); } + + // move before the `return` keyword + // this would be reported by this rule btw. such irony + return context.report({ + node, + messageId: 'invalidVoidExprReturn', + fix(fixer) { + const returnValue = returnStmt.argument!; + const returnValueText = sourceCode.getText(returnValue); + let newReturnStmtText = `${returnValueText}; return;`; + if (returnStmt.parent?.type !== AST_NODE_TYPES.BlockStatement) { + // e.g. `if (cond) return console.error();` + // add braces if not inside a block + newReturnStmtText = `{ ${newReturnStmtText} }`; + } + return fixer.replaceText(returnStmt, newReturnStmtText); + }, + }); } + + // handle generic case + if (options.ignoreVoidOperator) { + return context.report({ + node, + messageId: 'invalidVoidExprWrapVoid', + fix: wrapVoidFix, + }); + } + context.report({ + node, + messageId: 'invalidVoidExpr', + }); }, }; - function isParentAllowed(node: TSESTree.Node): boolean { - if (node.parent == null) { - return true; + /** + * Inspects the void expression's ancestors and finds closest invalid one. + * By default anything other than an ExpressionStatement is invalid. + * Parent expressions which can be used for their side effects are ignored + * and their parents are checked instead. + * @param node The void expression node to check. + * @returns Invalid ancestor node if it was found. `null` otherwise. + */ + function findInvalidAncestor(node: TSESTree.Node): TSESTree.Node | null { + const { parent } = node; + + if (parent == null) { + // if there is no parent then let's say it's valid + return null; + } + + if (parent.type === AST_NODE_TYPES.ExpressionStatement) { + // e.g. `{ console.log("foo"); }` + // this is always valid + return null; + } + + if (parent.type === AST_NODE_TYPES.LogicalExpression) { + if (parent.right === node) { + // e.g. `x && console.log(x)` + // this is valid only if the next ancestor is valid + return findInvalidAncestor(parent); + } } - switch (node.parent.type) { - case AST_NODE_TYPES.ExpressionStatement: - // e.g. `console.log("foo");` - return true; - case AST_NODE_TYPES.LogicalExpression: - // e.g. `x && console.log(x);` - return isParentAllowed(node.parent); - case AST_NODE_TYPES.ConditionalExpression: - // e.g. `cond ? console.log(true) : console.log(false);` - return isParentAllowed(node.parent); - case AST_NODE_TYPES.ArrowFunctionExpression: - // e.g. `() => console.log("foo")` - return options.ignoreArrowShorthand; - case AST_NODE_TYPES.UnaryExpression: - if (node.parent.operator === 'void') { - // e.g. `void console.log("foo")` - return options.ignoreVoidOperator; + + if (parent.type === AST_NODE_TYPES.ConditionalExpression) { + if (parent.consequent === node || parent.alternate === node) { + // e.g. `cond ? console.log(true) : console.log(false)` + // this is valid only if the next ancestor is valid + return findInvalidAncestor(parent); + } + } + + if (parent.type === AST_NODE_TYPES.ArrowFunctionExpression) { + // e.g. `() => console.log("foo")` + // this is valid with an appropriate option + if (options.ignoreArrowShorthand) { + return null; + } + } + + if (parent.type === AST_NODE_TYPES.UnaryExpression) { + if (parent.operator === 'void') { + // e.g. `void console.log("foo")` + // this is valid with an appropriate option + if (options.ignoreVoidOperator) { + return null; } - return false; - default: - return false; + } } + + // any other parent is invalid + return parent; } }, }); diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index bcf3e416c7a..cf62e78eedd 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -18,12 +18,11 @@ const ruleTester = new RuleTester({ ruleTester.run('no-void-expression', rule, { valid: [ ...batchedSingleLineTests({ - options: [{ ignoreVoidOperator: false }], code: ` + () => Math.random(); console.log('foo'); foo && console.log(foo); foo || console.log(foo); - (foo && console.log(true)) || console.log(false); foo ? console.log(true) : console.log(false); `, }), @@ -34,12 +33,12 @@ ruleTester.run('no-void-expression', rule, { () => console.log('foo'); foo => foo && console.log(foo); foo => foo || console.log(foo); - foo => (foo && console.log(true)) || console.log(false); foo => (foo ? console.log(true) : console.log(false)); `, }), ...batchedSingleLineTests({ + options: [{ ignoreVoidOperator: true }], code: ` void console.log('foo'); void foo && console.log(foo); @@ -50,75 +49,164 @@ ruleTester.run('no-void-expression', rule, { foo || void console.log(foo); (foo && void console.log(true)) || void console.log(false); foo ? void console.log(true) : void console.log(false); + return void console.log('foo'); `, }), ], invalid: [ ...batchedSingleLineTests({ - options: [{ ignoreVoidOperator: false }], code: ` const x = console.log('foo'); - return console.log('foo'); console.error(console.log('foo')); [console.log('foo')]; ({ x: console.log('foo') }); void console.log('foo'); + console.log('foo') ? true : false; + (console.log('foo') && true) || false; + (cond && console.log('ok')) || console.log('error'); + !console.log('foo'); `, errors: [ { line: 2, column: 11, messageId: 'invalidVoidExpr' }, - { line: 3, column: 16, messageId: 'invalidVoidExpr' }, - { line: 4, column: 23, messageId: 'invalidVoidExpr' }, - { line: 5, column: 10, messageId: 'invalidVoidExpr' }, - { line: 6, column: 15, messageId: 'invalidVoidExpr' }, - { line: 7, column: 14, messageId: 'invalidVoidExpr' }, + { line: 3, column: 23, messageId: 'invalidVoidExpr' }, + { line: 4, column: 10, messageId: 'invalidVoidExpr' }, + { line: 5, column: 15, messageId: 'invalidVoidExpr' }, + { line: 6, column: 14, messageId: 'invalidVoidExpr' }, + { line: 7, column: 9, messageId: 'invalidVoidExpr' }, + { line: 8, column: 10, messageId: 'invalidVoidExpr' }, + { line: 9, column: 18, messageId: 'invalidVoidExpr' }, + { line: 10, column: 10, messageId: 'invalidVoidExpr' }, ], }), { - options: [{ ignoreVoidOperator: false }], code: "() => console.log('foo');", - errors: [{ line: 1, column: 7, messageId: 'invalidVoidArrowExpr' }], + errors: [{ line: 1, column: 7, messageId: 'invalidVoidExprArrow' }], output: noFormat`() => { console.log('foo'); };`, }, { - options: [{ ignoreVoidOperator: false }], code: 'foo => foo && console.log(foo);', - errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExprArrow' }], + output: noFormat`foo => { foo && console.log(foo); };`, }, { - options: [{ ignoreVoidOperator: false }], code: 'foo => foo || console.log(foo);', - errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExprArrow' }], + output: noFormat`foo => { foo || console.log(foo); };`, }, { - options: [{ ignoreVoidOperator: false }], code: 'foo => (foo ? console.log(true) : console.log(false));', errors: [ - { line: 1, column: 15, messageId: 'invalidVoidExpr' }, - { line: 1, column: 35, messageId: 'invalidVoidExpr' }, + { line: 1, column: 15, messageId: 'invalidVoidExprArrow' }, + { line: 1, column: 35, messageId: 'invalidVoidExprArrow' }, ], + output: noFormat`foo => { foo ? console.log(true) : console.log(false); };`, + }, + { + code: ` + function f() { + return console.log('foo'); + console.log('bar'); + } + `, + errors: [{ line: 3, column: 18, messageId: 'invalidVoidExprReturn' }], + output: noFormat` + function f() { + console.log('foo'); return; + console.log('bar'); + } + `, + }, + { + code: ` + function f() { + console.log('foo'); + return console.log('bar'); + } + `, + errors: [{ line: 4, column: 18, messageId: 'invalidVoidExprReturnLast' }], + output: ` + function f() { + console.log('foo'); + console.log('bar'); + } + `, + }, + { + code: ` + const f = () => { + if (cond) { + return console.error('foo'); + } + console.log('bar'); + }; + `, + errors: [{ line: 4, column: 20, messageId: 'invalidVoidExprReturn' }], + output: noFormat` + const f = () => { + if (cond) { + console.error('foo'); return; + } + console.log('bar'); + }; + `, + }, + { + code: ` + const f = function () { + if (cond) return console.error('foo'); + console.log('bar'); + }; + `, + errors: [{ line: 3, column: 28, messageId: 'invalidVoidExprReturn' }], + output: noFormat` + const f = function () { + if (cond) { console.error('foo'); return; } + console.log('bar'); + }; + `, }, { + options: [{ ignoreVoidOperator: true }], code: "return console.log('foo');", - errors: [{ line: 1, column: 8, messageId: 'invalidVoidExpr' }], + errors: [ + { line: 1, column: 8, messageId: 'invalidVoidExprReturnWrapVoid' }, + ], output: "return void console.log('foo');", }, { + options: [{ ignoreVoidOperator: true }], code: "console.error(console.log('foo'));", - errors: [{ line: 1, column: 15, messageId: 'invalidVoidExpr' }], + errors: [{ line: 1, column: 15, messageId: 'invalidVoidExprWrapVoid' }], output: "console.error(void console.log('foo'));", }, { - code: "const x = console.log('foo');", - errors: [{ line: 1, column: 11, messageId: 'invalidVoidExpr' }], - output: "const x = void console.log('foo');", + options: [{ ignoreVoidOperator: true }], + code: "console.log('foo') ? true : false;", + errors: [{ line: 1, column: 1, messageId: 'invalidVoidExprWrapVoid' }], + output: "void console.log('foo') ? true : false;", }, { - code: "() => console.log('foo');", - errors: [{ line: 1, column: 7, messageId: 'invalidVoidExpr' }], - output: "() => void console.log('foo');", + options: [{ ignoreVoidOperator: true }], + code: "const x = foo ?? console.log('foo');", + errors: [{ line: 1, column: 18, messageId: 'invalidVoidExprWrapVoid' }], + output: "const x = foo ?? void console.log('foo');", + }, + { + options: [{ ignoreVoidOperator: true }], + code: 'foo => foo || console.log(foo);', + errors: [ + { line: 1, column: 15, messageId: 'invalidVoidExprArrowWrapVoid' }, + ], + output: 'foo => foo || void console.log(foo);', + }, + { + options: [{ ignoreVoidOperator: true }], + code: "!!console.log('foo');", + errors: [{ line: 1, column: 3, messageId: 'invalidVoidExprWrapVoid' }], + output: "!!void console.log('foo');", }, ], }); From 0a4ed4e734c476395e40eb520f9f14d8e5bb78b3 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 27 Sep 2020 23:31:12 +0200 Subject: [PATCH 05/25] feat(eslint-plugin): no-void-expression docs --- .../docs/rules/no-void-expression.md | 100 +++++++++++++++++- .../src/rules/no-void-expression.ts | 6 +- .../tests/rules/no-void-expression.test.ts | 60 +++++++++-- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index d91fe5245c0..3ad10ccf3a6 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -1,24 +1,114 @@ # Requires expressions of type void to appear in statement position (`no-void-expression`) -TODO +It’s misleading returning the results of an expression whose type is void. +Attempting to do so is likely a symptom of expecting a different return type from a function. +Even if used correctly, it can be misleading for other developers, +who don't know what a particular function does and if its result matters. + +This rule provides automatic fixes for most common cases. ## Examples Examples of **incorrect** code for this rule: ```ts -// TODO +// somebody forgot that `alert` doesn't return anything +const response = alert('Are you sure?'); +console.log(alert('Are you sure?')); + +// it's not obvious whether the chained promise will contain the response (fixable) +promise.then(value => window.postMessage(value)); + +// it looks like we are returning the result of `console.error` (fixable) +function doSomething() { + if (!somethingToDo) return console.error('Nothing to do!'); + + console.log('Doing a thing...'); +} ``` Examples of **correct** code for this rule: -```tsx -// TODO +```ts +// just a regular void function in a statement position +alert('Hello, world!'); + +// this function returns a boolean value so it's ok +const response = confirm('Are you sure?'); +console.log(confirm('Are you sure?')); + +// now it's obvious that `postMessage` doesn't return any response +promise.then(value => { + window.postMessage(value); +}); + +// now it's explicit that we want to log the error and return early +function doSomething() { + if (!somethingToDo) { + console.error('Nothing to do!'); + return; + } + + console.log('Doing a thing...'); +} + +// using logical expressions for their side effects is fine +cond && console.log('true'); +cond || console.error('false'); +cond ? console.log('true') : console.error('false'); +``` + +## Options + +An object option can be specified. Each boolean flag makes the rule less strict. + +### `ignoreArrowShorthand` + +`"no-void-expression": ["error", { "ignoreArrowShorthand": true }]` + +It might be undesirable to wrap every arrow function shorthand expression with braces. +Especially when using Prettier formatter, which spreads such code across 3 lines instead of 1. + +Examples of additional **correct** code with this option enabled: + +```ts +promise.then(value => window.postMessage(value)); +``` + +### `ignoreVoidOperator` + +`"no-void-expression": ["error", { "ignoreVoidOperator": true }]` + +It might be preferable to only use some distinct syntax +to explicitly mark the confusing but valid usage of void expressions. +This option allows void expressions which are explicitly wrapped in the `void` operator. +This can help avoid confusion among other developers as long as they are made aware of this code style. + +This option also changes the automatic fixes for common cases to use the `void` operator. +It also enables a suggestion fix to wrap the void expression with `void` operator for every problem reported. + +Examples of additional **correct** code with this option enabled: + +```ts +// now it's obvious that we don't expect any response (fixable) +promise.then(value => void window.postMessage(value)); + +// now it's explicit that we don't want to return anything (fixable) +function doSomething() { + if (!somethingToDo) return void console.error('Nothing to do!'); + + console.log('Doing a thing...'); +} + +// we are sure that we want to always log `undefined` (fixable via suggestion) +console.log(void alert('Hello, world!')); ``` ## When Not To Use It -TODO +The return type of a function can be inspected by going to its definition or hovering over it in an IDE. +If you don't care about being explicit about the void type in actual code then don't use this rule. +Also, if you prefer concise coding style then also don't use it. ## Related to diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 331d13fb0bf..912ad756ecd 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -26,7 +26,8 @@ export type MessageId = | 'invalidVoidExprArrowWrapVoid' | 'invalidVoidExprReturn' | 'invalidVoidExprReturnLast' - | 'invalidVoidExprReturnWrapVoid'; + | 'invalidVoidExprReturnWrapVoid' + | 'voidExprWrapVoid'; export default util.createRule({ name: 'no-void-expression', @@ -60,6 +61,7 @@ export default util.createRule({ invalidVoidExprReturnWrapVoid: 'Void expressions returned from a function ' + 'must be marked explicitly with the `void` operator.', + voidExprWrapVoid: 'Mark with an explicit `void` operator', }, schema: [ { @@ -210,7 +212,7 @@ export default util.createRule({ return context.report({ node, messageId: 'invalidVoidExprWrapVoid', - fix: wrapVoidFix, + suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }], }); } context.report({ diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index cf62e78eedd..89d63ef6232 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -179,20 +179,53 @@ ruleTester.run('no-void-expression', rule, { { options: [{ ignoreVoidOperator: true }], code: "console.error(console.log('foo'));", - errors: [{ line: 1, column: 15, messageId: 'invalidVoidExprWrapVoid' }], - output: "console.error(void console.log('foo'));", + errors: [ + { + line: 1, + column: 15, + messageId: 'invalidVoidExprWrapVoid', + suggestions: [ + { + messageId: 'voidExprWrapVoid', + output: "console.error(void console.log('foo'));", + }, + ], + }, + ], }, { options: [{ ignoreVoidOperator: true }], code: "console.log('foo') ? true : false;", - errors: [{ line: 1, column: 1, messageId: 'invalidVoidExprWrapVoid' }], - output: "void console.log('foo') ? true : false;", + errors: [ + { + line: 1, + column: 1, + messageId: 'invalidVoidExprWrapVoid', + suggestions: [ + { + messageId: 'voidExprWrapVoid', + output: "void console.log('foo') ? true : false;", + }, + ], + }, + ], }, { options: [{ ignoreVoidOperator: true }], code: "const x = foo ?? console.log('foo');", - errors: [{ line: 1, column: 18, messageId: 'invalidVoidExprWrapVoid' }], - output: "const x = foo ?? void console.log('foo');", + errors: [ + { + line: 1, + column: 18, + messageId: 'invalidVoidExprWrapVoid', + suggestions: [ + { + messageId: 'voidExprWrapVoid', + output: "const x = foo ?? void console.log('foo');", + }, + ], + }, + ], }, { options: [{ ignoreVoidOperator: true }], @@ -205,8 +238,19 @@ ruleTester.run('no-void-expression', rule, { { options: [{ ignoreVoidOperator: true }], code: "!!console.log('foo');", - errors: [{ line: 1, column: 3, messageId: 'invalidVoidExprWrapVoid' }], - output: "!!void console.log('foo');", + errors: [ + { + line: 1, + column: 3, + messageId: 'invalidVoidExprWrapVoid', + suggestions: [ + { + messageId: 'voidExprWrapVoid', + output: "!!void console.log('foo');", + }, + ], + }, + ], }, ], }); From d116ab346588f8506e5b0a3e34ae474b464891b3 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 28 Sep 2020 00:13:22 +0200 Subject: [PATCH 06/25] feat(eslint-plugin): oops --- packages/eslint-plugin/src/rules/no-void-expression.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 912ad756ecd..c520649360a 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -127,7 +127,6 @@ export default util.createRule({ fix(fixer) { const arrowBody = arrowFunction.body; const arrowBodyText = sourceCode.getText(arrowBody); - isParenthesized; const newArrowBodyText = `{ ${arrowBodyText}; }`; if (isParenthesized(arrowBody, sourceCode)) { const [bodyOpeningParen, bodyClosingParen] = [ From 7d54e93ef11b6675860a1cde01f63a2a569954be Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 28 Sep 2020 00:28:53 +0200 Subject: [PATCH 07/25] feat(eslint-plugin): meh --- packages/eslint-plugin/src/rules/no-void-expression.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index c520649360a..12f068c789c 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -188,7 +188,6 @@ export default util.createRule({ } // move before the `return` keyword - // this would be reported by this rule btw. such irony return context.report({ node, messageId: 'invalidVoidExprReturn', @@ -208,6 +207,7 @@ export default util.createRule({ // handle generic case if (options.ignoreVoidOperator) { + // this would be reported by this rule btw. such irony return context.report({ node, messageId: 'invalidVoidExprWrapVoid', From 877e84c6a2c2b62473458fe1fb0b3db5212babf2 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 28 Sep 2020 00:33:55 +0200 Subject: [PATCH 08/25] feat(eslint-plugin): the words short-circuit --- packages/eslint-plugin/src/rules/no-void-expression.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 12f068c789c..3424a608b88 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -224,8 +224,8 @@ export default util.createRule({ /** * Inspects the void expression's ancestors and finds closest invalid one. * By default anything other than an ExpressionStatement is invalid. - * Parent expressions which can be used for their side effects are ignored - * and their parents are checked instead. + * Parent expressions which can be used for their short-circuiting behavior + * are ignored and their parents are checked instead. * @param node The void expression node to check. * @returns Invalid ancestor node if it was found. `null` otherwise. */ From 4e7afea67a82d30283dc2280c9a036232f48b641 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 28 Sep 2020 00:41:37 +0200 Subject: [PATCH 09/25] feat(eslint-plugin): better tests --- .../tests/rules/no-void-expression.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index 89d63ef6232..d593bd5d69a 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -40,15 +40,15 @@ ruleTester.run('no-void-expression', rule, { ...batchedSingleLineTests({ options: [{ ignoreVoidOperator: true }], code: ` - void console.log('foo'); - void foo && console.log(foo); - void foo || console.log(foo); - void (foo && console.log(true)) || console.log(false); - void (foo ? console.log(true) : console.log(false)); - foo && void console.log(foo); - foo || void console.log(foo); - (foo && void console.log(true)) || void console.log(false); - foo ? void console.log(true) : void console.log(false); + !void console.log('foo'); + +void (foo && console.log(foo)); + -void (foo || console.log(foo)); + () => void ((foo && void console.log(true)) || console.log(false)); + const x = void (foo ? console.log(true) : console.log(false)); + !(foo && void console.log(foo)); + !!(foo || void console.log(foo)); + const x = (foo && void console.log(true)) || void console.log(false); + () => (foo ? void console.log(true) : void console.log(false)); return void console.log('foo'); `, }), From a2d091381591cb3700276da90344493df3415831 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Tue, 29 Sep 2020 00:43:55 +0200 Subject: [PATCH 10/25] feat(eslint-plugin): clearer phrasing in docs Co-authored-by: Tadhg McDonald-Jensen --- packages/eslint-plugin/docs/rules/no-void-expression.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index 3ad10ccf3a6..c812ab96d3d 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -1,6 +1,6 @@ # Requires expressions of type void to appear in statement position (`no-void-expression`) -It’s misleading returning the results of an expression whose type is void. +Returning the results of an expression whose type is void can be misleading. Attempting to do so is likely a symptom of expecting a different return type from a function. Even if used correctly, it can be misleading for other developers, who don't know what a particular function does and if its result matters. From a24072236051a06259e0534fd67d92b50db701e4 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Tue, 29 Sep 2020 01:38:23 +0200 Subject: [PATCH 11/25] feat(eslint-plugin): code review fixes --- .../docs/rules/no-void-expression.md | 28 +++++++++++++++---- .../src/rules/no-void-expression.ts | 5 ++-- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index c812ab96d3d..d63fe2734d1 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -64,7 +64,16 @@ An object option can be specified. Each boolean flag makes the rule less strict. ### `ignoreArrowShorthand` -`"no-void-expression": ["error", { "ignoreArrowShorthand": true }]` +`false` by default. + +```json +{ + "@typescript-eslint/no-void-expression": [ + "error", + { "ignoreArrowShorthand": true } + ] +} +``` It might be undesirable to wrap every arrow function shorthand expression with braces. Especially when using Prettier formatter, which spreads such code across 3 lines instead of 1. @@ -77,7 +86,16 @@ promise.then(value => window.postMessage(value)); ### `ignoreVoidOperator` -`"no-void-expression": ["error", { "ignoreVoidOperator": true }]` +`false` by default. + +```json +{ + "@typescript-eslint/no-void-expression": [ + "error", + { "ignoreVoidOperator": true } + ] +} +``` It might be preferable to only use some distinct syntax to explicitly mark the confusing but valid usage of void expressions. @@ -90,17 +108,17 @@ It also enables a suggestion fix to wrap the void expression with `void` operato Examples of additional **correct** code with this option enabled: ```ts -// now it's obvious that we don't expect any response (fixable) +// now it's obvious that we don't expect any response promise.then(value => void window.postMessage(value)); -// now it's explicit that we don't want to return anything (fixable) +// now it's explicit that we don't want to return anything function doSomething() { if (!somethingToDo) return void console.error('Nothing to do!'); console.log('Doing a thing...'); } -// we are sure that we want to always log `undefined` (fixable via suggestion) +// we are sure that we want to always log `undefined` console.log(void alert('Hello, world!')); ``` diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 3424a608b88..64cc776388c 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -42,10 +42,11 @@ export default util.createRule({ messages: { invalidVoidExpr: 'Placing a void expression inside another expression is forbidden. ' + - "Move it to it's own statement instead.", + 'Move it to its own statement instead.', invalidVoidExprWrapVoid: 'Void expressions used inside another expression ' + - 'must be marked explicitly with the `void` operator.', + 'must be moved to its own statement ' + + 'or marked explicitly with the `void` operator.', invalidVoidExprArrow: 'Returning a void expression from an arrow function shorthand is forbidden. ' + 'Please add braces to the arrow function.', From ee3e57a557fda2feaeb71a85e15dbd8ebc9ea07c Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Tue, 29 Sep 2020 02:19:58 +0200 Subject: [PATCH 12/25] feat(eslint-plugin): spaces in messages --- packages/eslint-plugin/src/rules/no-void-expression.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 64cc776388c..5b99601f970 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -54,10 +54,10 @@ export default util.createRule({ 'Void expressions returned from an arrow function shorthand ' + 'must be marked explicitly with the `void` operator.', invalidVoidExprReturn: - 'Returning a void expression from a function is forbidden.' + + 'Returning a void expression from a function is forbidden. ' + 'Please move it before the `return` statement.', invalidVoidExprReturnLast: - 'Returning a void expression from a function is forbidden.' + + 'Returning a void expression from a function is forbidden. ' + 'Please remove the `return` statement.', invalidVoidExprReturnWrapVoid: 'Void expressions returned from a function ' + From f353daab6f1c91ad78a1cf5798f0e7b346ca61b8 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Tue, 29 Sep 2020 02:23:40 +0200 Subject: [PATCH 13/25] feat(eslint-plugin): use rule in its own impl --- .../eslint-plugin/src/rules/no-void-expression.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 5b99601f970..f3d1dcdf868 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -1,3 +1,5 @@ +/* eslint-disable eslint-comments/no-use */ +/* eslint @typescript-eslint/no-void-expression: [error, { ignoreVoidOperator: true }] */ import { AST_NODE_TYPES, TSESLint, @@ -113,7 +115,7 @@ export default util.createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle wrapping with `void` if (options.ignoreVoidOperator) { - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprArrowWrapVoid', fix: wrapVoidFix, @@ -122,7 +124,7 @@ export default util.createRule({ // handle wrapping with braces const arrowFunction = invalidAncestor; - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprArrow', fix(fixer) { @@ -148,7 +150,7 @@ export default util.createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle wrapping with `void` if (options.ignoreVoidOperator) { - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprReturnWrapVoid', fix: wrapVoidFix, @@ -176,7 +178,7 @@ export default util.createRule({ // remove the `return` keyword if (isTopLevelReturn && isLastReturn) { - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprReturnLast', fix(fixer) { @@ -189,7 +191,7 @@ export default util.createRule({ } // move before the `return` keyword - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprReturn', fix(fixer) { @@ -208,8 +210,7 @@ export default util.createRule({ // handle generic case if (options.ignoreVoidOperator) { - // this would be reported by this rule btw. such irony - return context.report({ + return void context.report({ node, messageId: 'invalidVoidExprWrapVoid', suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }], From e1adee663c42e3fd23b974598cc3df990aca7881 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 13:54:53 +0200 Subject: [PATCH 14/25] docs(eslint-plugin): options type and defaults Co-authored-by: Brad Zacher --- .../eslint-plugin/docs/rules/no-void-expression.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index d63fe2734d1..b2f1feeabbb 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -62,6 +62,18 @@ cond ? console.log('true') : console.error('false'); An object option can be specified. Each boolean flag makes the rule less strict. +```ts +type Options = { + ignoreArrowShorthand?: boolean; + ignoreVoidOperator?: boolean; +}; + +const defaults: Options = { + ignoreArrowShorthand: false, + ignoreVoidOperator: false, +}; +``` + ### `ignoreArrowShorthand` `false` by default. From 6a45fe29068a00a588d556f2cf8b1095b1f5be67 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 14:01:55 +0200 Subject: [PATCH 15/25] style(eslint-plugin): code style Co-authored-by: Brad Zacher --- packages/eslint-plugin/docs/rules/no-void-expression.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index b2f1feeabbb..91b14026ff8 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -21,7 +21,9 @@ promise.then(value => window.postMessage(value)); // it looks like we are returning the result of `console.error` (fixable) function doSomething() { - if (!somethingToDo) return console.error('Nothing to do!'); + if (!somethingToDo) { + return console.error('Nothing to do!'); + } console.log('Doing a thing...'); } From 4ae9466a1702f6c07ef52e6ae1605f2f0ac698af Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 14:02:11 +0200 Subject: [PATCH 16/25] style(eslint-plugin): code style Co-authored-by: Brad Zacher --- packages/eslint-plugin/docs/rules/no-void-expression.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-void-expression.md index 91b14026ff8..1d45342d34c 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-void-expression.md @@ -127,7 +127,9 @@ promise.then(value => void window.postMessage(value)); // now it's explicit that we don't want to return anything function doSomething() { - if (!somethingToDo) return void console.error('Nothing to do!'); + if (!somethingToDo) { + return void console.error('Nothing to do!'); + } console.log('Doing a thing...'); } From 20c341f4e2c32fcfcc7daa67cedd46aacaf40539 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 14:03:40 +0200 Subject: [PATCH 17/25] style(eslint-plugin): nullThrows Co-authored-by: Brad Zacher --- packages/eslint-plugin/src/rules/no-void-expression.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index f3d1dcdf868..0bb5c8733ed 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -232,12 +232,10 @@ export default util.createRule({ * @returns Invalid ancestor node if it was found. `null` otherwise. */ function findInvalidAncestor(node: TSESTree.Node): TSESTree.Node | null { - const { parent } = node; - - if (parent == null) { - // if there is no parent then let's say it's valid - return null; - } + const parent = util.nullThrows( + node.parent, + util.NullThrowsReasons.MissingParent, + ); if (parent.type === AST_NODE_TYPES.ExpressionStatement) { // e.g. `{ console.log("foo"); }` From 95597ab977a2fd52a2920a82030c137233e9b5cc Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 14:05:33 +0200 Subject: [PATCH 18/25] style(eslint-plugin): remove array destructuring Co-authored-by: Brad Zacher --- packages/eslint-plugin/src/rules/no-void-expression.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 0bb5c8733ed..656824ec724 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -132,10 +132,8 @@ export default util.createRule({ const arrowBodyText = sourceCode.getText(arrowBody); const newArrowBodyText = `{ ${arrowBodyText}; }`; if (isParenthesized(arrowBody, sourceCode)) { - const [bodyOpeningParen, bodyClosingParen] = [ - sourceCode.getTokenBefore(arrowBody, isOpeningParenToken)!, - sourceCode.getTokenAfter(arrowBody, isClosingParenToken)!, - ]; + const bodyOpeningParen = sourceCode.getTokenBefore(arrowBody, isOpeningParenToken)!; + const bodyClosingParen = sourceCode.getTokenAfter(arrowBody, isClosingParenToken)!; return fixer.replaceTextRange( [bodyOpeningParen.range[0], bodyClosingParen.range[1]], newArrowBodyText, From 25913a6da866ca9859042d70e8d41185737c4260 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 14:21:25 +0200 Subject: [PATCH 19/25] fix(eslint-plugin): revert use rule in own impl This reverts commit 7d86695ab48fac5114fc85f762a639c077f98d1f. --- .../src/rules/no-void-expression.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 656824ec724..1d6a69df89d 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -1,5 +1,3 @@ -/* eslint-disable eslint-comments/no-use */ -/* eslint @typescript-eslint/no-void-expression: [error, { ignoreVoidOperator: true }] */ import { AST_NODE_TYPES, TSESLint, @@ -115,7 +113,7 @@ export default util.createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { // handle wrapping with `void` if (options.ignoreVoidOperator) { - return void context.report({ + return context.report({ node, messageId: 'invalidVoidExprArrowWrapVoid', fix: wrapVoidFix, @@ -124,7 +122,7 @@ export default util.createRule({ // handle wrapping with braces const arrowFunction = invalidAncestor; - return void context.report({ + return context.report({ node, messageId: 'invalidVoidExprArrow', fix(fixer) { @@ -132,8 +130,14 @@ export default util.createRule({ const arrowBodyText = sourceCode.getText(arrowBody); const newArrowBodyText = `{ ${arrowBodyText}; }`; if (isParenthesized(arrowBody, sourceCode)) { - const bodyOpeningParen = sourceCode.getTokenBefore(arrowBody, isOpeningParenToken)!; - const bodyClosingParen = sourceCode.getTokenAfter(arrowBody, isClosingParenToken)!; + const bodyOpeningParen = sourceCode.getTokenBefore( + arrowBody, + isOpeningParenToken, + )!; + const bodyClosingParen = sourceCode.getTokenAfter( + arrowBody, + isClosingParenToken, + )!; return fixer.replaceTextRange( [bodyOpeningParen.range[0], bodyClosingParen.range[1]], newArrowBodyText, @@ -148,7 +152,7 @@ export default util.createRule({ if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { // handle wrapping with `void` if (options.ignoreVoidOperator) { - return void context.report({ + return context.report({ node, messageId: 'invalidVoidExprReturnWrapVoid', fix: wrapVoidFix, @@ -176,7 +180,7 @@ export default util.createRule({ // remove the `return` keyword if (isTopLevelReturn && isLastReturn) { - return void context.report({ + return context.report({ node, messageId: 'invalidVoidExprReturnLast', fix(fixer) { @@ -189,7 +193,7 @@ export default util.createRule({ } // move before the `return` keyword - return void context.report({ + return context.report({ node, messageId: 'invalidVoidExprReturn', fix(fixer) { @@ -208,7 +212,8 @@ export default util.createRule({ // handle generic case if (options.ignoreVoidOperator) { - return void context.report({ + // this would be reported by this rule btw. such irony + return context.report({ node, messageId: 'invalidVoidExprWrapVoid', suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }], From ebf8c7df7a0bdc6968b0189e04001c26924fafc5 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 16:23:23 +0200 Subject: [PATCH 20/25] docs(eslint-plugin): update ROADMAP --- packages/eslint-plugin/ROADMAP.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index aece78f3357..74502e55095 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -96,7 +96,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th | [`no-unused-variable`] | 🌓 | [`@typescript-eslint/no-unused-vars`] | | [`no-use-before-declare`] | ✅ | [`@typescript-eslint/no-use-before-define`] | | [`no-var-keyword`] | 🌟 | [`no-var`][no-var] | -| [`no-void-expression`] | 🛑 | N/A (unrelated to the similarly named ESLint rule `no-void`) | +| [`no-void-expression`] | ✅ | [`@typescript-eslint/no-void-expression`] | | [`prefer-conditional-expression`] | 🛑 | N/A | | [`prefer-object-spread`] | 🌟 | [`prefer-object-spread`][prefer-object-spread] | | [`radix`] | 🌟 | [`radix`][radix] | @@ -650,6 +650,8 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/no-floating-promises`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-floating-promises.md [`@typescript-eslint/no-magic-numbers`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-magic-numbers.md [`@typescript-eslint/no-unsafe-member-access`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-member-access.md +[`@typescript-eslint/restrict-template-expressions`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-template-expressions.md +[`@typescript-eslint/no-void-expression`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-void-expression.md From 3038e04ab7d929d16781cfc5cc871085f270904b Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 17:18:35 +0200 Subject: [PATCH 21/25] style(eslint-plugin): enforce order via config --- .eslintrc.js | 9 +++++++++ packages/eslint-plugin/src/rules/index.ts | 2 -- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c789421b024..1c18917bbcb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -221,6 +221,15 @@ module.exports = { '@typescript-eslint/internal/plugin-test-formatting': 'error', }, }, + // files which list all the things + { + files: ['packages/eslint-plugin/src/rules/index.ts'], + rules: { + // enforce alphabetical ordering + 'sort-keys': 'error', + 'import/order': ['error', { alphabetize: { order: 'asc' } }], + }, + }, // tools and tests { files: ['**/tools/**/*.ts', '**/tests/**/*.ts'], diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index a9322e92626..19fb6a06939 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -1,5 +1,3 @@ -/* eslint-disable eslint-comments/no-use */ -/* eslint sort-keys: error */ import adjacentOverloadSignatures from './adjacent-overload-signatures'; import arrayType from './array-type'; import awaitThenable from './await-thenable'; From 9596ef5e41080e2d98b28de1f68681e5d025cad9 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 4 Oct 2020 21:40:28 +0200 Subject: [PATCH 22/25] fix(eslint-plugin): handle no semicolon --- .../src/rules/no-void-expression.ts | 109 +++++++++++++----- .../tests/rules/no-void-expression.test.ts | 32 +++++ 2 files changed, 109 insertions(+), 32 deletions(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 1d6a69df89d..6a8d0fcf808 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -1,16 +1,12 @@ import { AST_NODE_TYPES, + AST_TOKEN_TYPES, TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import * as tsutils from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; -import { - isClosingParenToken, - isOpeningParenToken, - isParenthesized, -} from '../util'; export type Options = [ { @@ -109,10 +105,11 @@ export default util.createRule({ return fixer.replaceText(node, newNodeText); }; - // handle arrow function shorthand if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) { - // handle wrapping with `void` + // handle arrow function shorthand + if (options.ignoreVoidOperator) { + // handle wrapping with `void` return context.report({ node, messageId: 'invalidVoidExprArrowWrapVoid', @@ -129,14 +126,14 @@ export default util.createRule({ const arrowBody = arrowFunction.body; const arrowBodyText = sourceCode.getText(arrowBody); const newArrowBodyText = `{ ${arrowBodyText}; }`; - if (isParenthesized(arrowBody, sourceCode)) { + if (util.isParenthesized(arrowBody, sourceCode)) { const bodyOpeningParen = sourceCode.getTokenBefore( arrowBody, - isOpeningParenToken, + util.isOpeningParenToken, )!; const bodyClosingParen = sourceCode.getTokenAfter( arrowBody, - isClosingParenToken, + util.isClosingParenToken, )!; return fixer.replaceTextRange( [bodyOpeningParen.range[0], bodyClosingParen.range[1]], @@ -148,10 +145,11 @@ export default util.createRule({ }); } - // handle return statement if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) { - // handle wrapping with `void` + // handle return statement + if (options.ignoreVoidOperator) { + // handle wrapping with `void` return context.report({ node, messageId: 'invalidVoidExprReturnWrapVoid', @@ -159,34 +157,21 @@ export default util.createRule({ }); } - // handle moving to it's own statement const returnStmt = invalidAncestor; - let isTopLevelReturn = false; - let isLastReturn = false; - if (returnStmt.parent?.type === AST_NODE_TYPES.BlockStatement) { - const block = returnStmt.parent; - const blockType = block.parent?.type; - if ( - blockType === AST_NODE_TYPES.FunctionDeclaration || - blockType === AST_NODE_TYPES.FunctionExpression || - blockType === AST_NODE_TYPES.ArrowFunctionExpression - ) { - isTopLevelReturn = true; - } - if (block.body.indexOf(returnStmt) === block.body.length - 1) { - isLastReturn = true; - } - } - // remove the `return` keyword - if (isTopLevelReturn && isLastReturn) { + if (isFinalReturn(returnStmt)) { + // remove the `return` keyword return context.report({ node, messageId: 'invalidVoidExprReturnLast', fix(fixer) { const returnValue = returnStmt.argument!; const returnValueText = sourceCode.getText(returnValue); - const newReturnStmtText = `${returnValueText};`; + let newReturnStmtText = `${returnValueText};`; + if (isPreventingASI(returnValue, sourceCode)) { + // put a semicolon at the beginning of the line + newReturnStmtText = `;${newReturnStmtText}`; + } return fixer.replaceText(returnStmt, newReturnStmtText); }, }); @@ -200,6 +185,10 @@ export default util.createRule({ const returnValue = returnStmt.argument!; const returnValueText = sourceCode.getText(returnValue); let newReturnStmtText = `${returnValueText}; return;`; + if (isPreventingASI(returnValue, sourceCode)) { + // put a semicolon at the beginning of the line + newReturnStmtText = `;${newReturnStmtText}`; + } if (returnStmt.parent?.type !== AST_NODE_TYPES.BlockStatement) { // e.g. `if (cond) return console.error();` // add braces if not inside a block @@ -283,5 +272,61 @@ export default util.createRule({ // any other parent is invalid return parent; } + + /** Checks whether the return statement is the last statement in a function body. */ + function isFinalReturn(node: TSESTree.ReturnStatement): boolean { + // the parent must be a block + const block = util.nullThrows( + node.parent, + util.NullThrowsReasons.MissingParent, + ); + if (block.type !== AST_NODE_TYPES.BlockStatement) { + // e.g. `if (cond) return;` (not in a block) + return false; + } + + // the block's parent must be a function + const blockParent = util.nullThrows( + block.parent, + util.NullThrowsReasons.MissingParent, + ); + if ( + ![ + AST_NODE_TYPES.FunctionDeclaration, + AST_NODE_TYPES.FunctionExpression, + AST_NODE_TYPES.ArrowFunctionExpression, + ].includes(blockParent.type) + ) { + // e.g. `if (cond) { return; }` + // not in a top-level function block + return false; + } + + // must be the last child of the block + if (block.body.indexOf(node) < block.body.length - 1) { + // not the last statement in the block + return false; + } + + return true; + } + + /** + * Checks whether the given node, if placed on its own line, + * would prevent automatic semicolon insertion on the line before. + * + * This happens if the line begins with `(`, `[` or `` ` `` + */ + function isPreventingASI( + node: TSESTree.Expression, + sourceCode: Readonly, + ): boolean { + const startToken = util.nullThrows( + sourceCode.getFirstToken(node), + util.NullThrowsReasons.MissingToken('first token', node.type), + ); + + return ['(', '[', '`'].includes(startToken.value); + } }, }); diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts index d593bd5d69a..8d3bbd779dd 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-void-expression.test.ts @@ -118,6 +118,23 @@ ruleTester.run('no-void-expression', rule, { } `, }, + { + code: noFormat` + function f() { + console.log('foo') + return ['bar', 'baz'].forEach(console.log) + console.log('quux') + } + `, + errors: [{ line: 4, column: 18, messageId: 'invalidVoidExprReturn' }], + output: noFormat` + function f() { + console.log('foo') + ;['bar', 'baz'].forEach(console.log); return; + console.log('quux') + } + `, + }, { code: ` function f() { @@ -133,6 +150,21 @@ ruleTester.run('no-void-expression', rule, { } `, }, + { + code: noFormat` + function f() { + console.log('foo') + return ['bar', 'baz'].forEach(console.log) + } + `, + errors: [{ line: 4, column: 18, messageId: 'invalidVoidExprReturnLast' }], + output: noFormat` + function f() { + console.log('foo') + ;['bar', 'baz'].forEach(console.log); + } + `, + }, { code: ` const f = () => { From 1ac96bfee7115ee8301486ce0f1fd977f120645e Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 1 Nov 2020 14:31:43 +0100 Subject: [PATCH 23/25] fix(eslint-plugin): remove unused import --- packages/eslint-plugin/src/rules/no-void-expression.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-void-expression.ts index 6a8d0fcf808..997ac5d5bc2 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-void-expression.ts @@ -1,6 +1,5 @@ import { AST_NODE_TYPES, - AST_TOKEN_TYPES, TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; From 4e4c77cf87ecdf36cc70f6ee2e9f654bb34a9985 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Sun, 1 Nov 2020 14:56:36 +0100 Subject: [PATCH 24/25] fix(eslint-plugin): sort rules --- packages/eslint-plugin/src/rules/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index e3246b9b33e..f50d9c307ba 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -14,7 +14,6 @@ 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'; @@ -103,6 +102,7 @@ import restrictTemplateExpressions from './restrict-template-expressions'; import returnAwait from './return-await'; import semi from './semi'; import spaceBeforeFunctionParen from './space-before-function-paren'; +import spaceInfixOps from './space-infix-ops'; import strictBooleanExpressions from './strict-boolean-expressions'; import switchExhaustivenessCheck from './switch-exhaustiveness-check'; import tripleSlashReference from './triple-slash-reference'; @@ -128,7 +128,6 @@ 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, @@ -217,6 +216,7 @@ export default { 'return-await': returnAwait, semi: semi, 'space-before-function-paren': spaceBeforeFunctionParen, + 'space-infix-ops': spaceInfixOps, 'strict-boolean-expressions': strictBooleanExpressions, 'switch-exhaustiveness-check': switchExhaustivenessCheck, 'triple-slash-reference': tripleSlashReference, From 29fca40b96aa576a8b243ad72b306ca90b3b85c0 Mon Sep 17 00:00:00 2001 From: Nikita Stefaniak Date: Mon, 2 Nov 2020 23:54:25 +0100 Subject: [PATCH 25/25] chore(eslint-plugin): rename new rule --- packages/eslint-plugin/README.md | 2 +- packages/eslint-plugin/ROADMAP.md | 4 ++-- ...-void-expression.md => no-confusing-void-expression.md} | 6 +++--- packages/eslint-plugin/src/configs/all.ts | 2 +- packages/eslint-plugin/src/rules/index.ts | 4 ++-- ...-void-expression.ts => no-confusing-void-expression.ts} | 2 +- ...ession.test.ts => no-confusing-void-expression.test.ts} | 7 +++++-- 7 files changed, 15 insertions(+), 12 deletions(-) rename packages/eslint-plugin/docs/rules/{no-void-expression.md => no-confusing-void-expression.md} (96%) rename packages/eslint-plugin/src/rules/{no-void-expression.ts => no-confusing-void-expression.ts} (99%) rename packages/eslint-plugin/tests/rules/{no-void-expression.test.ts => no-confusing-void-expression.test.ts} (98%) diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index b02a65a9286..3dcb079be5a 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -117,6 +117,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/naming-convention`](./docs/rules/naming-convention.md) | Enforces naming conventions for everything across a codebase | | | :thought_balloon: | | [`@typescript-eslint/no-base-to-string`](./docs/rules/no-base-to-string.md) | Requires that `.toString()` is only called on objects which provide useful information when stringified | | | :thought_balloon: | | [`@typescript-eslint/no-confusing-non-null-assertion`](./docs/rules/no-confusing-non-null-assertion.md) | Disallow non-null assertion in locations that may be confusing | | :wrench: | | +| [`@typescript-eslint/no-confusing-void-expression`](./docs/rules/no-confusing-void-expression.md) | Requires expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | | [`@typescript-eslint/no-dynamic-delete`](./docs/rules/no-dynamic-delete.md) | Disallow the delete operator with computed key expressions | | :wrench: | | | [`@typescript-eslint/no-empty-interface`](./docs/rules/no-empty-interface.md) | Disallow the declaration of empty interfaces | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/no-explicit-any`](./docs/rules/no-explicit-any.md) | Disallow usage of the `any` type | :heavy_check_mark: | :wrench: | | @@ -149,7 +150,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | -| [`@typescript-eslint/no-void-expression`](./docs/rules/no-void-expression.md) | Requires expressions of type void to appear in statement position | | :wrench: | :thought_balloon: | | [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | | | [`@typescript-eslint/prefer-enum-initializers`](./docs/rules/prefer-enum-initializers.md) | Prefer initializing each enums member value | | | | | [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | | diff --git a/packages/eslint-plugin/ROADMAP.md b/packages/eslint-plugin/ROADMAP.md index 74502e55095..749ba2bdf82 100644 --- a/packages/eslint-plugin/ROADMAP.md +++ b/packages/eslint-plugin/ROADMAP.md @@ -96,7 +96,7 @@ It lists all TSLint rules along side rules from the ESLint ecosystem that are th | [`no-unused-variable`] | 🌓 | [`@typescript-eslint/no-unused-vars`] | | [`no-use-before-declare`] | ✅ | [`@typescript-eslint/no-use-before-define`] | | [`no-var-keyword`] | 🌟 | [`no-var`][no-var] | -| [`no-void-expression`] | ✅ | [`@typescript-eslint/no-void-expression`] | +| [`no-void-expression`] | ✅ | [`@typescript-eslint/no-confusing-void-expression`] | | [`prefer-conditional-expression`] | 🛑 | N/A | | [`prefer-object-spread`] | 🌟 | [`prefer-object-spread`][prefer-object-spread] | | [`radix`] | 🌟 | [`radix`][radix] | @@ -651,7 +651,7 @@ Relevant plugins: [`chai-expect-keywords`](https://github.com/gavinaiken/eslint- [`@typescript-eslint/no-magic-numbers`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-magic-numbers.md [`@typescript-eslint/no-unsafe-member-access`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unsafe-member-access.md [`@typescript-eslint/restrict-template-expressions`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/restrict-template-expressions.md -[`@typescript-eslint/no-void-expression`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-void-expression.md +[`@typescript-eslint/no-confusing-void-expression`]: https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md diff --git a/packages/eslint-plugin/docs/rules/no-void-expression.md b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md similarity index 96% rename from packages/eslint-plugin/docs/rules/no-void-expression.md rename to packages/eslint-plugin/docs/rules/no-confusing-void-expression.md index 1d45342d34c..8a00255170d 100644 --- a/packages/eslint-plugin/docs/rules/no-void-expression.md +++ b/packages/eslint-plugin/docs/rules/no-confusing-void-expression.md @@ -1,4 +1,4 @@ -# Requires expressions of type void to appear in statement position (`no-void-expression`) +# Requires expressions of type void to appear in statement position (`no-confusing-void-expression`) Returning the results of an expression whose type is void can be misleading. Attempting to do so is likely a symptom of expecting a different return type from a function. @@ -82,7 +82,7 @@ const defaults: Options = { ```json { - "@typescript-eslint/no-void-expression": [ + "@typescript-eslint/no-confusing-void-expression": [ "error", { "ignoreArrowShorthand": true } ] @@ -104,7 +104,7 @@ promise.then(value => window.postMessage(value)); ```json { - "@typescript-eslint/no-void-expression": [ + "@typescript-eslint/no-confusing-void-expression": [ "error", { "ignoreVoidOperator": true } ] diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index c66a2f30837..d17db11d0b5 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -47,6 +47,7 @@ export = { '@typescript-eslint/no-array-constructor': 'error', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-non-null-assertion': 'error', + '@typescript-eslint/no-confusing-void-expression': 'error', 'no-dupe-class-members': 'off', '@typescript-eslint/no-dupe-class-members': 'error', 'no-duplicate-imports': 'off', @@ -109,7 +110,6 @@ export = { 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'error', '@typescript-eslint/no-var-requires': 'error', - '@typescript-eslint/no-void-expression': 'error', '@typescript-eslint/prefer-as-const': 'error', '@typescript-eslint/prefer-enum-initializers': 'error', '@typescript-eslint/prefer-for-of': 'error', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index f50d9c307ba..de982a91d87 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -29,6 +29,7 @@ import namingConvention from './naming-convention'; import noArrayConstructor from './no-array-constructor'; import noBaseToString from './no-base-to-string'; import confusingNonNullAssertionLikeNotEqual from './no-confusing-non-null-assertion'; +import noConfusingVoidExpression from './no-confusing-void-expression'; import noDupeClassMembers from './no-dupe-class-members'; import noDuplicateImports from './no-duplicate-imports'; import noDynamicDelete from './no-dynamic-delete'; @@ -77,7 +78,6 @@ import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUseBeforeDefine from './no-use-before-define'; import noUselessConstructor from './no-useless-constructor'; import noVarRequires from './no-var-requires'; -import noVoidExpression from './no-void-expression'; import preferAsConst from './prefer-as-const'; import preferEnumInitializers from './prefer-enum-initializers'; import preferForOf from './prefer-for-of'; @@ -143,6 +143,7 @@ export default { 'no-array-constructor': noArrayConstructor, 'no-base-to-string': noBaseToString, 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, + 'no-confusing-void-expression': noConfusingVoidExpression, 'no-dupe-class-members': noDupeClassMembers, 'no-duplicate-imports': noDuplicateImports, 'no-dynamic-delete': noDynamicDelete, @@ -191,7 +192,6 @@ export default { 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, 'no-var-requires': noVarRequires, - 'no-void-expression': noVoidExpression, 'prefer-as-const': preferAsConst, 'prefer-enum-initializers': preferEnumInitializers, 'prefer-for-of': preferForOf, diff --git a/packages/eslint-plugin/src/rules/no-void-expression.ts b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts similarity index 99% rename from packages/eslint-plugin/src/rules/no-void-expression.ts rename to packages/eslint-plugin/src/rules/no-confusing-void-expression.ts index 997ac5d5bc2..a9b715350e9 100644 --- a/packages/eslint-plugin/src/rules/no-void-expression.ts +++ b/packages/eslint-plugin/src/rules/no-confusing-void-expression.ts @@ -25,7 +25,7 @@ export type MessageId = | 'voidExprWrapVoid'; export default util.createRule({ - name: 'no-void-expression', + name: 'no-confusing-void-expression', meta: { docs: { description: diff --git a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts similarity index 98% rename from packages/eslint-plugin/tests/rules/no-void-expression.test.ts rename to packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts index 8d3bbd779dd..3586c3388f4 100644 --- a/packages/eslint-plugin/tests/rules/no-void-expression.test.ts +++ b/packages/eslint-plugin/tests/rules/no-confusing-void-expression.test.ts @@ -1,4 +1,7 @@ -import rule, { MessageId, Options } from '../../src/rules/no-void-expression'; +import rule, { + MessageId, + Options, +} from '../../src/rules/no-confusing-void-expression'; import { batchedSingleLineTests, getFixturesRootDir, @@ -15,7 +18,7 @@ const ruleTester = new RuleTester({ }, }); -ruleTester.run('no-void-expression', rule, { +ruleTester.run('no-confusing-void-expression', rule, { valid: [ ...batchedSingleLineTests({ code: `