diff --git a/.README/rules/require-returns-check.md b/.README/rules/require-returns-check.md index 286fe546c..a570d82de 100644 --- a/.README/rules/require-returns-check.md +++ b/.README/rules/require-returns-check.md @@ -2,10 +2,11 @@ Requires a return statement (or non-`undefined` Promise resolve value) in function bodies if a `@returns` tag (without a `void` or `undefined` type) -is specified in the function's jsdoc comment. +is specified in the function's JSDoc comment. Will also report `@returns {void}` and `@returns {undefined}` if `exemptAsync` -is set to `false` no non-`undefined` returned or resolved value is found. +is set to `false` and a non-`undefined` value is returned or a resolved value +is found. Also reports if `@returns {never}` is discovered with a return value. Will also report if multiple `@returns` tags are present. diff --git a/README.md b/README.md index 71e36b074..2d1ef1183 100644 --- a/README.md +++ b/README.md @@ -17208,10 +17208,11 @@ The following patterns are not considered problems: Requires a return statement (or non-`undefined` Promise resolve value) in function bodies if a `@returns` tag (without a `void` or `undefined` type) -is specified in the function's jsdoc comment. +is specified in the function's JSDoc comment. Will also report `@returns {void}` and `@returns {undefined}` if `exemptAsync` -is set to `false` no non-`undefined` returned or resolved value is found. +is set to `false` and a non-`undefined` value is returned or a resolved value +is found. Also reports if `@returns {never}` is discovered with a return value. Will also report if multiple `@returns` tags are present. @@ -17437,6 +17438,17 @@ export function readFixture(path: string): void; export function readFixture(path: string); // Message: JSDoc @returns declaration present but return expression not available in function. +/** + * @returns {SomeType} + */ +function quux (path) { + if (true) { + return; + } + return 15; +}; +// Message: JSDoc @returns declaration present but return expression not available in function. + /** * Reads a test fixture. * @@ -17449,6 +17461,87 @@ export function readFixture(path: string): void { return; }; // Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux () { + if (true) { + return true; + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux () { + if (true) { + } else { + return; + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux (someVar) { + switch (someVar) { + case 1: + return true; + case 2: + return; + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {boolean} + */ +const quux = (someVar) => { + if (someVar) { + return true; + } +}; +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux () { + try { + return true; + } catch (error) { + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux () { + try { + return true; + } catch (error) { + return true; + } finally { + return; + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. + +/** + * @returns {true} + */ +function quux () { + try { + } catch (error) { + } finally { + return true; + } +} +// Message: JSDoc @returns declaration present but return expression not available in function. ```` The following patterns are not considered problems: @@ -17614,7 +17707,7 @@ function quux () { return true; } catch (err) { } - return; + return true; } /** @@ -17625,7 +17718,7 @@ function quux () { } finally { return true; } - return; + return true; } /** @@ -17633,7 +17726,7 @@ function quux () { */ function quux () { try { - return; + return true; } catch (err) { } return true; @@ -17648,7 +17741,7 @@ function quux () { } catch (err) { return true; } - return; + return true; } /** @@ -17659,7 +17752,7 @@ function quux () { case 'abc': return true; } - return; + return true; } /** @@ -17668,7 +17761,7 @@ function quux () { function quux () { switch (true) { case 'abc': - return; + return true; } return true; } @@ -17680,7 +17773,7 @@ function quux () { for (const i of abc) { return true; } - return; + return true; } /** @@ -17696,7 +17789,7 @@ function quux () { * @returns {true} */ function quux () { - for (let i=0; i { * @returns {void} The file contents as buffer. */ export function readFixture(path: string); + +/** + * @returns {SomeType} + */ +function quux (path) { + if (true) { + return 5; + } + return 15; +}; + +/** + * @returns {*} Foo. + */ +const quux = () => new Promise((resolve) => { + resolve(3); +}); + +/** + * @returns {*} Foo. + */ +const quux = function () { + return new Promise((resolve) => { + resolve(3); + }); +}; ```` diff --git a/package.json b/package.json index 02624d5ff..01b43541f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "description": "JSDoc linting rules for ESLint.", "devDependencies": { "@babel/cli": "^7.19.3", - "@babel/core": "^7.19.3", + "@babel/core": "^7.19.6", "@babel/eslint-parser": "^7.19.1", "@babel/node": "^7.19.1", "@babel/plugin-syntax-class-properties": "^7.12.13", @@ -32,7 +32,7 @@ "chai": "^4.3.6", "cross-env": "^7.0.3", "decamelize": "^5.0.1", - "eslint": "^8.25.0", + "eslint": "^8.26.0", "eslint-config-canonical": "~33.0.1", "gitdown": "^3.1.5", "glob": "^8.0.3", diff --git a/src/iterateJsdoc.js b/src/iterateJsdoc.js index 23a921023..aff6dc77f 100644 --- a/src/iterateJsdoc.js +++ b/src/iterateJsdoc.js @@ -680,8 +680,8 @@ const getUtils = ( return jsdocUtils.hasDefinedTypeTag(tag); }; - utils.hasValueOrExecutorHasNonEmptyResolveValue = (anyPromiseAsReturn) => { - return jsdocUtils.hasValueOrExecutorHasNonEmptyResolveValue(node, anyPromiseAsReturn); + utils.hasValueOrExecutorHasNonEmptyResolveValue = (anyPromiseAsReturn, allBranches) => { + return jsdocUtils.hasValueOrExecutorHasNonEmptyResolveValue(node, anyPromiseAsReturn, allBranches); }; utils.hasYieldValue = () => { diff --git a/src/rules/requireReturnsCheck.js b/src/rules/requireReturnsCheck.js index 2cf01cb89..abc7c6c19 100755 --- a/src/rules/requireReturnsCheck.js +++ b/src/rules/requireReturnsCheck.js @@ -96,6 +96,7 @@ export default iterateJsdoc(({ ) && !utils.hasValueOrExecutorHasNonEmptyResolveValue( exemptAsync, + true, ) && (!exemptGenerators || !node.generator) ) { report(`JSDoc @${tagName} declaration present but return expression not available in function.`); diff --git a/src/utils/hasReturnValue.js b/src/utils/hasReturnValue.js index 28c5e3c24..528edc140 100644 --- a/src/utils/hasReturnValue.js +++ b/src/utils/hasReturnValue.js @@ -19,6 +19,92 @@ const undefinedKeywords = new Set([ 'TSVoidKeyword', 'TSUndefinedKeyword', 'TSNeverKeyword', ]); +/** + * Checks if a node has a return statement. Void return does not count. + * + * @param {object} node + * @param {PromiseFilter} promFilter + * @returns {boolean|Node} + */ +// eslint-disable-next-line complexity +const allBrancheshaveReturnValues = (node, promFilter) => { + if (!node) { + return false; + } + + switch (node.type) { + case 'TSDeclareFunction': + case 'TSFunctionType': + case 'TSMethodSignature': { + const type = node?.returnType?.typeAnnotation?.type; + return type && !undefinedKeywords.has(type); + } + + // case 'MethodDefinition': + // return allBrancheshaveReturnValues(node.value, promFilter); + case 'FunctionExpression': + case 'FunctionDeclaration': + case 'ArrowFunctionExpression': { + return node.expression && (!isNewPromiseExpression(node.body) || !isVoidPromise(node.body)) || + allBrancheshaveReturnValues(node.body, promFilter); + } + + case 'BlockStatement': { + const lastBodyNode = node.body.slice(-1)[0]; + return allBrancheshaveReturnValues(lastBodyNode, promFilter); + } + + case 'LabeledStatement': + case 'WhileStatement': + case 'DoWhileStatement': + case 'ForStatement': + case 'ForInStatement': + case 'ForOfStatement': + case 'WithStatement': { + return allBrancheshaveReturnValues(node.body, promFilter); + } + + case 'IfStatement': { + return allBrancheshaveReturnValues(node.consequent, promFilter) && allBrancheshaveReturnValues(node.alternate, promFilter); + } + + case 'TryStatement': { + return allBrancheshaveReturnValues(node.block, promFilter) && + allBrancheshaveReturnValues(node.handler && node.handler.body, promFilter) && + allBrancheshaveReturnValues(node.finalizer, promFilter); + } + + case 'SwitchStatement': { + return node.cases.every( + (someCase) => { + return someCase.consequent.every((nde) => { + return allBrancheshaveReturnValues(nde, 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; + } + + default: { + return false; + } + } +}; + /** * @callback PromiseFilter * @param {object} node @@ -29,11 +115,12 @@ const undefinedKeywords = new Set([ * Checks if a node has a return statement. Void return does not count. * * @param {object} node + * @param {boolean} throwOnNullReturn * @param {PromiseFilter} promFilter * @returns {boolean|Node} */ // eslint-disable-next-line complexity -const hasReturnValue = (node, promFilter) => { +const hasReturnValue = (node, throwOnNullReturn, promFilter) => { if (!node) { return false; } @@ -47,17 +134,17 @@ const hasReturnValue = (node, promFilter) => { } case 'MethodDefinition': - return hasReturnValue(node.value, promFilter); + return hasReturnValue(node.value, throwOnNullReturn, promFilter); case 'FunctionExpression': case 'FunctionDeclaration': case 'ArrowFunctionExpression': { return node.expression && (!isNewPromiseExpression(node.body) || !isVoidPromise(node.body)) || - hasReturnValue(node.body, promFilter); + hasReturnValue(node.body, throwOnNullReturn, promFilter); } case 'BlockStatement': { return node.body.some((bodyNode) => { - return bodyNode.type !== 'FunctionDeclaration' && hasReturnValue(bodyNode, promFilter); + return bodyNode.type !== 'FunctionDeclaration' && hasReturnValue(bodyNode, throwOnNullReturn, promFilter); }); } @@ -68,24 +155,25 @@ const hasReturnValue = (node, promFilter) => { case 'ForInStatement': case 'ForOfStatement': case 'WithStatement': { - return hasReturnValue(node.body, promFilter); + return hasReturnValue(node.body, throwOnNullReturn, promFilter); } case 'IfStatement': { - return hasReturnValue(node.consequent, promFilter) || hasReturnValue(node.alternate, promFilter); + return hasReturnValue(node.consequent, throwOnNullReturn, promFilter) || + hasReturnValue(node.alternate, throwOnNullReturn, promFilter); } case 'TryStatement': { - return hasReturnValue(node.block, promFilter) || - hasReturnValue(node.handler && node.handler.body, promFilter) || - hasReturnValue(node.finalizer, promFilter); + return hasReturnValue(node.block, throwOnNullReturn, promFilter) || + hasReturnValue(node.handler && node.handler.body, throwOnNullReturn, promFilter) || + hasReturnValue(node.finalizer, throwOnNullReturn, promFilter); } case 'SwitchStatement': { return node.cases.some( (someCase) => { return someCase.consequent.some((nde) => { - return hasReturnValue(nde, promFilter); + return hasReturnValue(nde, throwOnNullReturn, promFilter); }); }, ); @@ -94,6 +182,10 @@ const hasReturnValue = (node, promFilter) => { case 'ReturnStatement': { // void return does not count. if (node.argument === null) { + if (throwOnNullReturn) { + throw new Error('Null return'); + } + return false; } @@ -320,10 +412,31 @@ const hasNonEmptyResolverCall = (node, resolverName) => { * * @param {object} node * @param {boolean} anyPromiseAsReturn + * @param {boolean} allBranches * @returns {boolean} */ -const hasValueOrExecutorHasNonEmptyResolveValue = (node, anyPromiseAsReturn) => { - return hasReturnValue(node, (prom) => { +const hasValueOrExecutorHasNonEmptyResolveValue = (node, anyPromiseAsReturn, allBranches) => { + const hasReturnMethod = allBranches ? + (nde, promiseFilter) => { + try { + hasReturnValue(nde, true, promiseFilter); + } catch (error) { + // istanbul ignore else + if (error.message === 'Null return') { + return false; + } + + // istanbul ignore next + throw error; + } + + return allBrancheshaveReturnValues(nde, promiseFilter); + } : + (nde, promiseFilter) => { + return hasReturnValue(nde, false, promiseFilter); + }; + + return hasReturnMethod(node, (prom) => { if (anyPromiseAsReturn) { return true; } diff --git a/test/rules/assertions/requireReturnsCheck.js b/test/rules/assertions/requireReturnsCheck.js index f64b19323..c6bf9e8f5 100755 --- a/test/rules/assertions/requireReturnsCheck.js +++ b/test/rules/assertions/requireReturnsCheck.js @@ -411,6 +411,25 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * @returns {SomeType} + */ + function quux (path) { + if (true) { + return; + } + return 15; + }; + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, { code: ` /** @@ -433,6 +452,143 @@ export default { ], parser: require.resolve('@typescript-eslint/parser'), }, + { + code: ` + /** + * @returns {true} + */ + function quux () { + if (true) { + return true; + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {true} + */ + function quux () { + if (true) { + } else { + return; + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {true} + */ + function quux (someVar) { + switch (someVar) { + case 1: + return true; + case 2: + return; + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {boolean} + */ + const quux = (someVar) => { + if (someVar) { + return true; + } + }; + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {true} + */ + function quux () { + try { + return true; + } catch (error) { + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {true} + */ + function quux () { + try { + return true; + } catch (error) { + return true; + } finally { + return; + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, + { + code: ` + /** + * @returns {true} + */ + function quux () { + try { + } catch (error) { + } finally { + return true; + } + } + `, + errors: [ + { + line: 2, + message: 'JSDoc @returns declaration present but return expression not available in function.', + }, + ], + }, ], valid: [ { @@ -676,7 +832,7 @@ export default { return true; } catch (err) { } - return; + return true; } `, }, @@ -690,7 +846,7 @@ export default { } finally { return true; } - return; + return true; } `, }, @@ -701,7 +857,7 @@ export default { */ function quux () { try { - return; + return true; } catch (err) { } return true; @@ -719,7 +875,7 @@ export default { } catch (err) { return true; } - return; + return true; } `, }, @@ -733,7 +889,7 @@ export default { case 'abc': return true; } - return; + return true; } `, }, @@ -745,7 +901,7 @@ export default { function quux () { switch (true) { case 'abc': - return; + return true; } return true; } @@ -760,7 +916,7 @@ export default { for (const i of abc) { return true; } - return; + return true; } `, }, @@ -782,7 +938,7 @@ export default { * @returns {true} */ function quux () { - for (let i=0; i new Promise((resolve) => { + resolve(3); + }); + `, + }, + { + code: ` + /** + * @returns {*} Foo. + */ + const quux = function () { + return new Promise((resolve) => { + resolve(3); + }); + }; + `, + }, ], };