Skip to content

Commit

Permalink
feat(eslint-plugin): add no-meaningless-void-operator rule (#3641)
Browse files Browse the repository at this point in the history
  • Loading branch information
jtbandes committed Sep 3, 2021
1 parent f79ae9b commit ea40ab6
Show file tree
Hide file tree
Showing 6 changed files with 244 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Expand Up @@ -128,6 +128,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-implicit-any-catch`](./docs/rules/no-implicit-any-catch.md) | Disallow usage of the implicit `any` type in catch clauses | | :wrench: | |
| [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :white_check_mark: | :wrench: | |
| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallows usage of `void` type outside of generic or return types | | | |
| [`@typescript-eslint/no-meaningless-void-operator`](./docs/rules/no-meaningless-void-operator.md) | Disallow the `void` operator except when used to discard a value | | :wrench: | :thought_balloon: |
| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | |
| [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :white_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :white_check_mark: | | |
Expand Down
50 changes: 50 additions & 0 deletions packages/eslint-plugin/docs/rules/no-meaningless-void-operator.md
@@ -0,0 +1,50 @@
# Disallow the `void` operator except when used to discard a value (`no-meaningless-void-operator`)

Disallow the `void` operator when its argument is already of type `void` or `undefined`.

## Rule Details

The `void` operator is a useful tool to convey the programmer's intent to discard a value. For example, it is recommended as one way of suppressing [`@typescript-eslint/no-floating-promises`](./no-floating-promises.md) instead of adding `.catch()` to a promise.

This rule helps an author catch API changes where previously a value was being discarded at a call site, but the callee changed so it no longer returns a value. When combined with [no-unused-expressions](https://eslint.org/docs/rules/no-unused-expressions), it also helps _readers_ of the code by ensuring consistency: a statement that looks like `void foo();` is **always** discarding a return value, and a statement that looks like `foo();` is **never** discarding a return value.

Examples of **incorrect** code for this rule:

```ts
void (() => {})();

function foo() {}
void foo();
```

Examples of **correct** code for this rule:

```ts
(() => {})();

function foo() {}
foo(); // nothing to discard

function bar(x: number) {
void x; // discarding a number
return 2;
}
void bar(); // discarding a number
```

### Options

This rule accepts a single object option with the following default configuration:

```json
{
"@typescript-eslint/no-meaningless-void-operator": [
"error",
{
"checkNever": false
}
]
}
```

- `checkNever: true` will suggest removing `void` when the argument has type `never`.
1 change: 1 addition & 0 deletions packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -78,6 +78,7 @@ export = {
'@typescript-eslint/no-loss-of-precision': 'error',
'no-magic-numbers': 'off',
'@typescript-eslint/no-magic-numbers': 'error',
'@typescript-eslint/no-meaningless-void-operator': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-misused-promises': 'error',
'@typescript-eslint/no-namespace': 'error',
Expand Down
2 changes: 2 additions & 0 deletions packages/eslint-plugin/src/rules/index.ts
Expand Up @@ -50,6 +50,7 @@ import noInvalidVoidType from './no-invalid-void-type';
import noLoopFunc from './no-loop-func';
import noLossOfPrecision from './no-loss-of-precision';
import noMagicNumbers from './no-magic-numbers';
import noMeaninglessVoidOperator from './no-meaningless-void-operator';
import noMisusedNew from './no-misused-new';
import noMisusedPromises from './no-misused-promises';
import noNamespace from './no-namespace';
Expand Down Expand Up @@ -170,6 +171,7 @@ export default {
'no-loop-func': noLoopFunc,
'no-loss-of-precision': noLossOfPrecision,
'no-magic-numbers': noMagicNumbers,
'no-meaningless-void-operator': noMeaninglessVoidOperator,
'no-misused-new': noMisusedNew,
'no-misused-promises': noMisusedPromises,
'no-namespace': noNamespace,
Expand Down
100 changes: 100 additions & 0 deletions packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts
@@ -0,0 +1,100 @@
import {
ESLintUtils,
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import * as tsutils from 'tsutils';
import * as util from '../util';
import * as ts from 'typescript';

type Options = [
{
checkNever: boolean;
},
];

export default util.createRule<
Options,
'meaninglessVoidOperator' | 'removeVoid'
>({
name: 'no-meaningless-void-operator',
meta: {
type: 'suggestion',
docs: {
description:
'Disallow the `void` operator except when used to discard a value',
category: 'Best Practices',
recommended: false,
suggestion: true,
requiresTypeChecking: true,
},
fixable: 'code',
messages: {
meaninglessVoidOperator:
"void operator shouldn't be used on {{type}}; it should convey that a return value is being ignored",
removeVoid: "Remove 'void'",
},
schema: [
{
type: 'object',
properties: {
checkNever: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
],
},
defaultOptions: [{ checkNever: false }],

create(context, [{ checkNever }]) {
const parserServices = ESLintUtils.getParserServices(context);
const checker = parserServices.program.getTypeChecker();
const sourceCode = context.getSourceCode();

return {
'UnaryExpression[operator="void"]'(node: TSESTree.UnaryExpression): void {
const fix = (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => {
return fixer.removeRange([
sourceCode.getTokens(node)[0].range[0],
sourceCode.getTokens(node)[1].range[0],
]);
};

const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(
node.argument,
);
const argType = checker.getTypeAtLocation(argTsNode);
const unionParts = tsutils.unionTypeParts(argType);
if (
unionParts.every(
part => part.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined),
)
) {
context.report({
node,
messageId: 'meaninglessVoidOperator',
data: { type: checker.typeToString(argType) },
fix,
});
} else if (
checkNever &&
unionParts.every(
part =>
part.flags &
(ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never),
)
) {
context.report({
node,
messageId: 'meaninglessVoidOperator',
data: { type: checker.typeToString(argType) },
suggest: [{ messageId: 'removeVoid', fix }],
});
}
},
};
},
});
@@ -0,0 +1,90 @@
import rule from '../../src/rules/no-meaningless-void-operator';
import { RuleTester, getFixturesRootDir } from '../RuleTester';

const rootDir = getFixturesRootDir();

const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2018,
tsconfigRootDir: rootDir,
project: './tsconfig.json',
},
parser: '@typescript-eslint/parser',
});

ruleTester.run('no-meaningless-void-operator', rule, {
valid: [
`
(() => {})();
function foo() {}
foo(); // nothing to discard
function bar(x: number) {
void x;
return 2;
}
void bar(); // discarding a number
`,
`
function bar(x: never) {
void x;
}
`,
],
invalid: [
{
code: 'void (() => {})();',
output: '(() => {})();',
errors: [
{
messageId: 'meaninglessVoidOperator',
line: 1,
column: 1,
},
],
},
{
code: `
function foo() {}
void foo();
`,
output: `
function foo() {}
foo();
`,
errors: [
{
messageId: 'meaninglessVoidOperator',
line: 3,
column: 1,
},
],
},
{
options: [{ checkNever: true }],
code: `
function bar(x: never) {
void x;
}
`.trimRight(),
errors: [
{
messageId: 'meaninglessVoidOperator',
line: 3,
column: 3,
suggestions: [
{
messageId: 'removeVoid',
output: `
function bar(x: never) {
x;
}
`.trimRight(),
},
],
},
],
},
],
});

0 comments on commit ea40ab6

Please sign in to comment.