@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
185 lines (169 loc) • 6.35 kB
JavaScript
module.exports = {
rules: {
'minimize-complexflows': {
meta: {
type: 'suggestion',
docs: {
description:
'Enforces simplified control flow by limiting recursion and nesting depth, and detecting direct or lexically scoped recursion.',
recommended: 'warn',
},
messages: {
excessiveNesting:
'Avoid nesting control structures deeper than {{maxDepth}} levels. Current depth: {{currentDepth}}.',
unsafeRecursion:
'Direct recursion detected in function `{{functionName}}`. Consider iteration or ensure a clear, bounded termination condition if recursion is intended and allowed.',
lexicalRecursion:
'Lexical recursion: function `{{calledFunctionName}}` is called from an inner scope of `{{currentFunctionName}}`, creating a recursive call pattern.',
},
schema: [
{
type: 'object',
properties: {
maxNestingDepth: {
type: 'number',
minimum: 1,
default: 3,
},
allowRecursion: {
type: 'boolean',
default: false,
},
},
additionalProperties: false,
},
],
},
create(context) {
const options = context.options[0] || {};
const maxNestingDepth = options.maxNestingDepth ?? 3;
const allowRecursion = options.allowRecursion ?? false;
const nestingStack = [0]; // Initial global depth (not used for reporting)
const functionNameStack = []; // Stores names of functions currently in the definition call stack
function getFunctionNameForNode(node) {
if (node.id && node.id.name) {
return node.id.name;
}
if (
(node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression') &&
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.id &&
node.parent.id.type === 'Identifier'
) {
return node.parent.id.name;
}
if (
(node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression') &&
node.parent &&
node.parent.type === 'MethodDefinition' &&
node.parent.key &&
node.parent.key.type === 'Identifier'
) {
return node.parent.key.name;
}
return null;
}
function checkNesting(node) {
const currentFunctionNestingDepth =
nestingStack[nestingStack.length - 1];
if (currentFunctionNestingDepth > maxNestingDepth) {
context.report({
node: node,
messageId: 'excessiveNesting',
data: {
maxDepth: maxNestingDepth,
currentDepth: currentFunctionNestingDepth,
},
});
}
}
// Common logic for when exiting any function node
function handleFunctionExit() {
// These variables (nestingStack, functionNameStack) are from the create() scope (closure)
if (nestingStack.length > 1) {
// Ensure we don't pop the global base
nestingStack.pop();
}
if (functionNameStack.length > 0) {
functionNameStack.pop();
}
}
// --- AST Traversal ---
return {
// 1. Function Entry and Nesting Depth Management
'FunctionDeclaration, FunctionExpression, ArrowFunctionExpression'(
node
) {
nestingStack.push(0); // Each function gets its own nesting counter
const name = getFunctionNameForNode(node);
functionNameStack.push(name || '<anonymous>'); // Push name or placeholder
},
// Function Exit (using the common handler)
'FunctionDeclaration:exit': handleFunctionExit,
'FunctionExpression:exit': handleFunctionExit,
'ArrowFunctionExpression:exit': handleFunctionExit, // This was where the ReferenceError occurred
// Control structures that increase nesting depth
'IfStatement, ForStatement, ForInStatement, ForOfStatement, WhileStatement, DoWhileStatement, SwitchStatement'(
node
) {
if (nestingStack.length > 1) {
// Ensure we are inside a function
nestingStack[nestingStack.length - 1]++;
checkNesting(node);
}
},
// 2. Recursion Checks
CallExpression(node) {
if (allowRecursion || functionNameStack.length === 0) {
return;
}
let calledFunctionName = null;
if (node.callee.type === 'Identifier') {
calledFunctionName = node.callee.name;
} else if (
node.callee.type === 'MemberExpression' &&
node.callee.property.type === 'Identifier'
) {
calledFunctionName = node.callee.property.name;
}
if (!calledFunctionName) {
return;
}
const currentFunctionName =
functionNameStack[functionNameStack.length - 1];
// Direct recursion
if (
currentFunctionName &&
currentFunctionName !== '<anonymous>' &&
calledFunctionName === currentFunctionName
) {
context.report({
node: node,
messageId: 'unsafeRecursion',
data: { functionName: currentFunctionName },
});
return;
}
// Lexical recursion
if (
currentFunctionName &&
functionNameStack.slice(0, -1).includes(calledFunctionName)
) {
context.report({
node: node,
messageId: 'lexicalRecursion',
data: {
calledFunctionName: calledFunctionName,
currentFunctionName: currentFunctionName,
},
});
}
},
};
},
},
},
};