Skip to content

Commit 953e9aa

Browse files
Merge pull request #52 from mindfiredigital/dev
Release: new version with advance rules
2 parents 789315e + 9f39698 commit 953e9aa

25 files changed

+4852
-13
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ The `@mindfiredigital/eslint-plugin-hub` aims to help maintain consistent code q
2121
- [General Rules](#general-rules)
2222
- [React Rules](#react-rules)
2323
- [Angular Rules](#angular-rules)
24+
- [Advanced Rules](#advanced-rules)
2425
- [Usage](#usage)
2526
- [Flat Configuration (`eslint.config.js`)](#flat-configuration-eslintconfigjs)
2627
- [For ES Module](#for-es-module)
@@ -107,6 +108,20 @@ This plugin provides the following rules:
107108
| `angular-limit-input` | Enforces a limit on the number of inputs in Angular components. |
108109
| `angular-filenaming` | Enforces consistent naming conventions for Angular files. |
109110

111+
### Advanced Rules
112+
113+
| Rule Name | Description |
114+
| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115+
| `avoid-runtime-heap-allocation` | 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 |
116+
| `minimize-complexflows` | Enforces simplified control flow by limiting recursion and nesting depth, and detecting direct or lexically scoped recursion |
117+
| `check-return-values` | Enforces handling of return values from non-void functions. If the return value is intentionally not used, it should be explicitly ignored |
118+
| `fixed-loop-bounds` | Enforces that loops have clearly defined bounds or deterministic exit conditions to prevent potentially infinite loops |
119+
| `use-runtime-assertions` | Enforces the presence of a minimum number of runtime assertions in functions to validate inputs and critical intermediate values |
120+
| `minimize-deep-asynchronous-chains` | Limits the depth of Promise chains and the number of `await` expressions in async functions |
121+
| `limit-data-scope` | Enforces several best practices for data scoping: disallows global object modification, suggests moving variables to their narrowest functional scope, and discourages `var` usage. |
122+
| `limit-reference-depth` | Restricts the depth of chained property access and enforces optional chaining to prevent runtime errors, improve null safety, and encourage safer access patterns in deeply nested data structures. |
123+
| `keep-functions-concise` | Enforces a maximum number of lines per function, with options to skip blank lines and comments, to promote readability, maintainability, and concise logic blocks. |
124+
110125
## Usage
111126

112127
You can enable the plugin and configure the rules using either flat or legacy configurations.

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const generalRules = require('./lib/rules/general/index.js');
22
const reactRules = require('./lib/rules/react/index.js');
33
const angularRules = require('./lib/rules/angular/index.js');
4+
const advancedRules = require('./lib/rules/advanced/index.js');
45
const flatConfigBase = require('./configs/flat-config-base.js');
56
const legacyConfigBase = require('./configs/legacy-config-base.js');
67
const { name, version } = require('./package.json');
@@ -63,6 +64,7 @@ const hub = {
6364
...generalRules.rules,
6465
...reactRules.rules,
6566
...angularRules.rules,
67+
...advancedRules.rules,
6668
},
6769
};
6870

@@ -73,6 +75,7 @@ const configs = {
7375
general: createConfig(convertRulesToLegacyConfig(generalRules.rules)),
7476
react: createConfig(convertRulesToLegacyConfig(reactRules.rules)),
7577
angular: createConfig(convertRulesToLegacyConfig(angularRules.rules)),
78+
advanced: createConfig(convertRulesToLegacyConfig(advancedRules.rules)),
7679
mern: createConfig(mernRecommendedRulesLegacy),
7780

7881
// Flat format configurations
@@ -89,6 +92,10 @@ const configs = {
8992
convertRulesToFlatConfig(angularRules.rules),
9093
'hub/flat/angular'
9194
),
95+
'flat/advanced': createConfig(
96+
convertRulesToFlatConfig(advancedRules.rules),
97+
'hub/flat/advanced'
98+
),
9299
'flat/mern': createConfig(mernRecommendedRulesFlat, 'hub/flat/mern'),
93100
};
94101

index.mjs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import generalRules from './lib/rules/general/index.js';
33
import reactRules from './lib/rules/react/index.js';
44
import angularRules from './lib/rules/angular/index.js';
5+
import advancedRules from './lib/rules/advanced/index.js';
56
import flatConfigBase from './configs/flat-config-base.mjs';
67
import legacyConfigBase from './configs/legacy-config-base.mjs';
78
import { fileURLToPath } from 'url';
@@ -10,10 +11,12 @@ import { readFileSync } from 'fs';
1011

1112
const __filename = fileURLToPath(import.meta.url);
1213
const __dirname = dirname(__filename);
13-
const packageJson = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
14+
const packageJson = JSON.parse(
15+
readFileSync(join(__dirname, 'package.json'), 'utf8')
16+
);
1417

1518
// Helper function to convert rule definitions to rule configurations for legacy config
16-
const convertRulesToLegacyConfig = (rules) => {
19+
const convertRulesToLegacyConfig = rules => {
1720
const config = {};
1821
Object.entries(rules).forEach(([key, rule]) => {
1922
config[`@mindfiredigital/hub/${key}`] = ['error', rule];
@@ -22,7 +25,7 @@ const convertRulesToLegacyConfig = (rules) => {
2225
};
2326

2427
// Helper function to convert rule definitions to rule configurations for flat config
25-
const convertRulesToFlatConfig = (rules) => {
28+
const convertRulesToFlatConfig = rules => {
2629
const config = {};
2730
Object.entries(rules).forEach(([key]) => {
2831
config[`hub/${key}`] = 'error';
@@ -70,6 +73,7 @@ const hub = {
7073
...generalRules.rules,
7174
...reactRules.rules,
7275
...angularRules.rules,
76+
...advancedRules.rules,
7377
},
7478
};
7579

@@ -80,16 +84,27 @@ const configs = {
8084
general: createConfig(convertRulesToLegacyConfig(generalRules.rules)),
8185
react: createConfig(convertRulesToLegacyConfig(reactRules.rules)),
8286
angular: createConfig(convertRulesToLegacyConfig(angularRules.rules)),
87+
advanced: createConfig(convertRulesToLegacyConfig(advancedRules.rules)),
8388
mern: createConfig(mernRecommendedRulesLegacy),
8489

8590
// Flat format configurations
8691
'flat/all': createConfig(convertRulesToFlatConfig(hub.rules), 'hub/flat/all'),
87-
'flat/general': createConfig(convertRulesToFlatConfig(generalRules.rules), 'hub/flat/general'),
88-
'flat/react': createConfig(convertRulesToFlatConfig(reactRules.rules), 'hub/flat/react'),
89-
'flat/angular': createConfig(convertRulesToFlatConfig(angularRules.rules), 'hub/flat/angular'),
92+
'flat/general': createConfig(
93+
convertRulesToFlatConfig(generalRules.rules),
94+
'hub/flat/general'
95+
),
96+
'flat/react': createConfig(
97+
convertRulesToFlatConfig(reactRules.rules),
98+
'hub/flat/react'
99+
),
100+
'flat/angular': createConfig(
101+
convertRulesToFlatConfig(angularRules.rules),
102+
'hub/flat/angular'
103+
),
104+
'flat/advanced': createConfig(convertRulesToFlatConfig(advancedRules.rules), 'hub/flat/advanced'),
90105
'flat/mern': createConfig(mernRecommendedRulesFlat, 'hub/flat/mern'),
91106
};
92107

93108
// Export the hub and its configurations
94109
export { hub, configs };
95-
export default { ...hub, configs };
110+
export default { ...hub, configs };

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ module.exports = {
88
moduleNameMapper: {
99
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
1010
},
11+
maxWorkers: 1,
1112
};

lib/rules/advanced/index.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
const limitDataScope = require('./plugin/limit-data-scope');
2+
const keepFunctionsConcise = require('./plugin/keep-functions-concise');
3+
const fixedLoopBounds = require('./plugin/fixed-loop-bounds');
4+
const limitReferenceDepth = require('./plugin/limit-reference-depth');
5+
const checkReturnValues = require('./plugin/check-return-values');
6+
const minimizeComplexflows = require('./plugin/minimize-complexflows');
7+
const avoidRuntimeHeapAllocation = require('./plugin/avoid-runtime-heap-allocation');
8+
const useRuntimeAssesrtion = require('./plugin/use-runtime-assertions');
9+
const minimizeDeepAsynchronousChains = require('./plugin/minimize-deep-asynchronous-chains');
10+
11+
module.exports = {
12+
rules: {
13+
...limitDataScope.rules,
14+
...keepFunctionsConcise.rules,
15+
...fixedLoopBounds.rules,
16+
...limitReferenceDepth.rules,
17+
...checkReturnValues.rules,
18+
...minimizeComplexflows.rules,
19+
...avoidRuntimeHeapAllocation.rules,
20+
...useRuntimeAssesrtion.rules,
21+
...minimizeDeepAsynchronousChains.rules,
22+
},
23+
};
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
module.exports = {
2+
rules: {
3+
'avoid-runtime-heap-allocation': {
4+
meta: {
5+
type: 'suggestion',
6+
docs: {
7+
description:
8+
'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.',
9+
recommended: 'warn',
10+
url: '', // TODO: Add a URL to your rule's documentation
11+
},
12+
messages: {
13+
allocationInFunction:
14+
"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.",
15+
allocationInLoop:
16+
"Runtime allocation of '{{constructType}}' ({{nodeText}}) detected inside a loop within function '{{functionName}}'. This can severely impact performance. Pre-allocate and reuse this structure.",
17+
},
18+
schema: [
19+
{
20+
type: 'object',
21+
properties: {
22+
checkLoopsOnly: {
23+
type: 'boolean',
24+
default: false,
25+
description:
26+
'If true, only flags allocations found inside loops within functions.',
27+
},
28+
allowedConstructs: {
29+
type: 'array',
30+
items: {
31+
type: 'string',
32+
enum: ['Array', 'Object', 'Map', 'Set', 'WeakMap', 'WeakSet'],
33+
},
34+
default: [],
35+
description:
36+
"List of constructor names (e.g., 'Map', 'Set') to allow even if allocated at runtime within functions/loops.",
37+
},
38+
},
39+
additionalProperties: false,
40+
},
41+
],
42+
fixable: null,
43+
},
44+
create(context) {
45+
const options = context.options[0] || {};
46+
const checkLoopsOnly = options.checkLoopsOnly ?? false;
47+
const allowedConstructs = new Set(options.allowedConstructs || []);
48+
49+
const sourceCode = context.getSourceCode();
50+
51+
const functionStack = [];
52+
let loopDepth = 0;
53+
54+
function getFunctionName(node) {
55+
if (node.type === 'FunctionDeclaration' && node.id) {
56+
return node.id.name;
57+
}
58+
if (
59+
node.type === 'FunctionExpression' ||
60+
node.type === 'ArrowFunctionExpression'
61+
) {
62+
if (
63+
node.parent.type === 'VariableDeclarator' &&
64+
node.parent.id &&
65+
node.parent.id.type === 'Identifier'
66+
) {
67+
return node.parent.id.name;
68+
}
69+
if (
70+
node.parent.type === 'MethodDefinition' &&
71+
node.parent.key &&
72+
node.parent.key.type === 'Identifier'
73+
) {
74+
return node.parent.key.name;
75+
}
76+
if (
77+
node.parent.type === 'Property' &&
78+
node.parent.key &&
79+
node.parent.key.type === 'Identifier'
80+
) {
81+
return node.parent.key.name;
82+
}
83+
}
84+
return '<anonymous>';
85+
}
86+
87+
function reportAllocation(node, constructType) {
88+
if (allowedConstructs.has(constructType)) {
89+
return;
90+
}
91+
if (functionStack.length === 0) {
92+
return; // Not inside a function (module scope)
93+
}
94+
95+
const currentFunctionName = functionStack[functionStack.length - 1];
96+
const nodeTextFull = sourceCode.getText(node);
97+
const nodeText =
98+
nodeTextFull.length > 25
99+
? nodeTextFull.slice(0, 25) + '...'
100+
: nodeTextFull;
101+
102+
if (loopDepth > 0) {
103+
context.report({
104+
node,
105+
messageId: 'allocationInLoop',
106+
data: {
107+
constructType,
108+
nodeText,
109+
functionName: currentFunctionName,
110+
},
111+
});
112+
} else if (!checkLoopsOnly) {
113+
context.report({
114+
node,
115+
messageId: 'allocationInFunction',
116+
data: {
117+
constructType,
118+
nodeText,
119+
functionName: currentFunctionName,
120+
},
121+
});
122+
}
123+
}
124+
125+
return {
126+
':function'(node) {
127+
functionStack.push(getFunctionName(node));
128+
},
129+
':function:exit'() {
130+
functionStack.pop();
131+
},
132+
133+
ForStatement() {
134+
loopDepth++;
135+
},
136+
ForInStatement() {
137+
loopDepth++;
138+
},
139+
ForOfStatement() {
140+
loopDepth++;
141+
},
142+
WhileStatement() {
143+
loopDepth++;
144+
},
145+
DoWhileStatement() {
146+
loopDepth++;
147+
},
148+
'ForStatement:exit'() {
149+
loopDepth--;
150+
},
151+
'ForInStatement:exit'() {
152+
loopDepth--;
153+
},
154+
'ForOfStatement:exit'() {
155+
loopDepth--;
156+
},
157+
'WhileStatement:exit'() {
158+
loopDepth--;
159+
},
160+
'DoWhileStatement:exit'() {
161+
loopDepth--;
162+
},
163+
164+
ArrayExpression(node) {
165+
if (
166+
node.elements.length === 0 &&
167+
node.parent &&
168+
node.parent.type === 'AssignmentPattern' &&
169+
node.parent.right === node
170+
) {
171+
return;
172+
}
173+
reportAllocation(node, 'Array');
174+
},
175+
ObjectExpression(node) {
176+
if (
177+
node.properties.length === 0 &&
178+
node.parent &&
179+
node.parent.type === 'AssignmentPattern' &&
180+
node.parent.right === node
181+
) {
182+
return;
183+
}
184+
reportAllocation(node, 'Object');
185+
},
186+
NewExpression(node) {
187+
if (node.callee.type === 'Identifier') {
188+
const constructorName = node.callee.name;
189+
if (
190+
[
191+
'Array',
192+
'Object',
193+
'Map',
194+
'Set',
195+
'WeakMap',
196+
'WeakSet',
197+
].includes(constructorName)
198+
) {
199+
reportAllocation(node, constructorName);
200+
}
201+
}
202+
},
203+
};
204+
},
205+
},
206+
},
207+
};

0 commit comments

Comments
 (0)