Skip to content

feat(eslint-plugin): add no-meaningless-void-operator rule #3641

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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 @@ -169,6 +170,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
Original file line number Diff line number Diff line change
@@ -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 }],
});
}
},
};
},
});
Original file line number Diff line number Diff line change
@@ -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(),
},
],
},
],
},
],
});