"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. } }, });