Skip to content
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

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

Merged
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 @@ -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
@@ -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(),
},
],
},
],
},
],
});