@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
208 lines (198 loc) • 6.49 kB
JavaScript
module.exports = {
rules: {
'avoid-runtime-heap-allocation': {
meta: {
type: 'suggestion',
docs: {
description:
'Discourages heap allocation of common data structures (arrays, objects, Maps, Sets) within function bodies, especially in loops, to promote reuse of pre-allocated structures and reduce garbage collection pressure.',
recommended: 'warn',
url: '', // TODO: Add a URL to your rule's documentation
},
messages: {
allocationInFunction:
"Runtime allocation of '{{constructType}}' ({{nodeText}}) detected in function '{{functionName}}'. Consider pre-allocating and reusing, especially if this function is called frequently or is performance-sensitive.",
allocationInLoop:
"Runtime allocation of '{{constructType}}' ({{nodeText}}) detected inside a loop within function '{{functionName}}'. This can severely impact performance. Pre-allocate and reuse this structure.",
},
schema: [
{
type: 'object',
properties: {
checkLoopsOnly: {
type: 'boolean',
default: false,
description:
'If true, only flags allocations found inside loops within functions.',
},
allowedConstructs: {
type: 'array',
items: {
type: 'string',
enum: ['Array', 'Object', 'Map', 'Set', 'WeakMap', 'WeakSet'],
},
default: [],
description:
"List of constructor names (e.g., 'Map', 'Set') to allow even if allocated at runtime within functions/loops.",
},
},
additionalProperties: false,
},
],
fixable: null,
},
create(context) {
const options = context.options[0] || {};
const checkLoopsOnly = options.checkLoopsOnly ?? false;
const allowedConstructs = new Set(options.allowedConstructs || []);
const sourceCode = context.getSourceCode();
const functionStack = [];
let loopDepth = 0;
function getFunctionName(node) {
if (node.type === 'FunctionDeclaration' && node.id) {
return node.id.name;
}
if (
node.type === 'FunctionExpression' ||
node.type === 'ArrowFunctionExpression'
) {
if (
node.parent.type === 'VariableDeclarator' &&
node.parent.id &&
node.parent.id.type === 'Identifier'
) {
return node.parent.id.name;
}
if (
node.parent.type === 'MethodDefinition' &&
node.parent.key &&
node.parent.key.type === 'Identifier'
) {
return node.parent.key.name;
}
if (
node.parent.type === 'Property' &&
node.parent.key &&
node.parent.key.type === 'Identifier'
) {
return node.parent.key.name;
}
}
return '<anonymous>';
}
function reportAllocation(node, constructType) {
if (allowedConstructs.has(constructType)) {
return;
}
if (functionStack.length === 0) {
return; // Not inside a function (module scope)
}
const currentFunctionName = functionStack[functionStack.length - 1];
const nodeTextFull = sourceCode.getText(node);
const nodeText =
nodeTextFull.length > 25
? nodeTextFull.slice(0, 25) + '...'
: nodeTextFull;
if (loopDepth > 0) {
context.report({
node,
messageId: 'allocationInLoop',
data: {
constructType,
nodeText,
functionName: currentFunctionName,
},
});
} else if (!checkLoopsOnly) {
context.report({
node,
messageId: 'allocationInFunction',
data: {
constructType,
nodeText,
functionName: currentFunctionName,
},
});
}
}
return {
':function'(node) {
functionStack.push(getFunctionName(node));
},
':function:exit'() {
functionStack.pop();
},
ForStatement() {
loopDepth++;
},
ForInStatement() {
loopDepth++;
},
ForOfStatement() {
loopDepth++;
},
WhileStatement() {
loopDepth++;
},
DoWhileStatement() {
loopDepth++;
},
'ForStatement:exit'() {
loopDepth--;
},
'ForInStatement:exit'() {
loopDepth--;
},
'ForOfStatement:exit'() {
loopDepth--;
},
'WhileStatement:exit'() {
loopDepth--;
},
'DoWhileStatement:exit'() {
loopDepth--;
},
ArrayExpression(node) {
if (
node.elements.length === 0 &&
node.parent &&
node.parent.type === 'AssignmentPattern' &&
node.parent.right === node
) {
return;
}
reportAllocation(node, 'Array');
},
ObjectExpression(node) {
if (
node.properties.length === 0 &&
node.parent &&
node.parent.type === 'AssignmentPattern' &&
node.parent.right === node
) {
return;
}
reportAllocation(node, 'Object');
},
NewExpression(node) {
if (node.callee.type === 'Identifier') {
const constructorName = node.callee.name;
if (
[
'Array',
'Object',
'Map',
'Set',
'WeakMap',
'WeakSet',
].includes(constructorName)
) {
reportAllocation(node, constructorName);
}
}
},
};
},
},
},
};