📄 no-confusing-void-expression¶
📊 Analysis Summary¶
| Metric | Count |
|---|---|
| 🔧 Functions | 8 |
| 📦 Imports | 11 |
| 📑 Type Aliases | 4 |
📚 Table of Contents¶
🛠️ File Location:¶
📂 packages/eslint-plugin/src/rules/no-confusing-void-expression.ts
📤 Default Export¶
| Property | Value |
|---|---|
name |
'no-confusing-void-expression' |
meta.type |
'problem' |
meta.docs.description |
'Require expressions of type void to appear in statement position' |
meta.docs.recommended |
'strict' |
meta.docs.requiresTypeChecking |
true |
meta.fixable |
'code' |
meta.hasSuggestions |
true |
meta.messages.invalidVoidExpr |
'Placing a void expression inside another expression is forbidden. ' + 'Move it to its own statement instead.' |
meta.messages.invalidVoidExprArrow |
'Returning a void expression from an arrow function shorthand is forbidden. ' + 'Please add braces to the arrow funct... |
meta.messages.invalidVoidExprArrowWrapVoid |
'Void expressions returned from an arrow function shorthand ' + 'must be marked explicitly with the void operator.' |
meta.messages.invalidVoidExprReturn |
'Returning a void expression from a function is forbidden. ' + 'Please move it before the return statement.' |
meta.messages.invalidVoidExprReturnLast |
'Returning a void expression from a function is forbidden. ' + 'Please remove the return statement.' |
meta.messages.invalidVoidExprReturnWrapVoid |
'Void expressions returned from a function ' + 'must be marked explicitly with the void operator.' |
meta.messages.invalidVoidExprWrapVoid |
'Void expressions used inside another expression ' + 'must be moved to its own statement ' + 'or marked explicitly wi... |
meta.messages.voidExprWrapVoid |
'Mark with an explicit void operator.' |
meta.schema |
[ { type: 'object', additionalProperties: false, properties: { ignoreArrowShorthand: { type: 'boolean', description: ... |
defaultOptions |
[ { ignoreArrowShorthand: false, ignoreVoidOperator: false, ignoreVoidReturningFunctions: false, }, ] |
Entry point: create — documented under Functions.
📦 Imports¶
| Name | Source |
|---|---|
NodeWithParent |
@typescript-eslint/utils |
TSESLint |
@typescript-eslint/utils |
TSESTree |
@typescript-eslint/utils |
AST_NODE_TYPES |
@typescript-eslint/utils |
MakeRequired |
../util |
createRule |
../util |
getConstrainedTypeAtLocation |
../util |
getParserServices |
../util |
nullThrows |
../util |
NullThrowsReasons |
../util |
getParentFunctionNode |
../util/getParentFunctionNode |
Functions¶
create(context: any, [options]: any): { 'AwaitExpression, CallExpression, TaggedTemplateExpressio…¶
Parameters:
contextany[options]any
Returns: { 'AwaitExpression, CallExpression, TaggedTemplateExpression'(node: TSESTree.AwaitExpression | TSESTree.CallExpression | TSESTree.TaggedTemplateExpression): void; }
Calls:
getParserServices (from ../util)findInvalidAncestorgetConstrainedTypeAtLocation (from ../util)tsutils.isTypeFlagSetcontext.sourceCode.getTextfixer.replaceTextisVoidReturningFunctionNodecontext.reportcanFixnullThrows (from ../util)context.sourceCode.getTokenBeforeNullThrowsReasons.MissingTokenfixer.replaceTextRangegetParentFunctionNode (from ../util/getParentFunctionNode)isFinalReturnisPreventingASI[ AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.FunctionExpression, ].includesblock.body.indexOfcontext.sourceCode.getFirstToken['(', '[', ''].includes`tsutils.getCallSignaturesOfTypecallSignatures.somesignature.getReturnTypetsutils .unionConstituents(returnType) .someservices.getTypeFromTypeNodeservices.getContextualTypetsutils .unionConstituents(functionType) .some
Internal Comments:
// void expression is in valid position
// not a void expression
// handle arrow function shorthand
// handle wrapping with `void` (x2)
// handle wrapping with braces (x2)
// handle return statement
// remove the `return` keyword
// put a semicolon at the beginning of the line (x6)
// move before the `return` keyword
// e.g. `if (cond) return console.error();` (x3)
// add braces if not inside a block (x3)
// handle generic case
// this would be reported by this rule btw. such irony
/**
* Inspects the void expression's ancestors and finds closest invalid one.
* By default anything other than an ExpressionStatement is invalid.
* Parent expressions which can be used for their short-circuiting behavior
* are ignored and their parents are checked instead.
* @param node The void expression node to check.
* @returns Invalid ancestor node if it was found. `null` otherwise.
*/
// e.g. `{ console.log("foo"); }`
// this is always valid
// e.g. `x && console.log(x)`
// this is valid only if the next ancestor is valid (x2)
// e.g. `cond ? console.log(true) : console.log(false)`
// e.g. `() => console.log("foo")` (x2)
// this is valid with an appropriate option (x4)
// e.g. `void console.log("foo")` (x2)
// e.g. `console?.log('foo')`
// Any other parent is invalid.
// We can assume a return statement will have an argument.
/** Checks whether the return statement is the last statement in a function body. */
// the parent must be a block (x2)
// e.g. `if (cond) return;` (not in a block)
// the block's parent must be a function (x2)
// e.g. `if (cond) { return; }`
// not in a top-level function block
// must be the last child of the block
// not the last statement in the block
/**
* Checks whether the given node, if placed on its own line,
* would prevent automatic semicolon insertion on the line before.
*
* This happens if the line begins with `(`, `[` or `` ` ``
*/
// Game plan:
// - If the function node has a type annotation, check if it includes `void`.
// - If it does then the function is safe to return `void` expressions in.
// - Otherwise, check if the function is a function-expression or an arrow-function.
// - If it is, get its contextual type and bail if we cannot.
// - Return based on whether the contextual type includes `void` or not
Code
create(context, [options]) {
const services = getParserServices(context);
return {
'AwaitExpression, CallExpression, TaggedTemplateExpression'(
node:
| TSESTree.AwaitExpression
| TSESTree.CallExpression
| TSESTree.TaggedTemplateExpression,
): void {
const invalidAncestor = findInvalidAncestor(node);
if (invalidAncestor == null) {
// void expression is in valid position
return;
}
const type = getConstrainedTypeAtLocation(services, node);
if (!tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike)) {
// not a void expression
return;
}
const wrapVoidFix = (fixer: TSESLint.RuleFixer): TSESLint.RuleFix => {
const nodeText = context.sourceCode.getText(node);
const newNodeText = `void ${nodeText}`;
return fixer.replaceText(node, newNodeText);
};
if (invalidAncestor.type === AST_NODE_TYPES.ArrowFunctionExpression) {
// handle arrow function shorthand
if (options.ignoreVoidReturningFunctions) {
const returnsVoid = isVoidReturningFunctionNode(invalidAncestor);
if (returnsVoid) {
return;
}
}
if (options.ignoreVoidOperator) {
// handle wrapping with `void`
return context.report({
node,
messageId: 'invalidVoidExprArrowWrapVoid',
fix: wrapVoidFix,
});
}
// handle wrapping with braces
const arrowFunction = invalidAncestor;
return context.report({
node,
messageId: 'invalidVoidExprArrow',
fix(fixer) {
if (!canFix(arrowFunction)) {
return null;
}
const arrowToken = nullThrows(
context.sourceCode.getTokenBefore(arrowFunction.body, {
filter: token => token.value === '=>',
}),
NullThrowsReasons.MissingToken(
'arrow token',
'before arrow function body',
),
);
return [
fixer.replaceTextRange(
[arrowToken.range[1], arrowFunction.body.range[0]],
' { ',
),
fixer.replaceTextRange(
[arrowFunction.body.range[1], arrowFunction.range[1]],
'; }',
),
];
},
});
}
if (invalidAncestor.type === AST_NODE_TYPES.ReturnStatement) {
// handle return statement
if (options.ignoreVoidReturningFunctions) {
const functionNode = getParentFunctionNode(invalidAncestor);
if (functionNode) {
const returnsVoid = isVoidReturningFunctionNode(functionNode);
if (returnsVoid) {
return;
}
}
}
if (options.ignoreVoidOperator) {
// handle wrapping with `void`
return context.report({
node,
messageId: 'invalidVoidExprReturnWrapVoid',
fix: wrapVoidFix,
});
}
if (isFinalReturn(invalidAncestor)) {
// remove the `return` keyword
return context.report({
node,
messageId: 'invalidVoidExprReturnLast',
fix(fixer) {
if (!canFix(invalidAncestor)) {
return null;
}
const returnValue = invalidAncestor.argument;
const returnValueText = context.sourceCode.getText(returnValue);
let newReturnStmtText = `${returnValueText};`;
if (isPreventingASI(returnValue)) {
// put a semicolon at the beginning of the line
newReturnStmtText = `;${newReturnStmtText}`;
}
return fixer.replaceText(invalidAncestor, newReturnStmtText);
},
});
}
// move before the `return` keyword
return context.report({
node,
messageId: 'invalidVoidExprReturn',
fix(fixer) {
const returnValue = invalidAncestor.argument;
const returnValueText = context.sourceCode.getText(returnValue);
let newReturnStmtText = `${returnValueText}; return;`;
if (isPreventingASI(returnValue)) {
// put a semicolon at the beginning of the line
newReturnStmtText = `;${newReturnStmtText}`;
}
if (
invalidAncestor.parent.type !== AST_NODE_TYPES.BlockStatement
) {
// e.g. `if (cond) return console.error();`
// add braces if not inside a block
newReturnStmtText = `{ ${newReturnStmtText} }`;
}
return fixer.replaceText(invalidAncestor, newReturnStmtText);
},
});
}
// handle generic case
if (options.ignoreVoidOperator) {
// this would be reported by this rule btw. such irony
return context.report({
node,
messageId: 'invalidVoidExprWrapVoid',
suggest: [{ messageId: 'voidExprWrapVoid', fix: wrapVoidFix }],
});
}
context.report({
node,
messageId: 'invalidVoidExpr',
});
},
};
type ReturnStatementWithArgument = MakeRequired<
TSESTree.ReturnStatement,
'argument'
>;
type InvalidAncestor =
| Exclude<TSESTree.Node, TSESTree.ReturnStatement>
| ReturnStatementWithArgument;
/**
* Inspects the void expression's ancestors and finds closest invalid one.
* By default anything other than an ExpressionStatement is invalid.
* Parent expressions which can be used for their short-circuiting behavior
* are ignored and their parents are checked instead.
* @param node The void expression node to check.
* @returns Invalid ancestor node if it was found. `null` otherwise.
*/
function findInvalidAncestor(node: NodeWithParent): InvalidAncestor | null {
const parent = node.parent;
if (
parent.type === AST_NODE_TYPES.SequenceExpression &&
node !== parent.expressions[parent.expressions.length - 1]
) {
return null;
}
if (parent.type === AST_NODE_TYPES.ExpressionStatement) {
// e.g. `{ console.log("foo"); }`
// this is always valid
return null;
}
if (
parent.type === AST_NODE_TYPES.LogicalExpression &&
parent.right === node
) {
// e.g. `x && console.log(x)`
// this is valid only if the next ancestor is valid
return findInvalidAncestor(parent);
}
if (
parent.type === AST_NODE_TYPES.ConditionalExpression &&
(parent.consequent === node || parent.alternate === node)
) {
// e.g. `cond ? console.log(true) : console.log(false)`
// this is valid only if the next ancestor is valid
return findInvalidAncestor(parent);
}
if (
parent.type === AST_NODE_TYPES.ArrowFunctionExpression &&
// e.g. `() => console.log("foo")`
// this is valid with an appropriate option
options.ignoreArrowShorthand
) {
return null;
}
if (
parent.type === AST_NODE_TYPES.UnaryExpression &&
parent.operator === 'void' &&
// e.g. `void console.log("foo")`
// this is valid with an appropriate option
options.ignoreVoidOperator
) {
return null;
}
if (parent.type === AST_NODE_TYPES.ChainExpression) {
// e.g. `console?.log('foo')`
return findInvalidAncestor(parent);
}
// Any other parent is invalid.
// We can assume a return statement will have an argument.
return parent as InvalidAncestor;
}
/** Checks whether the return statement is the last statement in a function body. */
function isFinalReturn(node: TSESTree.ReturnStatement): boolean {
// the parent must be a block
const block = node.parent;
if (block.type !== AST_NODE_TYPES.BlockStatement) {
// e.g. `if (cond) return;` (not in a block)
return false;
}
// the block's parent must be a function
const blockParent = block.parent;
if (
![
AST_NODE_TYPES.ArrowFunctionExpression,
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.FunctionExpression,
].includes(blockParent.type)
) {
// e.g. `if (cond) { return; }`
// not in a top-level function block
return false;
}
// must be the last child of the block
if (block.body.indexOf(node) < block.body.length - 1) {
// not the last statement in the block
return false;
}
return true;
}
/**
* Checks whether the given node, if placed on its own line,
* would prevent automatic semicolon insertion on the line before.
*
* This happens if the line begins with `(`, `[` or `` ` ``
*/
function isPreventingASI(node: TSESTree.Expression): boolean {
const startToken = nullThrows(
context.sourceCode.getFirstToken(node),
NullThrowsReasons.MissingToken('first token', node.type),
);
return ['(', '[', '`'].includes(startToken.value);
}
function canFix(
node: ReturnStatementWithArgument | TSESTree.ArrowFunctionExpression,
): boolean {
const targetNode =
node.type === AST_NODE_TYPES.ReturnStatement
? node.argument
: node.body;
const type = getConstrainedTypeAtLocation(services, targetNode);
return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike);
}
function isFunctionReturnTypeIncludesVoid(functionType: ts.Type): boolean {
const callSignatures = tsutils.getCallSignaturesOfType(functionType);
return callSignatures.some(signature => {
const returnType = signature.getReturnType();
return tsutils
.unionConstituents(returnType)
.some(tsutils.isIntrinsicVoidType);
});
}
function isVoidReturningFunctionNode(
functionNode:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression,
): boolean {
// Game plan:
// - If the function node has a type annotation, check if it includes `void`.
// - If it does then the function is safe to return `void` expressions in.
// - Otherwise, check if the function is a function-expression or an arrow-function.
// - If it is, get its contextual type and bail if we cannot.
// - Return based on whether the contextual type includes `void` or not
if (functionNode.returnType) {
const returnType = services.getTypeFromTypeNode(
functionNode.returnType.typeAnnotation,
);
return tsutils
.unionConstituents(returnType)
.some(tsutils.isIntrinsicVoidType);
}
if (functionNode.type !== AST_NODE_TYPES.FunctionDeclaration) {
const functionType = services.getContextualType(functionNode);
if (functionType) {
return tsutils
.unionConstituents(functionType)
.some(isFunctionReturnTypeIncludesVoid);
}
}
return false;
}
}
Internal helpers¶
Declared inside another function in this file.
wrapVoidFix(fixer: TSESLint.RuleFixer): TSESLint.RuleFix¶
Parameters:
fixerTSESLint.RuleFixer
Returns: TSESLint.RuleFix
Calls:
context.sourceCode.getTextfixer.replaceText
Code
findInvalidAncestor(node: NodeWithParent): InvalidAncestor | null¶
Inspects the void expression's ancestors and finds closest invalid one. By default anything other than an ExpressionStatement is invalid. Parent expressions which can be used for their short-circuiting behavior are ignored and their parents are checked instead.
Parameters:
nodeany: The void expression node to check.
Returns: undefined
Invalid ancestor node if it was found. null otherwise.
Raw JSDoc
/**
* Inspects the void expression's ancestors and finds closest invalid one.
* By default anything other than an ExpressionStatement is invalid.
* Parent expressions which can be used for their short-circuiting behavior
* are ignored and their parents are checked instead.
* @param node The void expression node to check.
* @returns Invalid ancestor node if it was found. `null` otherwise.
*/
Calls:
findInvalidAncestor
Internal Comments:
// e.g. `{ console.log("foo"); }`
// this is always valid
// e.g. `x && console.log(x)`
// this is valid only if the next ancestor is valid (x2)
// e.g. `cond ? console.log(true) : console.log(false)`
// e.g. `() => console.log("foo")` (x2)
// this is valid with an appropriate option (x4)
// e.g. `void console.log("foo")` (x2)
// e.g. `console?.log('foo')`
// Any other parent is invalid.
// We can assume a return statement will have an argument.
Code
function findInvalidAncestor(node: NodeWithParent): InvalidAncestor | null {
const parent = node.parent;
if (
parent.type === AST_NODE_TYPES.SequenceExpression &&
node !== parent.expressions[parent.expressions.length - 1]
) {
return null;
}
if (parent.type === AST_NODE_TYPES.ExpressionStatement) {
// e.g. `{ console.log("foo"); }`
// this is always valid
return null;
}
if (
parent.type === AST_NODE_TYPES.LogicalExpression &&
parent.right === node
) {
// e.g. `x && console.log(x)`
// this is valid only if the next ancestor is valid
return findInvalidAncestor(parent);
}
if (
parent.type === AST_NODE_TYPES.ConditionalExpression &&
(parent.consequent === node || parent.alternate === node)
) {
// e.g. `cond ? console.log(true) : console.log(false)`
// this is valid only if the next ancestor is valid
return findInvalidAncestor(parent);
}
if (
parent.type === AST_NODE_TYPES.ArrowFunctionExpression &&
// e.g. `() => console.log("foo")`
// this is valid with an appropriate option
options.ignoreArrowShorthand
) {
return null;
}
if (
parent.type === AST_NODE_TYPES.UnaryExpression &&
parent.operator === 'void' &&
// e.g. `void console.log("foo")`
// this is valid with an appropriate option
options.ignoreVoidOperator
) {
return null;
}
if (parent.type === AST_NODE_TYPES.ChainExpression) {
// e.g. `console?.log('foo')`
return findInvalidAncestor(parent);
}
// Any other parent is invalid.
// We can assume a return statement will have an argument.
return parent as InvalidAncestor;
}
isFinalReturn(node: TSESTree.ReturnStatement): boolean¶
Checks whether the return statement is the last statement in a function body.
Calls:
[ AST_NODE_TYPES.ArrowFunctionExpression, AST_NODE_TYPES.FunctionDeclaration, AST_NODE_TYPES.FunctionExpression, ].includesblock.body.indexOf
Internal Comments:
// the parent must be a block (x2)
// e.g. `if (cond) return;` (not in a block)
// the block's parent must be a function (x2)
// e.g. `if (cond) { return; }`
// not in a top-level function block
// must be the last child of the block
// not the last statement in the block
Code
function isFinalReturn(node: TSESTree.ReturnStatement): boolean {
// the parent must be a block
const block = node.parent;
if (block.type !== AST_NODE_TYPES.BlockStatement) {
// e.g. `if (cond) return;` (not in a block)
return false;
}
// the block's parent must be a function
const blockParent = block.parent;
if (
![
AST_NODE_TYPES.ArrowFunctionExpression,
AST_NODE_TYPES.FunctionDeclaration,
AST_NODE_TYPES.FunctionExpression,
].includes(blockParent.type)
) {
// e.g. `if (cond) { return; }`
// not in a top-level function block
return false;
}
// must be the last child of the block
if (block.body.indexOf(node) < block.body.length - 1) {
// not the last statement in the block
return false;
}
return true;
}
isPreventingASI(node: TSESTree.Expression): boolean¶
Checks whether the given node, if placed on its own line, would prevent automatic semicolon insertion on the line before.
This happens if the line begins with (, [ or `
Raw JSDoc
Calls:
nullThrows (from ../util)context.sourceCode.getFirstTokenNullThrowsReasons.MissingToken['(', '[', ''].includes`
Code
canFix(node: ReturnStatementWithArgument | TSESTree.…): boolean¶
Parameters:
nodeReturnStatementWithArgument | TSESTree.ArrowFunctionExpression
Returns: boolean
Calls:
getConstrainedTypeAtLocation (from ../util)tsutils.isTypeFlagSet
Code
function canFix(
node: ReturnStatementWithArgument | TSESTree.ArrowFunctionExpression,
): boolean {
const targetNode =
node.type === AST_NODE_TYPES.ReturnStatement
? node.argument
: node.body;
const type = getConstrainedTypeAtLocation(services, targetNode);
return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike);
}
isFunctionReturnTypeIncludesVoid(functionType: ts.Type): boolean¶
Parameters:
functionTypets.Type
Returns: boolean
Calls:
tsutils.getCallSignaturesOfTypecallSignatures.somesignature.getReturnTypetsutils .unionConstituents(returnType) .some
Code
function isFunctionReturnTypeIncludesVoid(functionType: ts.Type): boolean {
const callSignatures = tsutils.getCallSignaturesOfType(functionType);
return callSignatures.some(signature => {
const returnType = signature.getReturnType();
return tsutils
.unionConstituents(returnType)
.some(tsutils.isIntrinsicVoidType);
});
}
isVoidReturningFunctionNode(functionNode: | TSESTree.ArrowFunctionExpression | TS…): boolean¶
Parameters:
functionNode| TSESTree.ArrowFunctionExpression | TSESTree.FunctionDeclaration | TSESTree.FunctionExpression
Returns: boolean
Calls:
services.getTypeFromTypeNodetsutils .unionConstituents(returnType) .someservices.getContextualTypetsutils .unionConstituents(functionType) .some
Internal Comments:
// Game plan:
// - If the function node has a type annotation, check if it includes `void`.
// - If it does then the function is safe to return `void` expressions in.
// - Otherwise, check if the function is a function-expression or an arrow-function.
// - If it is, get its contextual type and bail if we cannot.
// - Return based on whether the contextual type includes `void` or not
Code
function isVoidReturningFunctionNode(
functionNode:
| TSESTree.ArrowFunctionExpression
| TSESTree.FunctionDeclaration
| TSESTree.FunctionExpression,
): boolean {
// Game plan:
// - If the function node has a type annotation, check if it includes `void`.
// - If it does then the function is safe to return `void` expressions in.
// - Otherwise, check if the function is a function-expression or an arrow-function.
// - If it is, get its contextual type and bail if we cannot.
// - Return based on whether the contextual type includes `void` or not
if (functionNode.returnType) {
const returnType = services.getTypeFromTypeNode(
functionNode.returnType.typeAnnotation,
);
return tsutils
.unionConstituents(returnType)
.some(tsutils.isIntrinsicVoidType);
}
if (functionNode.type !== AST_NODE_TYPES.FunctionDeclaration) {
const functionType = services.getContextualType(functionNode);
if (functionType) {
return tsutils
.unionConstituents(functionType)
.some(isFunctionReturnTypeIncludesVoid);
}
}
return false;
}
Type Aliases¶
Options¶
type Options = [
{
ignoreArrowShorthand?: boolean;
ignoreVoidOperator?: boolean;
ignoreVoidReturningFunctions?: boolean;
},
];
MessageId¶
type MessageId = | 'invalidVoidExpr'
| 'invalidVoidExprArrow'
| 'invalidVoidExprArrowWrapVoid'
| 'invalidVoidExprReturn'
| 'invalidVoidExprReturnLast'
| 'invalidVoidExprReturnWrapVoid'
| 'invalidVoidExprWrapVoid'
| 'voidExprWrapVoid';
ReturnStatementWithArgument¶
InvalidAncestor¶
type InvalidAncestor = | Exclude<TSESTree.Node, TSESTree.ReturnStatement>
| ReturnStatementWithArgument;
Generated by Syntax Scribe