Skip to content

Commit

Permalink
force error type in the jsdoc tag. support recursive code blocks. bum…
Browse files Browse the repository at this point in the history
…p version.
  • Loading branch information
cedeber committed Sep 8, 2024
1 parent fbd53e4 commit 3b563f3
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 51 deletions.
112 changes: 89 additions & 23 deletions lib/rules/throw-documentation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,63 +5,129 @@ 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 */
function checkThrows(node) {
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 },
});
}
}
}
}

return {
ArrowFunctionExpression: checkThrows,
FunctionDeclaration: checkThrows,
FunctionExpression: checkThrows,
};
},
};
56 changes: 30 additions & 26 deletions lib/tests/throw-documentation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
/**
Expand Down Expand Up @@ -77,16 +67,6 @@ ruleTester.run("throw-documentation", rule, {
`,
},
// Arrow Function Expression
{
code: `
/**
* @throws
*/
const test = () => {
throw new Error('test');
}
`,
},
{
code: `
/**
Expand Down Expand Up @@ -117,7 +97,7 @@ ruleTester.run("throw-documentation", rule, {
throw new Error('test');
}
`,
errors: [{ messageId: "missingThrows" }],
errors: [{ messageId: "missingThrows", data: { type: "Error" } }],
},
{
code: `
Expand All @@ -128,15 +108,15 @@ ruleTester.run("throw-documentation", rule, {
throw new Error('test');
}
`,
errors: [{ messageId: "missingThrows" }],
errors: [{ messageId: "missingThrows", data: { type: "Error" } }],
},
{
code: `
const test = () => {
throw new Error('test');
}
`,
errors: [{ messageId: "missingThrows" }],
errors: [{ messageId: "missingThrows", data: { type: "Error" } }],
},
{
code: `
Expand All @@ -148,7 +128,7 @@ ruleTester.run("throw-documentation", rule, {
}
}
`,
errors: [{ messageId: "missingThrows" }],
errors: [{ messageId: "missingThrows", data: { type: "Error" } }],
},
{
code: `
Expand All @@ -160,7 +140,7 @@ ruleTester.run("throw-documentation", rule, {
}
}
`,
errors: [{ messageId: "missingThrows" }],
errors: [{ messageId: "missingThrows", data: { type: "Error" } }],
},
{
code: `
Expand All @@ -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" } }],
},
],
});
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 3b563f3

Please sign in to comment.