diff --git a/lib/rules/throw-documentation.mjs b/lib/rules/throw-documentation.mjs index 67b139d..425c4f5 100644 --- a/lib/rules/throw-documentation.mjs +++ b/lib/rules/throw-documentation.mjs @@ -5,31 +5,91 @@ export default { meta: { type: "suggestion", docs: { - description: - "enforce JSDoc @throws tag for functions that throw exceptions", + description: "enforce JSDoc @throws tag for functions that throw exceptions", category: "Best Practices", recommended: "error", }, messages: { - missingThrows: - "Function throws an exception but lacks a @throws tag in JSDoc.", + missingThrows: "Function throws '{{type}}' but lacks a @throws tag in JSDoc.", }, schema: [], // no options }, create(context) { /** @param {import("estree").Statement[]} block */ - function hasThrowInBlock(block = []) { - return block.some((statement) => { - if (statement.type === "ThrowStatement") return true; - if (statement.type === "TryStatement") { - return ( - (statement.handler && - hasThrowInBlock(statement.handler.body.body)) || - (statement.finalizer && hasThrowInBlock(statement.finalizer.body)) - ); + function getThrowTypes(block = []) { + const throwTypes = new Set(); + + /** @param {import("estree").Statement[]} block */ + function checkAndAddThrowType(block = []) { + const handlerThrowTypes = getThrowTypes(block); + if (handlerThrowTypes.size > 0) { + throwTypes.add(...handlerThrowTypes); + } + } + + // block may be another function? + if (Array.isArray(block)) { + for (const statement of block) { + if (statement.type === "ThrowStatement") { + // Assuming the argument is an Expression that can be evaluated to get the error type + if ( + statement.argument.type === "NewExpression" && + statement.argument.callee.name === "Error" + ) { + throwTypes.add("Error"); // Generic Error or customize based on arguments if possible + } else if (statement.argument.type === "Identifier") { + throwTypes.add(statement.argument.name); // Assuming the identifier is an error type + } else { + throwTypes.add("Unknown"); // For other types of throws + } + } + + // Handle TryStatement + if (statement.type === "TryStatement") { + if (statement.handler) { + checkAndAddThrowType(statement.handler.body.body); + } + + if (statement.finalizer) { + checkAndAddThrowType(statement.finalizer.body); + } + } + + // Handle IfStatement + if (statement.type === "IfStatement") { + checkAndAddThrowType([statement.consequent]); + statement.alternate && checkAndAddThrowType([statement.alternate]); + } + + // Handle DoWhileStatement and WhileStatement + if (statement.type === "DoWhileStatement" || statement.type === "WhileStatement") { + checkAndAddThrowType([statement.body]); + } + + // Handle ForStatement and ForInStatement + if ( + statement.type === "ForStatement" || + statement.type === "ForInStatement" || + statement.type === "ForOfStatement" + ) { + checkAndAddThrowType([statement.body]); + } + + // Handle SwitchStatement + if (statement.type === "SwitchStatement") { + for (const switchCase of statement.cases) { + checkAndAddThrowType(switchCase.consequent); + } + } + + // Handle BlockStatement + if (statement.type === "BlockStatement") { + checkAndAddThrowType(statement.body); + } } - return false; - }); + } + + return throwTypes; } /** @param {(import("estree").ArrowFunctionExpression | (import("estree").FunctionDeclaration)) & import("eslint").Rule.NodeParentExtension} node */ @@ -37,24 +97,29 @@ export default { const sourceCode = context.sourceCode; const jsDocComment = sourceCode.getJSDocComment(node); - const hasThrow = hasThrowInBlock(node.body.body); + const throwTypes = getThrowTypes(node.body.body); - if (hasThrow) { + if (throwTypes.size > 0) { // Missing JSDoc @throws if (!jsDocComment) { context.report({ node: node, messageId: "missingThrows", + data: { type: Array.from(throwTypes).join(", ") }, }); return; } - const throwsTag = jsDocComment.value.includes("@throws"); - if (!throwsTag) { - context.report({ - node: node, - messageId: "missingThrows", - }); + for (const type of throwTypes) { + const throwsTag = jsDocComment.value.includes(`@throws {${type}}`); + + if (!throwsTag) { + context.report({ + node: node, + messageId: "missingThrows", + data: { type }, + }); + } } } } @@ -62,6 +127,7 @@ export default { return { ArrowFunctionExpression: checkThrows, FunctionDeclaration: checkThrows, + FunctionExpression: checkThrows, }; }, }; diff --git a/lib/tests/throw-documentation.test.mjs b/lib/tests/throw-documentation.test.mjs index 38bc88f..2b2263d 100644 --- a/lib/tests/throw-documentation.test.mjs +++ b/lib/tests/throw-documentation.test.mjs @@ -9,16 +9,6 @@ const ruleTester = new AvaRuleTester(test, { ruleTester.run("throw-documentation", rule, { valid: [ // Function Declaration - { - code: ` - /** - * @throws - */ - function test() { - throw new Error('test'); - } - `, - }, { code: ` /** @@ -77,16 +67,6 @@ ruleTester.run("throw-documentation", rule, { `, }, // Arrow Function Expression - { - code: ` - /** - * @throws - */ - const test = () => { - throw new Error('test'); - } - `, - }, { code: ` /** @@ -117,7 +97,7 @@ ruleTester.run("throw-documentation", rule, { throw new Error('test'); } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, { code: ` @@ -128,7 +108,7 @@ ruleTester.run("throw-documentation", rule, { throw new Error('test'); } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, { code: ` @@ -136,7 +116,7 @@ ruleTester.run("throw-documentation", rule, { throw new Error('test'); } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, { code: ` @@ -148,7 +128,7 @@ ruleTester.run("throw-documentation", rule, { } } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, { code: ` @@ -160,7 +140,7 @@ ruleTester.run("throw-documentation", rule, { } } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, { code: ` @@ -171,7 +151,31 @@ ruleTester.run("throw-documentation", rule, { throw new Error('test'); } `, - errors: [{ messageId: "missingThrows" }], + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], + }, + { + code: ` + /** + * @throws + */ + function test() { + throw new Error('test'); + } + `, + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], + }, + { + code: ` + /** + * @throws + */ + function test() { + while(true) { + throw new Error('err') + } + } + `, + errors: [{ messageId: "missingThrows", data: { type: "Error" } }], }, ], }); diff --git a/package-lock.json b/package-lock.json index 71c12a3..78fbf44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-throw-aware", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eslint-plugin-throw-aware", - "version": "1.0.0-beta.1", + "version": "1.0.0-beta.2", "license": "Unlicense", "devDependencies": { "@eslint/js": "^9.10.0",