-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
241 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
}; | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
/** @param {import("estree").Statement[]} block */ | ||
export function getThrowTypes(block = []) { | ||
/** @type {Set<string>} */ | ||
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; | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" } }], | ||
}, | ||
], | ||
}); |
Oops, something went wrong.