UNPKG

eslint-plugin-jsdoc

Version:
573 lines (490 loc) 17.1 kB
/** * @typedef {import('estree').Node| * import('@typescript-eslint/types').TSESTree.Node} ESTreeOrTypeScriptNode */ /** * Checks if a node is a promise but has no resolve value or an empty value. * An `undefined` resolve does not count. * @param {ESTreeOrTypeScriptNode|undefined|null} node * @returns {boolean|undefined|null} */ const isNewPromiseExpression = (node) => { return node && node.type === 'NewExpression' && node.callee.type === 'Identifier' && node.callee.name === 'Promise'; }; /** * @param {ESTreeOrTypeScriptNode|null|undefined} node * @returns {boolean} */ const isVoidPromise = (node) => { return /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ (node)?.typeArguments?.params?.[0]?.type === 'TSVoidKeyword' /* c8 ignore next 5 */ // eslint-disable-next-line @stylistic/operator-linebreak -- c8 || /** @type {import('@typescript-eslint/types').TSESTree.TSTypeReference} */ ( node // @ts-expect-error Ok )?.typeParameters?.params?.[0]?.type === 'TSVoidKeyword'; }; const undefinedKeywords = new Set([ 'TSNeverKeyword', 'TSUndefinedKeyword', 'TSVoidKeyword', ]); /** * Checks if a node has a return statement. Void return does not count. * @param {ESTreeOrTypeScriptNode|undefined|null} node * @param {boolean} [throwOnNullReturn] * @param {PromiseFilter} [promFilter] * @returns {boolean|undefined} */ // eslint-disable-next-line complexity const hasReturnValue = (node, throwOnNullReturn, promFilter) => { if (!node) { return false; } switch (node.type) { case 'ArrowFunctionExpression': case 'FunctionDeclaration': case 'FunctionExpression': { return 'expression' in node && node.expression && (!isNewPromiseExpression( node.body, ) || !isVoidPromise(node.body)) || hasReturnValue(node.body, throwOnNullReturn, promFilter); } case 'BlockStatement': { return node.body.some((bodyNode) => { return bodyNode.type !== 'FunctionDeclaration' && hasReturnValue(bodyNode, throwOnNullReturn, promFilter); }); } case 'DoWhileStatement': case 'ForInStatement': case 'ForOfStatement': case 'ForStatement': case 'LabeledStatement': case 'WhileStatement': case 'WithStatement': { return hasReturnValue(node.body, throwOnNullReturn, promFilter); } case 'IfStatement': { return hasReturnValue(node.consequent, throwOnNullReturn, promFilter) || hasReturnValue(node.alternate, throwOnNullReturn, promFilter); } case 'MethodDefinition': return hasReturnValue(node.value, throwOnNullReturn, promFilter); case 'ReturnStatement': { // void return does not count. if (node.argument === null) { if (throwOnNullReturn) { throw new Error('Null return'); } return false; } if (promFilter && isNewPromiseExpression(node.argument)) { // Let caller decide how to filter, but this is, at the least, // a return of sorts and truthy return promFilter(node.argument); } return true; } case 'SwitchStatement': { return node.cases.some( (someCase) => { return someCase.consequent.some((nde) => { return hasReturnValue(nde, throwOnNullReturn, promFilter); }); }, ); } case 'TryStatement': { return hasReturnValue(node.block, throwOnNullReturn, promFilter) || hasReturnValue(node.handler && node.handler.body, throwOnNullReturn, promFilter) || hasReturnValue(node.finalizer, throwOnNullReturn, promFilter); } case 'TSDeclareFunction': case 'TSFunctionType': case 'TSMethodSignature': { const type = node?.returnType?.typeAnnotation?.type; return type && !undefinedKeywords.has(type); } default: { return false; } } }; /** * Checks if a node has a return statement. Void return does not count. * @param {ESTreeOrTypeScriptNode|null|undefined} node * @param {PromiseFilter} promFilter * @returns {undefined|boolean|ESTreeOrTypeScriptNode} */ // eslint-disable-next-line complexity const allBrancheshaveReturnValues = (node, promFilter) => { if (!node) { return false; } switch (node.type) { // case 'MethodDefinition': // return allBrancheshaveReturnValues(node.value, promFilter); case 'ArrowFunctionExpression': case 'FunctionDeclaration': case 'FunctionExpression': { return 'expression' in node && node.expression && (!isNewPromiseExpression(node.body) || !isVoidPromise(node.body)) || allBrancheshaveReturnValues(node.body, promFilter) || /** @type {import('@typescript-eslint/types').TSESTree.BlockStatement} */ (node.body).body.some((nde) => { return nde.type === 'ReturnStatement'; }); } case 'BlockStatement': { const lastBodyNode = node.body.slice(-1)[0]; return allBrancheshaveReturnValues(lastBodyNode, promFilter); } case 'DoWhileStatement': case 'WhileStatement': if ( /** * @type {import('@typescript-eslint/types').TSESTree.Literal} */ (node.test).value === true ) { // If this is an infinite loop, we assume only one branch // is needed to provide a return return hasReturnValue(node.body, false, promFilter); } // Fallthrough case 'ForStatement': if (node.test === null) { // If this is an infinite loop, we assume only one branch // is needed to provide a return return hasReturnValue(node.body, false, promFilter); } case 'ForInStatement': case 'ForOfStatement': case 'LabeledStatement': case 'WithStatement': { return allBrancheshaveReturnValues(node.body, promFilter); } case 'IfStatement': { return allBrancheshaveReturnValues(node.consequent, promFilter) && allBrancheshaveReturnValues(node.alternate, promFilter); } case 'ReturnStatement': { // void return does not count. if (node.argument === null) { return false; } if (promFilter && isNewPromiseExpression(node.argument)) { // Let caller decide how to filter, but this is, at the least, // a return of sorts and truthy return promFilter(node.argument); } return true; } case 'SwitchStatement': { return /** @type {import('@typescript-eslint/types').TSESTree.SwitchStatement} */ (node).cases.every( (someCase) => { return !someCase.consequent.some((consNode) => { return consNode.type === 'BreakStatement' || consNode.type === 'ReturnStatement' && consNode.argument === null; }); }, ); } case 'ThrowStatement': { return true; } case 'TryStatement': { // If `finally` returns, all return return node.finalizer && allBrancheshaveReturnValues(node.finalizer, promFilter) || // Return in `try`/`catch` may still occur despite `finally` allBrancheshaveReturnValues(node.block, promFilter) && (!node.handler || allBrancheshaveReturnValues(node.handler && node.handler.body, promFilter)) && (!node.finalizer || (() => { try { hasReturnValue(node.finalizer, true, promFilter); } catch (error) { if (/** @type {Error} */ (error).message === 'Null return') { return false; } /* c8 ignore next 3 */ // eslint-disable-next-line @stylistic/padding-line-between-statements -- c8 throw error; } // As long as not an explicit empty return, then return true return true; })()); } case 'TSDeclareFunction': case 'TSFunctionType': case 'TSMethodSignature': { const type = node?.returnType?.typeAnnotation?.type; return type && !undefinedKeywords.has(type); } default: { return false; } } }; /** * @callback PromiseFilter * @param {ESTreeOrTypeScriptNode|undefined} node * @returns {boolean} */ /** * Avoids further checking child nodes if a nested function shadows the * resolver, but otherwise, if name is used (by call or passed in as an * argument to another function), will be considered as non-empty. * * This could check for redeclaration of the resolver, but as such is * unlikely, we avoid the performance cost of checking everywhere for * (re)declarations or assignments. * @param {import('@typescript-eslint/types').TSESTree.Node|null|undefined} node * @param {string} resolverName * @returns {boolean} */ // eslint-disable-next-line complexity const hasNonEmptyResolverCall = (node, resolverName) => { if (!node) { return false; } // Arrow function without block switch (node.type) { case 'ArrayExpression': case 'ArrayPattern': return node.elements.some((element) => { return hasNonEmptyResolverCall(element, resolverName); }); case 'ArrowFunctionExpression': case 'FunctionDeclaration': case 'FunctionExpression': { // Shadowing if (/** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ ( node.params[0] )?.name === resolverName) { return false; } return hasNonEmptyResolverCall(node.body, resolverName); } case 'AssignmentExpression': case 'BinaryExpression': case 'LogicalExpression': { return hasNonEmptyResolverCall(node.left, resolverName) || hasNonEmptyResolverCall(node.right, resolverName); } case 'AssignmentPattern': return hasNonEmptyResolverCall(node.right, resolverName); case 'AwaitExpression': case 'SpreadElement': case 'UnaryExpression': case 'YieldExpression': return hasNonEmptyResolverCall(node.argument, resolverName); case 'BlockStatement': case 'ClassBody': return node.body.some((bodyNode) => { return hasNonEmptyResolverCall(bodyNode, resolverName); }); /* c8 ignore next 2 -- In Babel? */ case 'CallExpression': // @ts-expect-error Babel? case 'OptionalCallExpression': return /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ ( node.callee ).name === resolverName && ( // Implicit or explicit undefined node.arguments.length > 1 || node.arguments[0] !== undefined ) || node.arguments.some((nde) => { // Being passed in to another function (which might invoke it) return nde.type === 'Identifier' && nde.name === resolverName || // Handle nested items hasNonEmptyResolverCall(nde, resolverName); }); case 'ChainExpression': case 'Decorator': case 'ExpressionStatement': return hasNonEmptyResolverCall(node.expression, resolverName); case 'ClassDeclaration': case 'ClassExpression': return hasNonEmptyResolverCall(node.body, resolverName); /* c8 ignore next 2 -- In Babel? */ // @ts-expect-error Babel? case 'ClassMethod': case 'MethodDefinition': return node.decorators && node.decorators.some((decorator) => { return hasNonEmptyResolverCall(decorator, resolverName); }) || node.computed && hasNonEmptyResolverCall(node.key, resolverName) || hasNonEmptyResolverCall(node.value, resolverName); /* c8 ignore next 2 -- In Babel? */ // @ts-expect-error Babel? case 'ClassProperty': /* c8 ignore next 2 -- In Babel? */ // @ts-expect-error Babel? case 'ObjectProperty': case 'Property': case 'PropertyDefinition': return node.computed && hasNonEmptyResolverCall(node.key, resolverName) || hasNonEmptyResolverCall(node.value, resolverName); case 'ConditionalExpression': case 'IfStatement': { return hasNonEmptyResolverCall(node.test, resolverName) || hasNonEmptyResolverCall(node.consequent, resolverName) || hasNonEmptyResolverCall(node.alternate, resolverName); } case 'DoWhileStatement': case 'ForInStatement': case 'ForOfStatement': case 'ForStatement': case 'LabeledStatement': case 'WhileStatement': case 'WithStatement': { return hasNonEmptyResolverCall(node.body, resolverName); } /* c8 ignore next 2 -- In Babel? */ // @ts-expect-error Babel? case 'Import': case 'ImportExpression': return hasNonEmptyResolverCall(node.source, resolverName); // ?. /* c8 ignore next 2 -- In Babel? */ case 'MemberExpression': // @ts-expect-error Babel? case 'OptionalMemberExpression': return hasNonEmptyResolverCall(node.object, resolverName) || hasNonEmptyResolverCall(node.property, resolverName); case 'ObjectExpression': case 'ObjectPattern': return node.properties.some((property) => { return hasNonEmptyResolverCall(property, resolverName); }); /* c8 ignore next 2 -- In Babel? */ // @ts-expect-error Babel? case 'ObjectMethod': /* c8 ignore next 6 -- In Babel? */ // @ts-expect-error return node.computed && hasNonEmptyResolverCall(node.key, resolverName) || // @ts-expect-error node.arguments.some((nde) => { return hasNonEmptyResolverCall(nde, resolverName); }); case 'ReturnStatement': { if (node.argument === null) { return false; } return hasNonEmptyResolverCall(node.argument, resolverName); } // Comma case 'SequenceExpression': case 'TemplateLiteral': return node.expressions.some((subExpression) => { return hasNonEmptyResolverCall(subExpression, resolverName); }); case 'SwitchStatement': { return node.cases.some( (someCase) => { return someCase.consequent.some((nde) => { return hasNonEmptyResolverCall(nde, resolverName); }); }, ); } case 'TaggedTemplateExpression': return hasNonEmptyResolverCall(node.quasi, resolverName); case 'TryStatement': { return hasNonEmptyResolverCall(node.block, resolverName) || hasNonEmptyResolverCall(node.handler && node.handler.body, resolverName) || hasNonEmptyResolverCall(node.finalizer, resolverName); } case 'VariableDeclaration': { return node.declarations.some((nde) => { return hasNonEmptyResolverCall(nde, resolverName); }); } case 'VariableDeclarator': { return hasNonEmptyResolverCall(node.id, resolverName) || hasNonEmptyResolverCall(node.init, resolverName); } /* // Shouldn't need to parse literals/literal components, etc. case 'Identifier': case 'TemplateElement': case 'Super': // Exports not relevant in this context */ default: return false; } }; /** * Checks if a Promise executor has no resolve value or an empty value. * An `undefined` resolve does not count. * @param {ESTreeOrTypeScriptNode} node * @param {boolean} anyPromiseAsReturn * @param {boolean} [allBranches] * @returns {boolean} */ const hasValueOrExecutorHasNonEmptyResolveValue = (node, anyPromiseAsReturn, allBranches) => { const hasReturnMethod = allBranches ? /** * @param {ESTreeOrTypeScriptNode} nde * @param {PromiseFilter} promiseFilter * @returns {boolean} */ (nde, promiseFilter) => { let hasReturn; try { hasReturn = hasReturnValue(nde, true, promiseFilter); } catch (error) { // c8 ignore else if (/** @type {Error} */ (error).message === 'Null return') { return false; } /* c8 ignore next 3 */ // eslint-disable-next-line @stylistic/padding-line-between-statements -- c8 throw error; } // `hasReturn` check needed since `throw` treated as valid return by // `allBrancheshaveReturnValues` return Boolean(hasReturn && allBrancheshaveReturnValues(nde, promiseFilter)); } : /** * @param {ESTreeOrTypeScriptNode} nde * @param {PromiseFilter} promiseFilter * @returns {boolean} */ (nde, promiseFilter) => { return Boolean(hasReturnValue(nde, false, promiseFilter)); }; return hasReturnMethod(node, (prom) => { if (anyPromiseAsReturn) { return true; } if (isVoidPromise(prom)) { return false; } const { body, params, } = /** * @type {import('@typescript-eslint/types').TSESTree.FunctionExpression| * import('@typescript-eslint/types').TSESTree.ArrowFunctionExpression} */ ( /** @type {import('@typescript-eslint/types').TSESTree.NewExpression} */ ( prom ).arguments[0] ) || {}; if (!params?.length) { return false; } const { name: resolverName, } = /** @type {import('@typescript-eslint/types').TSESTree.Identifier} */ ( params[0] ); return hasNonEmptyResolverCall(body, resolverName); }); }; export { hasReturnValue, hasValueOrExecutorHasNonEmptyResolveValue, };