Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(eslint-plugin): add
no-meaningless-void-operator
rule (#3641)
- Loading branch information
Showing
6 changed files
with
244 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
packages/eslint-plugin/docs/rules/no-meaningless-void-operator.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/eslint-plugin/src/rules/no-meaningless-void-operator.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }], | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
90 changes: 90 additions & 0 deletions
90
packages/eslint-plugin/tests/rules/no-meaningless-void-operator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
], | ||
}); |