@mindfiredigital/eslint-plugin-hub
Version:
eslint-plugin-hub is a powerful, flexible ESLint plugin that provides a curated set of rules to enhance code readability, maintainability, and prevent common errors. Whether you're working with vanilla JavaScript, TypeScript, React, or Angular, eslint-plu
237 lines (223 loc) • 8.37 kB
JavaScript
module.exports = {
rules: {
'use-runtime-assertions': {
meta: {
type: 'problem',
docs: {
description:
'Enforce the presence of a minimum number of runtime assertions in functions to validate inputs and critical intermediate values.',
category: 'Best Practices',
recommended: false,
},
fixable: null,
schema: [
{
type: 'object',
properties: {
minAssertions: {
type: 'integer',
minimum: 0,
default: 2,
},
assertionUtilityNames: {
type: 'array',
items: {
type: 'string',
},
default: ['assert'],
},
ignoreEmptyFunctions: {
type: 'boolean',
default: true,
},
},
additionalProperties: false,
},
],
messages: {
missingAssertions:
'Function "{{functionName}}" should have at least {{minCount}} runtime assertions, but found {{foundCount}}.',
},
},
create: function (context) {
const options = context.options[0] || {};
const minAssertions =
options.minAssertions === undefined ? 2 : options.minAssertions;
const assertionUtilityNames = new Set(
options.assertionUtilityNames || ['assert']
);
const ignoreEmptyFunctions =
options.ignoreEmptyFunctions === undefined
? true
: options.ignoreEmptyFunctions;
function getFunctionName(node) {
if (node.id && node.id.name) {
return node.id.name;
}
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.id &&
node.parent.id.name
) {
return node.parent.id.name;
}
if (
node.parent &&
node.parent.type === 'Property' &&
node.parent.key &&
node.parent.key.name
) {
return node.parent.key.name;
}
if (
node.parent &&
node.parent.type === 'AssignmentExpression' &&
node.parent.left &&
node.parent.left.type === 'Identifier' && // Ensure left is an Identifier
node.parent.left.name
) {
return node.parent.left.name;
}
return 'anonymous';
}
function isEmpty(functionBodyStatements) {
return !functionBodyStatements || functionBodyStatements.length === 0;
}
function checkFunction(node) {
// Skip if no body (for function declarations with no body, rare in JS, or arrow functions with implicit return)
if (!node.body) {
return;
}
// Arrow functions with expression body (e.g. `() => x`) don't have a BlockStatement body
if (node.body.type !== 'BlockStatement') {
// If minAssertions is 0, these are fine. Otherwise, they have 0 assertions.
if (minAssertions > 0) {
context.report({
node: node,
messageId: 'missingAssertions',
data: {
functionName: getFunctionName(node),
minCount: minAssertions,
foundCount: 0,
},
});
}
return;
}
const functionBodyStatements = node.body.body;
if (ignoreEmptyFunctions && isEmpty(functionBodyStatements)) {
// If minAssertions is 0 and ignoreEmptyFunctions is true, it's still fine.
// If ignoreEmptyFunctions is false, the check proceeds below.
// If minAssertions > 0 and ignoreEmptyFunctions is true, this is fine.
if (minAssertions > 0) {
return;
}
// If minAssertions is 0, and ignoreEmptyFunctions is true, we can return.
// If ignoreEmptyFunctions is false, we let it proceed to count (which will be 0)
// and then it will correctly report if minAssertions is > 0 (covered by the final check).
// This ensures that { ignoreEmptyFunctions: false, minAssertions: 0 } is valid.
if (minAssertions === 0) {
return;
}
}
let assertionCount = 0;
function findAssertionsRecursive(currentNode) {
if (!currentNode) {
return;
}
// Check if the current node itself is an assertion primitive
if (currentNode.type === 'ThrowStatement') {
assertionCount++;
} else if (
currentNode.type === 'ExpressionStatement' &&
currentNode.expression.type === 'CallExpression'
) {
const callee = currentNode.expression.callee;
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === 'console' &&
callee.property.type === 'Identifier' &&
callee.property.name === 'assert' &&
assertionUtilityNames.has('assert')
) {
assertionCount++;
} else if (
callee.type === 'Identifier' &&
assertionUtilityNames.has(callee.name)
) {
assertionCount++;
}
}
switch (currentNode.type) {
case 'BlockStatement':
currentNode.body.forEach(findAssertionsRecursive);
break;
case 'IfStatement':
findAssertionsRecursive(currentNode.consequent);
if (currentNode.alternate) {
findAssertionsRecursive(currentNode.alternate);
}
break;
case 'ForStatement':
if (currentNode.init) findAssertionsRecursive(currentNode.init);
findAssertionsRecursive(currentNode.body);
break;
case 'ForInStatement':
case 'ForOfStatement':
// left and right are expressions or declarations
findAssertionsRecursive(currentNode.body);
break;
case 'WhileStatement':
case 'DoWhileStatement':
// test is an expression
findAssertionsRecursive(currentNode.body);
break;
case 'SwitchStatement':
// discriminant is an expression
currentNode.cases.forEach(switchCase => {
// switchCase.test is an expression
switchCase.consequent.forEach(findAssertionsRecursive);
});
break;
case 'TryStatement':
findAssertionsRecursive(currentNode.block);
if (currentNode.handler) {
// CatchClause
findAssertionsRecursive(currentNode.handler.body);
}
if (currentNode.finalizer) {
findAssertionsRecursive(currentNode.finalizer);
}
break;
case 'LabeledStatement':
case 'WithStatement':
findAssertionsRecursive(currentNode.body);
break;
}
}
// For functions with BlockStatement bodies (already checked earlier)
// Start traversal from the function's BlockStatement node (node.body)
findAssertionsRecursive(node.body);
if (assertionCount < minAssertions) {
context.report({
node: node,
messageId: 'missingAssertions',
data: {
functionName: getFunctionName(node),
minCount: minAssertions,
foundCount: assertionCount,
},
});
}
}
return {
FunctionDeclaration: checkFunction,
FunctionExpression: checkFunction,
ArrowFunctionExpression: checkFunction,
};
},
},
},
};