@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
250 lines (224 loc) • 8.49 kB
JavaScript
module.exports = {
rules: {
'minimize-deep-asynchronous-chains': {
meta: {
type: 'suggestion',
docs: {
description:
'Limits the depth of Promise chains and the number of await expressions in async functions.',
category: 'Best Practices',
recommended: false,
url: 'https://your-doc-site.com/rules/minimize-deep-asynchronous-chains',
},
fixable: null,
schema: [
{
type: 'object',
properties: {
maxPromiseChainLength: {
type: 'integer',
minimum: 1,
default: 3,
},
maxAwaitExpressions: {
type: 'integer',
minimum: 1,
default: 3,
},
},
additionalProperties: false,
},
],
messages: {
tooManyThenCalls:
'Promise chain starting at {{functionName}} has {{count}} .then/.catch/.finally calls, exceeding the maximum of {{maxCount}}.',
tooManyAwaitExpressions:
'Async function "{{functionName}}" has {{count}} await expressions, exceeding the maximum of {{maxCount}}.',
},
},
create: function (context) {
const options = context.options[0] || {};
const maxPromiseChainLength =
options.maxPromiseChainLength === undefined
? 3
: options.maxPromiseChainLength;
const maxAwaitExpressions =
options.maxAwaitExpressions === undefined
? 3
: options.maxAwaitExpressions;
function getFunctionName(node) {
if (node.id && node.id.name) return node.id.name;
if (
node.parent &&
node.parent.type === 'VariableDeclarator' &&
node.parent.id.name
)
return node.parent.id.name;
if (
node.parent &&
node.parent.type === 'Property' &&
node.parent.key.name
)
return node.parent.key.name;
if (
node.parent &&
node.parent.type === 'AssignmentExpression' &&
node.parent.left.name
)
return node.parent.left.name;
return 'anonymous function';
}
// Track processed chains to avoid duplicate reports
const processedChains = new Set();
function getChainIdentifier(node) {
// Create a unique identifier for the chain based on location
return `${node.loc.start.line}:${node.loc.start.column}-${node.loc.end.line}:${node.loc.end.column}`;
}
function getPromiseOriginName(node) {
// Navigate to the root of the promise chain
let current = node;
// Go up the chain to find the original promise
while (
current.callee &&
current.callee.type === 'MemberExpression' &&
['then', 'catch', 'finally'].includes(current.callee.property.name)
) {
if (current.callee.object.type === 'CallExpression') {
// Check if this is still part of the chain
if (
current.callee.object.callee.type === 'MemberExpression' &&
['then', 'catch', 'finally'].includes(
current.callee.object.callee.property.name
)
) {
current = current.callee.object;
} else {
// This is the original function call that returns a promise
break;
}
} else {
// Reached the base promise
break;
}
}
// Extract the name from the original promise
const originalPromise = current.callee.object;
if (originalPromise.type === 'Identifier') {
return originalPromise.name;
} else if (originalPromise.type === 'CallExpression') {
if (originalPromise.callee.type === 'Identifier') {
return originalPromise.callee.name + '()';
} else if (
originalPromise.callee.type === 'MemberExpression' &&
originalPromise.callee.property.name
) {
return originalPromise.callee.property.name + '()';
}
} else if (originalPromise.type === 'NewExpression') {
if (originalPromise.callee.type === 'Identifier') {
return 'new ' + originalPromise.callee.name + '()';
}
}
return 'a Promise';
}
function analyzePromiseChain(startNode) {
let current = startNode;
let chainLength = 0;
const chainNodes = [];
// Count the chain length starting from this node
while (
current &&
current.type === 'CallExpression' &&
current.callee.type === 'MemberExpression' &&
['then', 'catch', 'finally'].includes(current.callee.property.name)
) {
chainLength++;
chainNodes.push(current);
current = current.callee.object;
}
return { chainLength, chainNodes, rootPromise: current };
}
// Simplified approach: Count chain length from each .then/.catch/.finally
function checkPromiseChain(node) {
const chainId = getChainIdentifier(node);
if (processedChains.has(chainId)) {
return; // Already processed this exact node
}
const { chainLength } = analyzePromiseChain(node);
if (chainLength > maxPromiseChainLength) {
processedChains.add(chainId);
const originName = getPromiseOriginName(node);
context.report({
node: node,
messageId: 'tooManyThenCalls',
data: {
functionName: originName,
count: chainLength,
maxCount: maxPromiseChainLength,
},
});
}
}
// --- Async/Await Logic ---
const functionAwaitCounts = new Map();
function enterAsyncFunction(node) {
functionAwaitCounts.set(node, 0);
}
function exitAsyncFunction(node) {
const awaitCount = functionAwaitCounts.get(node) || 0;
if (awaitCount > maxAwaitExpressions) {
context.report({
node: node,
messageId: 'tooManyAwaitExpressions',
data: {
functionName: getFunctionName(node),
count: awaitCount,
maxCount: maxAwaitExpressions,
},
});
}
functionAwaitCounts.delete(node);
}
function findContainingAsyncFunction(node) {
let parent = node.parent;
while (parent) {
if (
(parent.type === 'FunctionDeclaration' ||
parent.type === 'FunctionExpression' ||
parent.type === 'ArrowFunctionExpression') &&
parent.async
) {
return parent;
}
parent = parent.parent;
}
return null;
}
return {
// For Promise chains - target the outermost .then/.catch/.finally calls
'CallExpression[callee.type="MemberExpression"][callee.property.name=/^(then|catch|finally)$/]':
function (node) {
checkPromiseChain(node);
},
// For async/await
'FunctionDeclaration[async=true]': enterAsyncFunction,
'FunctionDeclaration[async=true]:exit': exitAsyncFunction,
'ArrowFunctionExpression[async=true]': enterAsyncFunction,
'ArrowFunctionExpression[async=true]:exit': exitAsyncFunction,
'FunctionExpression[async=true]': enterAsyncFunction,
'FunctionExpression[async=true]:exit': exitAsyncFunction,
AwaitExpression(node) {
const containingFunction = findContainingAsyncFunction(node);
if (
containingFunction &&
functionAwaitCounts.has(containingFunction)
) {
const currentCount = functionAwaitCounts.get(containingFunction);
functionAwaitCounts.set(containingFunction, currentCount + 1);
}
},
};
},
},
},
};