diff --git a/README.md b/README.md index f7234f2..a1a4e64 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ A plugin for ESLint to enforce naming conventions and JSDoc annotations for func - Support `@throws` but without a type. Optional. - Check if a `@throws` tag is set, but not required. +- Support of anonymous functions - Support of async function -- Function Naming ## Installation @@ -45,6 +45,7 @@ You can customize the behavior of this plugin by adjusting the rule settings: plugins: { "throw-aware": pluginThrowAware }, + // Recommended configuration rules: { "throw-aware/throw-function-naming": ["error", { suffix: "OrThrow" }], "throw-aware/require-throws-doc": ["warn"] diff --git a/lib/index.mjs b/lib/index.mjs index cfeeef7..bb3a386 100644 --- a/lib/index.mjs +++ b/lib/index.mjs @@ -1,9 +1,8 @@ import fs from "fs"; import throwDocumentation from "./rules/throw-documentation.mjs"; +import throwNaming from "./rules/throw-naming.mjs"; -const pkg = JSON.parse( - fs.readFileSync(new URL("../package.json", import.meta.url), "utf8") -); +const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")); /** * @type {import("eslint").ESLint.Plugin} @@ -16,7 +15,7 @@ const plugin = { configs: {}, rules: { "require-throws-doc": throwDocumentation, - // "throw-function-naming": , + "throw-function-naming": throwNaming, }, processors: {}, }; @@ -28,7 +27,7 @@ Object.assign(plugin.configs, { }, rules: { "throw-aware/require-throws-doc": "warn", - // "throw-aware/throw-function-naming": ["error", { suffix: "OrThrow" }], + "throw-aware/throw-function-naming": "error", }, }, }); diff --git a/lib/rules/reject-documentation.mjs b/lib/rules/reject-documentation.mjs new file mode 100644 index 0000000..e69de29 diff --git a/lib/rules/throw-documentation.mjs b/lib/rules/throw-documentation.mjs index 425c4f5..9be0639 100644 --- a/lib/rules/throw-documentation.mjs +++ b/lib/rules/throw-documentation.mjs @@ -1,3 +1,5 @@ +import { getThrowTypes } from "./utils.mjs"; + /** * @type {import("eslint").Rule.RuleModule} */ @@ -7,7 +9,7 @@ export default { docs: { description: "enforce JSDoc @throws tag for functions that throw exceptions", category: "Best Practices", - recommended: "error", + recommended: "warn", }, messages: { missingThrows: "Function throws '{{type}}' but lacks a @throws tag in JSDoc.", @@ -15,83 +17,6 @@ export default { schema: [], // no options }, create(context) { - /** @param {import("estree").Statement[]} block */ - 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 throwTypes; - } - /** @param {(import("estree").ArrowFunctionExpression | (import("estree").FunctionDeclaration)) & import("eslint").Rule.NodeParentExtension} node */ function checkThrows(node) { const sourceCode = context.sourceCode; diff --git a/lib/rules/throw-naming.mjs b/lib/rules/throw-naming.mjs new file mode 100644 index 0000000..ca9f549 --- /dev/null +++ b/lib/rules/throw-naming.mjs @@ -0,0 +1,59 @@ +import { hasThrowInBlock } from "./utils.mjs"; + +/** + * @type {import("eslint").Rule.RuleModule} + */ +export default { + meta: { + type: "suggestion", + docs: { + description: + "enforce function names to end with a specified suffix (default to 'OrThrow') if they throw exceptions", + category: "Best Practices", + recommended: "error", + }, + messages: { + missingSuffix: + "Function '{{name}}' throws an exception but its name does not end with '{{suffix}}'.", + }, + schema: [ + { + type: "object", + properties: { + suffix: { + type: "string", + default: "OrThrow", + }, + }, + additionalProperties: false, + }, + ], + }, + create(context) { + const options = context.options[0] ?? {}; + const suffix = options.suffix ?? "OrThrow"; + + /** @param {(import("estree").ArrowFunctionExpression | (import("estree").FunctionDeclaration)) & import("eslint").Rule.NodeParentExtension} node */ + function checkFunctionName(node) { + const hasThrow = hasThrowInBlock(node.body.body); + + if (hasThrow) { + const functionName = node.id?.name; + + if (functionName && !functionName.endsWith(suffix)) { + context.report({ + node: node.id, + messageId: "missingSuffix", + data: { name: functionName, suffix }, + }); + } + } + } + + return { + FunctionDeclaration: checkFunctionName, + FunctionExpression: checkFunctionName, + ArrowFunctionExpression: checkFunctionName, + }; + }, +}; diff --git a/lib/rules/utils.mjs b/lib/rules/utils.mjs new file mode 100644 index 0000000..91b3382 --- /dev/null +++ b/lib/rules/utils.mjs @@ -0,0 +1,128 @@ +/** @param {import("estree").Statement[]} block */ +export function getThrowTypes(block = []) { + /** @type {Set} */ + 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 throwTypes; +} + +/** @param {import("estree").Statement[]} block */ +export function hasThrowInBlock(block = []) { + if (!Array.isArray(block)) return false; + + return block.some((statement) => { + if (statement.type === "ThrowStatement") return true; + + // Handle TryStatement + if (statement.type === "TryStatement") { + return ( + (statement.handler && hasThrowInBlock(statement.handler.body.body)) || + (statement.finalizer && hasThrowInBlock(statement.finalizer.body)) + ); + } + + // Handle IfStatement + if (statement.type === "IfStatement") { + return ( + hasThrowInBlock([statement.consequent]) || + (statement.alternate && hasThrowInBlock([statement.alternate])) + ); + } + + // Handle DoWhileStatement and WhileStatement + if (statement.type === "DoWhileStatement" || statement.type === "WhileStatement") { + return hasThrowInBlock([statement.body]); + } + + // Handle ForStatement and ForInStatement + if ( + statement.type === "ForStatement" || + statement.type === "ForInStatement" || + statement.type === "ForOfStatement" + ) { + return hasThrowInBlock([statement.body]); + } + + // Handle SwitchStatement + if (statement.type === "SwitchStatement") { + return statement.cases.some((switchCase) => hasThrowInBlock(switchCase.consequent)); + } + + // Handle BlockStatement + if (statement.type === "BlockStatement") { + return hasThrowInBlock(statement.body); + } + + return false; + }); +} diff --git a/lib/tests/throw-documentation.test.mjs b/lib/tests/throw-documentation.test.mjs index 2b2263d..0b07a75 100644 --- a/lib/tests/throw-documentation.test.mjs +++ b/lib/tests/throw-documentation.test.mjs @@ -7,6 +7,7 @@ const ruleTester = new AvaRuleTester(test, { }); ruleTester.run("throw-documentation", rule, { + /** @type {import("eslint").RuleTester.ValidTestCase[]} */ valid: [ // Function Declaration { @@ -90,6 +91,7 @@ ruleTester.run("throw-documentation", rule, { `, }, ], + /** @type {import("eslint").RuleTester.InvalidTestCase[]} */ invalid: [ { code: ` diff --git a/lib/tests/throw-naming.test.mjs b/lib/tests/throw-naming.test.mjs new file mode 100644 index 0000000..27d1fa8 --- /dev/null +++ b/lib/tests/throw-naming.test.mjs @@ -0,0 +1,40 @@ +import test from "ava"; +import AvaRuleTester from "eslint-ava-rule-tester"; +import rule from "../rules/throw-naming.mjs"; + +const ruleTester = new AvaRuleTester(test, { + languageOptions: { ecmaVersion: 2021, sourceType: "module" }, +}); + +ruleTester.run("throw-function-naming", rule, { + /** @type {import("eslint").RuleTester.ValidTestCase[]} */ + valid: [ + { + code: ` + function testOrThrow() { + throw new Error('test'); + } + `, + }, + ], + /** @type {import("eslint").RuleTester.InvalidTestCase[]} */ + invalid: [ + { + code: ` + function test() { + throw new Error('test'); + } + `, + errors: [{ messageId: "missingSuffix", data: { name: "test", suffix: "OrThrow" } }], + }, + { + code: ` + function test() { + throw new Error('test'); + } + `, + options: [{ suffix: "MayFail" }], + errors: [{ messageId: "missingSuffix", data: { name: "test", suffix: "MayFail" } }], + }, + ], +}); diff --git a/package-lock.json b/package-lock.json index 78fbf44..82abcbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "eslint-plugin-throw-aware", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "eslint-plugin-throw-aware", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "license": "Unlicense", "devDependencies": { "@eslint/js": "^9.10.0", diff --git a/package.json b/package.json index 07c77d4..94b9be4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-throw-aware", - "version": "1.0.0-beta.2", + "version": "1.0.0-beta.3", "description": "An ESLint plugin to enforce naming conventions and JSDoc annotations for functions that throw exceptions.", "type": "module", "main": "lib/index.mjs",