Skip to content

Commit ea40ab6

Browse files
authored
feat(eslint-plugin): add no-meaningless-void-operator rule (typescript-eslint#3641)
1 parent f79ae9b commit ea40ab6

File tree

6 files changed

+244
-0
lines changed

6 files changed

+244
-0
lines changed

packages/eslint-plugin/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
128128
| [`@typescript-eslint/no-implicit-any-catch`](./docs/rules/no-implicit-any-catch.md) | Disallow usage of the implicit `any` type in catch clauses | | :wrench: | |
129129
| [`@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: | |
130130
| [`@typescript-eslint/no-invalid-void-type`](./docs/rules/no-invalid-void-type.md) | Disallows usage of `void` type outside of generic or return types | | | |
131+
| [`@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: |
131132
| [`@typescript-eslint/no-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :white_check_mark: | | |
132133
| [`@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: |
133134
| [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :white_check_mark: | | |
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Disallow the `void` operator except when used to discard a value (`no-meaningless-void-operator`)
2+
3+
Disallow the `void` operator when its argument is already of type `void` or `undefined`.
4+
5+
## Rule Details
6+
7+
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.
8+
9+
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.
10+
11+
Examples of **incorrect** code for this rule:
12+
13+
```ts
14+
void (() => {})();
15+
16+
function foo() {}
17+
void foo();
18+
```
19+
20+
Examples of **correct** code for this rule:
21+
22+
```ts
23+
(() => {})();
24+
25+
function foo() {}
26+
foo(); // nothing to discard
27+
28+
function bar(x: number) {
29+
void x; // discarding a number
30+
return 2;
31+
}
32+
void bar(); // discarding a number
33+
```
34+
35+
### Options
36+
37+
This rule accepts a single object option with the following default configuration:
38+
39+
```json
40+
{
41+
"@typescript-eslint/no-meaningless-void-operator": [
42+
"error",
43+
{
44+
"checkNever": false
45+
}
46+
]
47+
}
48+
```
49+
50+
- `checkNever: true` will suggest removing `void` when the argument has type `never`.

packages/eslint-plugin/src/configs/all.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export = {
7878
'@typescript-eslint/no-loss-of-precision': 'error',
7979
'no-magic-numbers': 'off',
8080
'@typescript-eslint/no-magic-numbers': 'error',
81+
'@typescript-eslint/no-meaningless-void-operator': 'error',
8182
'@typescript-eslint/no-misused-new': 'error',
8283
'@typescript-eslint/no-misused-promises': 'error',
8384
'@typescript-eslint/no-namespace': 'error',

packages/eslint-plugin/src/rules/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import noInvalidVoidType from './no-invalid-void-type';
5050
import noLoopFunc from './no-loop-func';
5151
import noLossOfPrecision from './no-loss-of-precision';
5252
import noMagicNumbers from './no-magic-numbers';
53+
import noMeaninglessVoidOperator from './no-meaningless-void-operator';
5354
import noMisusedNew from './no-misused-new';
5455
import noMisusedPromises from './no-misused-promises';
5556
import noNamespace from './no-namespace';
@@ -170,6 +171,7 @@ export default {
170171
'no-loop-func': noLoopFunc,
171172
'no-loss-of-precision': noLossOfPrecision,
172173
'no-magic-numbers': noMagicNumbers,
174+
'no-meaningless-void-operator': noMeaninglessVoidOperator,
173175
'no-misused-new': noMisusedNew,
174176
'no-misused-promises': noMisusedPromises,
175177
'no-namespace': noNamespace,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
ESLintUtils,
3+
TSESLint,
4+
TSESTree,
5+
} from '@typescript-eslint/experimental-utils';
6+
import * as tsutils from 'tsutils';
7+
import * as util from '../util';
8+
import * as ts from 'typescript';
9+
10+
type Options = [
11+
{
12+
checkNever: boolean;
13+
},
14+
];
15+
16+
export default util.createRule<
17+
Options,
18+
'meaninglessVoidOperator' | 'removeVoid'
19+
>({
20+
name: 'no-meaningless-void-operator',
21+
meta: {
22+
type: 'suggestion',
23+
docs: {
24+
description:
25+
'Disallow the `void` operator except when used to discard a value',
26+
category: 'Best Practices',
27+
recommended: false,
28+
suggestion: true,
29+
requiresTypeChecking: true,
30+
},
31+
fixable: 'code',
32+
messages: {
33+
meaninglessVoidOperator:
34+
"void operator shouldn't be used on {{type}}; it should convey that a return value is being ignored",
35+
removeVoid: "Remove 'void'",
36+
},
37+
schema: [
38+
{
39+
type: 'object',
40+
properties: {
41+
checkNever: {
42+
type: 'boolean',
43+
default: false,
44+
},
45+
},
46+
additionalProperties: false,
47+
},
48+
],
49+
},
50+
defaultOptions: [{ checkNever: false }],
51+
52+
create(context, [{ checkNever }]) {
53+
const parserServices = ESLintUtils.getParserServices(context);
54+
const checker = parserServices.program.getTypeChecker();
55+
const sourceCode = context.getSourceCode();
56+
57+
return {
58+
'UnaryExpression[operator="void"]'(node: TSESTree.UnaryExpression): void {
59+
const fix = (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => {
60+
return fixer.removeRange([
61+
sourceCode.getTokens(node)[0].range[0],
62+
sourceCode.getTokens(node)[1].range[0],
63+
]);
64+
};
65+
66+
const argTsNode = parserServices.esTreeNodeToTSNodeMap.get(
67+
node.argument,
68+
);
69+
const argType = checker.getTypeAtLocation(argTsNode);
70+
const unionParts = tsutils.unionTypeParts(argType);
71+
if (
72+
unionParts.every(
73+
part => part.flags & (ts.TypeFlags.Void | ts.TypeFlags.Undefined),
74+
)
75+
) {
76+
context.report({
77+
node,
78+
messageId: 'meaninglessVoidOperator',
79+
data: { type: checker.typeToString(argType) },
80+
fix,
81+
});
82+
} else if (
83+
checkNever &&
84+
unionParts.every(
85+
part =>
86+
part.flags &
87+
(ts.TypeFlags.Void | ts.TypeFlags.Undefined | ts.TypeFlags.Never),
88+
)
89+
) {
90+
context.report({
91+
node,
92+
messageId: 'meaninglessVoidOperator',
93+
data: { type: checker.typeToString(argType) },
94+
suggest: [{ messageId: 'removeVoid', fix }],
95+
});
96+
}
97+
},
98+
};
99+
},
100+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import rule from '../../src/rules/no-meaningless-void-operator';
2+
import { RuleTester, getFixturesRootDir } from '../RuleTester';
3+
4+
const rootDir = getFixturesRootDir();
5+
6+
const ruleTester = new RuleTester({
7+
parserOptions: {
8+
ecmaVersion: 2018,
9+
tsconfigRootDir: rootDir,
10+
project: './tsconfig.json',
11+
},
12+
parser: '@typescript-eslint/parser',
13+
});
14+
15+
ruleTester.run('no-meaningless-void-operator', rule, {
16+
valid: [
17+
`
18+
(() => {})();
19+
20+
function foo() {}
21+
foo(); // nothing to discard
22+
23+
function bar(x: number) {
24+
void x;
25+
return 2;
26+
}
27+
void bar(); // discarding a number
28+
`,
29+
`
30+
function bar(x: never) {
31+
void x;
32+
}
33+
`,
34+
],
35+
invalid: [
36+
{
37+
code: 'void (() => {})();',
38+
output: '(() => {})();',
39+
errors: [
40+
{
41+
messageId: 'meaninglessVoidOperator',
42+
line: 1,
43+
column: 1,
44+
},
45+
],
46+
},
47+
{
48+
code: `
49+
function foo() {}
50+
void foo();
51+
`,
52+
output: `
53+
function foo() {}
54+
foo();
55+
`,
56+
errors: [
57+
{
58+
messageId: 'meaninglessVoidOperator',
59+
line: 3,
60+
column: 1,
61+
},
62+
],
63+
},
64+
{
65+
options: [{ checkNever: true }],
66+
code: `
67+
function bar(x: never) {
68+
void x;
69+
}
70+
`.trimRight(),
71+
errors: [
72+
{
73+
messageId: 'meaninglessVoidOperator',
74+
line: 3,
75+
column: 3,
76+
suggestions: [
77+
{
78+
messageId: 'removeVoid',
79+
output: `
80+
function bar(x: never) {
81+
x;
82+
}
83+
`.trimRight(),
84+
},
85+
],
86+
},
87+
],
88+
},
89+
],
90+
});

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy