378 lines
17 KiB
Plaintext
378 lines
17 KiB
Plaintext
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const utils_1 = require("@typescript-eslint/utils");
|
|
const tsutils = __importStar(require("ts-api-utils"));
|
|
const ts = __importStar(require("typescript"));
|
|
const util = __importStar(require("../util"));
|
|
exports.default = util.createRule({
|
|
name: 'strict-void-return',
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Disallow passing a value-returning function in a position accepting a void function',
|
|
requiresTypeChecking: true,
|
|
},
|
|
messages: {
|
|
asyncFunc: 'Async function used in a context where a void function is expected.',
|
|
nonVoidFunc: 'Value-returning function used in a context where a void function is expected.',
|
|
nonVoidReturn: 'Value returned in a context where a void return is expected.',
|
|
},
|
|
schema: [
|
|
{
|
|
type: 'object',
|
|
additionalProperties: false,
|
|
properties: {
|
|
allowReturnAny: {
|
|
type: 'boolean',
|
|
description: 'Whether to allow functions returning `any` to be used in place expecting a `void` function.',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
defaultOptions: [
|
|
{
|
|
allowReturnAny: false,
|
|
},
|
|
],
|
|
create(context, [options]) {
|
|
const sourceCode = context.sourceCode;
|
|
const parserServices = util.getParserServices(context);
|
|
const checker = parserServices.program.getTypeChecker();
|
|
return {
|
|
ArrayExpression: (node) => {
|
|
for (const elemNode of node.elements) {
|
|
if (elemNode != null &&
|
|
elemNode.type !== utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
checkExpressionNode(elemNode);
|
|
}
|
|
}
|
|
},
|
|
ArrowFunctionExpression: (node) => {
|
|
if (node.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
checkExpressionNode(node.body);
|
|
}
|
|
},
|
|
AssignmentExpression: (node) => {
|
|
checkExpressionNode(node.right); // should ignore operators like `+=` or `-=` automatically
|
|
},
|
|
'CallExpression, NewExpression': checkFunctionCallNode,
|
|
JSXAttribute: (node) => {
|
|
if (node.value?.type === utils_1.AST_NODE_TYPES.JSXExpressionContainer &&
|
|
node.value.expression.type !== utils_1.AST_NODE_TYPES.JSXEmptyExpression) {
|
|
checkExpressionNode(node.value.expression);
|
|
}
|
|
},
|
|
MethodDefinition: checkClassMethodNode,
|
|
ObjectExpression: (node) => {
|
|
for (const propNode of node.properties) {
|
|
if (propNode.type !== utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
checkObjectPropertyNode(propNode);
|
|
}
|
|
}
|
|
},
|
|
PropertyDefinition: checkClassPropertyNode,
|
|
ReturnStatement: (node) => {
|
|
if (node.argument != null) {
|
|
checkExpressionNode(node.argument);
|
|
}
|
|
},
|
|
VariableDeclarator: (node) => {
|
|
if (node.init != null) {
|
|
checkExpressionNode(node.init);
|
|
}
|
|
},
|
|
};
|
|
function isVoidReturningFunctionType(type) {
|
|
const returnTypes = tsutils
|
|
.getCallSignaturesOfType(type)
|
|
.flatMap(signature => tsutils.unionConstituents(signature.getReturnType()));
|
|
return (returnTypes.length > 0 &&
|
|
returnTypes.every(type => tsutils.isTypeFlagSet(type, ts.TypeFlags.Void)));
|
|
}
|
|
/**
|
|
* Finds errors in any expression node.
|
|
*
|
|
* Compares the type of the node against the contextual (expected) type.
|
|
*
|
|
* @returns `true` if the expected type was void function.
|
|
*/
|
|
function checkExpressionNode(node) {
|
|
const expectedType = parserServices.getContextualType(node);
|
|
if (expectedType != null && isVoidReturningFunctionType(expectedType)) {
|
|
reportIfNonVoidFunction(node);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Finds errors in function calls.
|
|
*
|
|
* When checking arguments, we also manually figure out the argument types
|
|
* by iterating over all the function signatures.
|
|
* Thanks to this, we can find arguments like `(() => void) | (() => any)`
|
|
* and treat them as void too.
|
|
* This is done to also support checking functions like `addEventListener`
|
|
* which have overloads where one callback returns any.
|
|
*
|
|
* Implementation mostly based on no-misused-promises,
|
|
* which does this to find `(() => void) | (() => NotThenable)`
|
|
* and report them too.
|
|
*/
|
|
function checkFunctionCallNode(callNode) {
|
|
if (callNode.arguments.length === 0) {
|
|
return;
|
|
}
|
|
const callTsNode = parserServices.esTreeNodeToTSNodeMap.get(callNode);
|
|
const funcType = checker.getTypeAtLocation(callTsNode.expression);
|
|
const funcSignatures = tsutils
|
|
.unionConstituents(funcType)
|
|
.flatMap(type => ts.isCallExpression(callTsNode)
|
|
? type.getCallSignatures()
|
|
: type.getConstructSignatures());
|
|
for (const [argIdx, argNode] of callNode.arguments.entries()) {
|
|
if (argNode.type === utils_1.AST_NODE_TYPES.SpreadElement) {
|
|
continue;
|
|
}
|
|
// Collect the types from all of the call signatures
|
|
const argExpectedReturnTypes = funcSignatures
|
|
.map(s => s.parameters[argIdx])
|
|
.filter(Boolean)
|
|
.map(param => checker.getTypeOfSymbolAtLocation(param, callTsNode.expression))
|
|
.flatMap(paramType => tsutils.unionConstituents(paramType))
|
|
.flatMap(paramType => paramType.getCallSignatures())
|
|
.map(paramSignature => paramSignature.getReturnType());
|
|
const hasSingleSignature = funcSignatures.length === 1;
|
|
const allSignaturesReturnVoid = argExpectedReturnTypes.every(type => isVoid(type) ||
|
|
// Treat as void even though it might be technically any.
|
|
isNullishOrAny(type) ||
|
|
// `getTypeOfSymbolAtLocation` returns unresolved type parameters
|
|
// (e.g. `T`), even for overloads that match the call.
|
|
//
|
|
// Since we can't tell whether a generic overload currently matches,
|
|
// we treat TypeParameters similar to void.
|
|
tsutils.isTypeParameter(type));
|
|
// Check against the contextual type first, but only when there is a
|
|
// single signature or when all signatures return void, because
|
|
// `getContextualType` resolves to the first overload's return type even
|
|
// though there may be another one that matches the call.
|
|
if ((hasSingleSignature || allSignaturesReturnVoid) &&
|
|
checkExpressionNode(argNode)) {
|
|
continue;
|
|
}
|
|
if (
|
|
// At least one return type is void
|
|
argExpectedReturnTypes.some(isVoid) &&
|
|
// The rest are nullish or any
|
|
argExpectedReturnTypes.every(isNullishOrAny)) {
|
|
// We treat this argument as void even though it might be technically any.
|
|
reportIfNonVoidFunction(argNode);
|
|
}
|
|
}
|
|
}
|
|
function isNullishOrAny(type) {
|
|
return tsutils.isTypeFlagSet(type, ts.TypeFlags.VoidLike |
|
|
ts.TypeFlags.Undefined |
|
|
ts.TypeFlags.Null |
|
|
ts.TypeFlags.Any |
|
|
ts.TypeFlags.Never);
|
|
}
|
|
function isVoid(type) {
|
|
return tsutils.isTypeFlagSet(type, ts.TypeFlags.Void);
|
|
}
|
|
/**
|
|
* Finds errors in an object property.
|
|
*
|
|
* Object properties require different logic
|
|
* when the property is a method shorthand.
|
|
*/
|
|
function checkObjectPropertyNode(propNode) {
|
|
const valueNode = propNode.value;
|
|
const propTsNode = parserServices.esTreeNodeToTSNodeMap.get(propNode);
|
|
if (propTsNode.kind === ts.SyntaxKind.MethodDeclaration) {
|
|
// Object property is a method shorthand.
|
|
if (propTsNode.name.kind === ts.SyntaxKind.ComputedPropertyName) {
|
|
// Don't check object methods with computed name.
|
|
return;
|
|
}
|
|
const objType = parserServices.getContextualType(propNode.parent);
|
|
if (objType == null) {
|
|
// Expected object type is unknown.
|
|
return;
|
|
}
|
|
const propSymbol = checker.getPropertyOfType(objType, propTsNode.name.text);
|
|
if (propSymbol == null) {
|
|
// Expected object type is known, but it doesn't have this method.
|
|
return;
|
|
}
|
|
const propExpectedType = checker.getTypeOfSymbolAtLocation(propSymbol, propTsNode);
|
|
if (isVoidReturningFunctionType(propExpectedType)) {
|
|
reportIfNonVoidFunction(valueNode);
|
|
}
|
|
return;
|
|
}
|
|
// Object property is a regular property.
|
|
checkExpressionNode(valueNode);
|
|
}
|
|
/**
|
|
* Finds errors in a class property.
|
|
*
|
|
* In addition to the regular check against the contextual type,
|
|
* we also check against the base class property (when the class extends another class)
|
|
* and the implemented interfaces (when the class implements an interface).
|
|
*/
|
|
function checkClassPropertyNode(propNode) {
|
|
if (propNode.value == null) {
|
|
return;
|
|
}
|
|
// Check in comparison to the base types.
|
|
for (const { baseMemberType } of util.getBaseTypesOfClassMember(parserServices, propNode)) {
|
|
if (isVoidReturningFunctionType(baseMemberType)) {
|
|
reportIfNonVoidFunction(propNode.value);
|
|
return; // Report at most one error.
|
|
}
|
|
}
|
|
// Check in comparison to the contextual type.
|
|
checkExpressionNode(propNode.value);
|
|
}
|
|
/**
|
|
* Finds errors in a class method.
|
|
*
|
|
* We check against the base class method (when the class extends another class)
|
|
* and the implemented interfaces (when the class implements an interface).
|
|
*/
|
|
function checkClassMethodNode(methodNode) {
|
|
if (methodNode.value.type === utils_1.AST_NODE_TYPES.TSEmptyBodyFunctionExpression) {
|
|
return;
|
|
}
|
|
// Check in comparison to the base types.
|
|
for (const { baseMemberType } of util.getBaseTypesOfClassMember(parserServices, methodNode)) {
|
|
if (isVoidReturningFunctionType(baseMemberType)) {
|
|
reportIfNonVoidFunction(methodNode.value);
|
|
return; // Report at most one error.
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Reports an error if the provided node is not allowed in a void function context.
|
|
*/
|
|
function reportIfNonVoidFunction(funcNode) {
|
|
const allowedReturnType = ts.TypeFlags.Void |
|
|
ts.TypeFlags.Never |
|
|
ts.TypeFlags.Undefined |
|
|
(options.allowReturnAny ? ts.TypeFlags.Any : 0);
|
|
const tsNode = parserServices.esTreeNodeToTSNodeMap.get(funcNode);
|
|
const actualType = checker.getApparentType(checker.getTypeAtLocation(tsNode));
|
|
if (tsutils
|
|
.getCallSignaturesOfType(actualType)
|
|
.map(signature => signature.getReturnType())
|
|
.flatMap(returnType => tsutils.unionConstituents(returnType))
|
|
.every(type => tsutils.isTypeFlagSet(type, allowedReturnType))) {
|
|
// The function is already void.
|
|
return;
|
|
}
|
|
if (funcNode.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
funcNode.type !== utils_1.AST_NODE_TYPES.FunctionExpression) {
|
|
// The provided function is not a function literal.
|
|
// Report a generic error.
|
|
return context.report({
|
|
node: funcNode,
|
|
messageId: `nonVoidFunc`,
|
|
});
|
|
}
|
|
// The provided function is a function literal.
|
|
if (funcNode.generator) {
|
|
// The provided function is a generator function.
|
|
// Generator functions are not allowed.
|
|
return context.report({
|
|
loc: util.getFunctionHeadLoc(funcNode, sourceCode),
|
|
messageId: `nonVoidFunc`,
|
|
});
|
|
}
|
|
if (funcNode.async) {
|
|
// The provided function is an async function.
|
|
// Async functions aren't allowed.
|
|
return context.report({
|
|
loc: util.getFunctionHeadLoc(funcNode, sourceCode),
|
|
messageId: `asyncFunc`,
|
|
});
|
|
}
|
|
if (funcNode.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
// The provided function is an arrow function shorthand without braces.
|
|
return context.report({
|
|
node: funcNode.body,
|
|
messageId: `nonVoidReturn`,
|
|
});
|
|
}
|
|
// The function is a regular or arrow function with a block body.
|
|
// Check return type annotation.
|
|
if (funcNode.returnType != null) {
|
|
// The provided function has an explicit return type annotation.
|
|
const typeAnnotationNode = funcNode.returnType.typeAnnotation;
|
|
if (typeAnnotationNode.type !== utils_1.AST_NODE_TYPES.TSVoidKeyword) {
|
|
// The explicit return type is not `void`.
|
|
return context.report({
|
|
node: typeAnnotationNode,
|
|
messageId: `nonVoidFunc`,
|
|
});
|
|
}
|
|
}
|
|
// Iterate over all function's return statements.
|
|
for (const statement of util.walkStatements(funcNode.body.body)) {
|
|
if (statement.type !== utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
statement.argument == null) {
|
|
// We only care about return statements with a value.
|
|
continue;
|
|
}
|
|
const returnType = checker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(statement.argument));
|
|
if (tsutils.isTypeFlagSet(returnType, allowedReturnType)) {
|
|
// Only visit return statements with invalid type.
|
|
continue;
|
|
}
|
|
// This return statement causes the non-void return type.
|
|
const returnKeyword = util.nullThrows(sourceCode.getFirstToken(statement, {
|
|
filter: token => token.value === 'return',
|
|
}), util.NullThrowsReasons.MissingToken('return keyword', statement.type));
|
|
context.report({
|
|
node: returnKeyword,
|
|
messageId: `nonVoidReturn`,
|
|
});
|
|
}
|
|
// No invalid returns found. The function is allowed.
|
|
}
|
|
},
|
|
});
|