eslint-plugin-performance-rules 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +197 -0
- package/index.js +21 -0
- package/package.json +33 -0
- package/src/lib/formatters/performance.js +30 -0
- package/src/lib/rules/no-large-json-parse-in-loop.js +109 -0
- package/src/lib/rules/no-quadratic-loops.js +172 -0
- package/src/lib/rules/no-sync-in-loop.js +115 -0
- package/src/lib/rules/no-unnecessary-array-clone.js +156 -0
- package/src/lib/rules/react-no-inline-object-creation.js +81 -0
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# eslint-plugin-performance
|
|
2
|
+
|
|
3
|
+
ESLint plugin to detect performance anti-patterns in JavaScript and TypeScript codebases. This plugin helps identify code patterns that may cause runtime performance degradation through static AST analysis.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- š Detects quadratic loop complexity (O(n²) patterns)
|
|
8
|
+
- š« Identifies synchronous blocking calls in loops
|
|
9
|
+
- ā” Catches expensive JSON parsing in loops
|
|
10
|
+
- āļø Finds React inline object creation causing re-renders
|
|
11
|
+
- šÆ Detects unnecessary array cloning
|
|
12
|
+
- š Performance score formatter with impact assessment
|
|
13
|
+
- š§ Works with JavaScript and TypeScript
|
|
14
|
+
- ā
Zero runtime dependencies
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install eslint-plugin-performance --save-dev
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Configuration
|
|
23
|
+
|
|
24
|
+
### ESLint Configuration (.eslintrc.json)
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"plugins": ["performance"],
|
|
29
|
+
"extends": ["plugin:performance/recommended"]
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or configure rules individually:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"plugins": ["performance"],
|
|
38
|
+
"rules": {
|
|
39
|
+
"performance/no-quadratic-loops": "warn",
|
|
40
|
+
"performance/no-sync-in-loop": "warn",
|
|
41
|
+
"performance/no-large-json-parse-in-loop": "warn",
|
|
42
|
+
"performance/react-no-inline-object-creation": "warn",
|
|
43
|
+
"performance/no-unnecessary-array-clone": "warn"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Flat Config (eslint.config.js)
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
const performancePlugin = require('eslint-plugin-performance');
|
|
52
|
+
|
|
53
|
+
module.exports = [
|
|
54
|
+
{
|
|
55
|
+
plugins: {
|
|
56
|
+
performance: performancePlugin
|
|
57
|
+
},
|
|
58
|
+
rules: {
|
|
59
|
+
'performance/no-quadratic-loops': 'warn',
|
|
60
|
+
'performance/no-sync-in-loop': 'warn',
|
|
61
|
+
'performance/no-large-json-parse-in-loop': 'warn',
|
|
62
|
+
'performance/react-no-inline-object-creation': 'warn',
|
|
63
|
+
'performance/no-unnecessary-array-clone': 'warn'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Or use the recommended configuration:
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
const performancePlugin = require('eslint-plugin-performance');
|
|
73
|
+
|
|
74
|
+
module.exports = [
|
|
75
|
+
{
|
|
76
|
+
plugins: {
|
|
77
|
+
performance: performancePlugin
|
|
78
|
+
},
|
|
79
|
+
rules: performancePlugin.configs.recommended.rules
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### TypeScript Configuration
|
|
85
|
+
|
|
86
|
+
For TypeScript projects, configure the parser:
|
|
87
|
+
|
|
88
|
+
**.eslintrc.json:**
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"parser": "@typescript-eslint/parser",
|
|
93
|
+
"parserOptions": {
|
|
94
|
+
"ecmaVersion": 2020,
|
|
95
|
+
"sourceType": "module",
|
|
96
|
+
"ecmaFeatures": {
|
|
97
|
+
"jsx": true
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
"plugins": ["performance"],
|
|
101
|
+
"extends": ["plugin:performance/recommended"]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**eslint.config.js:**
|
|
106
|
+
|
|
107
|
+
```javascript
|
|
108
|
+
const performancePlugin = require('eslint-plugin-performance');
|
|
109
|
+
const tsParser = require('@typescript-eslint/parser');
|
|
110
|
+
|
|
111
|
+
module.exports = [
|
|
112
|
+
{
|
|
113
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
114
|
+
languageOptions: {
|
|
115
|
+
parser: tsParser,
|
|
116
|
+
parserOptions: {
|
|
117
|
+
ecmaVersion: 2020,
|
|
118
|
+
sourceType: 'module',
|
|
119
|
+
ecmaFeatures: {
|
|
120
|
+
jsx: true
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
plugins: {
|
|
125
|
+
performance: performancePlugin
|
|
126
|
+
},
|
|
127
|
+
rules: performancePlugin.configs.recommended.rules
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Rules
|
|
133
|
+
|
|
134
|
+
### no-quadratic-loops
|
|
135
|
+
Detects nested loops that iterate over the same array reference, which causes O(n²) algorithmic complexity.
|
|
136
|
+
|
|
137
|
+
**Rationale:** Nested loops over the same array create quadratic time complexity, causing exponential performance degradation as data size grows. A 1000-item array becomes 1,000,000 iterations. Detects synchronous blocking calls (fs.*Sync, child_process.*Sync) inside loops that block the event loop repeatedly.
|
|
138
|
+
|
|
139
|
+
**Rationale:** Synchronous file system and process operations block the Node.js event loop. Calling them in a loop multiplies the blocking time, freezing your application for extended periods.
|
|
140
|
+
|
|
141
|
+
### no-large-json-parse-in-loop
|
|
142
|
+
Detects JSON.parse() calls inside loops, which causes repeated expensive parsing operations.
|
|
143
|
+
|
|
144
|
+
**Rationale:** JSON parsing is CPU-intensive. Parsing JSON repeatedly in a loop wastes processing time. Parse once before the loop or restructure your data flow.
|
|
145
|
+
|
|
146
|
+
### react-no-inline-object-creation
|
|
147
|
+
Detects inline object literals, array literals, and arrow functions in JSX props that cause unnecessary re-renders.
|
|
148
|
+
|
|
149
|
+
**Rationale:** Creating new objects, arrays, or functions inline in JSX creates a new reference on every render. This causes child components to re-render even when the actual values haven't changed, breaking React's reconciliation optimization.
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
### no-unnecessary-array-clone
|
|
153
|
+
|
|
154
|
+
Detects array cloning operations (spread operator, Array.from(), slice()) when the cloned array is never mutated.
|
|
155
|
+
|
|
156
|
+
**Rationale:** Cloning arrays allocates new memory and copies all elements. If you never mutate the clone, you're wasting memory and CPU cycles. Use the original array reference instead.
|
|
157
|
+
|
|
158
|
+
## Usage
|
|
159
|
+
|
|
160
|
+
### Running ESLint
|
|
161
|
+
```bash
|
|
162
|
+
# Standard ESLint run
|
|
163
|
+
npx eslint .
|
|
164
|
+
|
|
165
|
+
# With auto-fix (where applicable)
|
|
166
|
+
npx eslint . --fix
|
|
167
|
+
|
|
168
|
+
# Check specific files
|
|
169
|
+
npx eslint src/**/*.js
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Example CLI Output
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
/project/src/utils/data.js
|
|
176
|
+
12:3 warning Nested loop iterates over same array, causing O(n²) complexity performance/no-quadratic-loops
|
|
177
|
+
28:5 warning Synchronous blocking call inside loop blocks event loop performance/no-sync-in-loop
|
|
178
|
+
|
|
179
|
+
/project/src/components/UserList.jsx
|
|
180
|
+
15:23 warning Inline object creation in JSX prop causes re-renders performance/react-no-inline-object-creation
|
|
181
|
+
16:21 warning Inline arrow function in JSX prop causes re-renders performance/react-no-inline-object-creation
|
|
182
|
+
|
|
183
|
+
ā 4 problems (0 errors, 4 warnings)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Performance Formatter
|
|
187
|
+
|
|
188
|
+
Use the custom performance formatter to get an aggregated performance score:
|
|
189
|
+
```bash
|
|
190
|
+
npx eslint . --format ./node_modules/eslint-plugin-performance/lib/formatters/performance.js
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
Contributions are welcome! Please open an issue or submit a pull request on GitHub.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rules: {
|
|
3
|
+
'no-quadratic-loops': require('./lib/rules/no-quadratic-loops'),
|
|
4
|
+
'no-sync-in-loop': require('./lib/rules/no-sync-in-loop'),
|
|
5
|
+
'no-large-json-parse-in-loop': require('./lib/rules/no-large-json-parse-in-loop'),
|
|
6
|
+
'react-no-inline-object-creation': require('./lib/rules/react-no-inline-object-creation'),
|
|
7
|
+
'no-unnecessary-array-clone': require('./lib/rules/no-unnecessary-array-clone')
|
|
8
|
+
},
|
|
9
|
+
configs: {
|
|
10
|
+
recommended: {
|
|
11
|
+
plugins: ['performance'],
|
|
12
|
+
rules: {
|
|
13
|
+
'performance/no-quadratic-loops': 'warn',
|
|
14
|
+
'performance/no-sync-in-loop': 'warn',
|
|
15
|
+
'performance/no-large-json-parse-in-loop': 'warn',
|
|
16
|
+
'performance/react-no-inline-object-creation': 'warn',
|
|
17
|
+
'performance/no-unnecessary-array-clone': 'warn'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-performance-rules",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "ESLint plugin to detect performance anti-patterns",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"lint": "eslint ."
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
"keywords": [
|
|
13
|
+
"eslint",
|
|
14
|
+
"eslintplugin",
|
|
15
|
+
"performance",
|
|
16
|
+
"optimization",
|
|
17
|
+
"react",
|
|
18
|
+
"nodejs"
|
|
19
|
+
],
|
|
20
|
+
"author": "",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"eslint": ">=7.0.0"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=12.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@typescript-eslint/parser": "^8.56.1",
|
|
30
|
+
"eslint": "^10.0.2",
|
|
31
|
+
"typescript": "^5.9.3"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module.exports = function(results) {
|
|
2
|
+
// Implementation will be added in Task 8
|
|
3
|
+
let score = 100;
|
|
4
|
+
let issueCount = 0;
|
|
5
|
+
|
|
6
|
+
for (const result of results) {
|
|
7
|
+
for (const message of result.messages) {
|
|
8
|
+
issueCount++;
|
|
9
|
+
if (message.severity === 1) {
|
|
10
|
+
score -= 2;
|
|
11
|
+
} else if (message.severity === 2) {
|
|
12
|
+
score -= 5;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
score = Math.max(0, score);
|
|
18
|
+
|
|
19
|
+
const pointsDeducted = 100 - score;
|
|
20
|
+
let impact;
|
|
21
|
+
if (pointsDeducted <= 10) {
|
|
22
|
+
impact = 'Low';
|
|
23
|
+
} else if (pointsDeducted <= 30) {
|
|
24
|
+
impact = 'Medium';
|
|
25
|
+
} else {
|
|
26
|
+
impact = 'High';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return `Performance Score: ${score}/100\nIssues Found: ${issueCount}\nEstimated Impact: ${impact}\n`;
|
|
30
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Detect JSON.parse() calls inside loops',
|
|
6
|
+
category: 'performance',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: []
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
let loopDepth = 0;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a CallExpression node is a JSON.parse call
|
|
16
|
+
* @param {ASTNode} node - The CallExpression node
|
|
17
|
+
* @returns {boolean} - True if it's a JSON.parse call
|
|
18
|
+
*/
|
|
19
|
+
function isJSONParse(node) {
|
|
20
|
+
const { callee } = node;
|
|
21
|
+
|
|
22
|
+
// Must be a MemberExpression (e.g., JSON.parse)
|
|
23
|
+
if (!callee || callee.type !== 'MemberExpression') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get the object and property nodes
|
|
28
|
+
const objectNode = callee.object;
|
|
29
|
+
const propertyNode = callee.property;
|
|
30
|
+
|
|
31
|
+
// Check for JSON.parse pattern
|
|
32
|
+
if (objectNode.type === 'Identifier' && propertyNode.type === 'Identifier') {
|
|
33
|
+
const objectName = objectNode.name;
|
|
34
|
+
const propertyName = propertyNode.name;
|
|
35
|
+
|
|
36
|
+
if (objectName === 'JSON' && propertyName === 'parse') {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handle loop entry - increment depth counter
|
|
46
|
+
*/
|
|
47
|
+
function onLoopEnter() {
|
|
48
|
+
loopDepth++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Handle loop exit - decrement depth counter
|
|
53
|
+
*/
|
|
54
|
+
function onLoopExit() {
|
|
55
|
+
loopDepth--;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
// Traditional loop statements
|
|
60
|
+
ForStatement: onLoopEnter,
|
|
61
|
+
'ForStatement:exit': onLoopExit,
|
|
62
|
+
|
|
63
|
+
ForOfStatement: onLoopEnter,
|
|
64
|
+
'ForOfStatement:exit': onLoopExit,
|
|
65
|
+
|
|
66
|
+
ForInStatement: onLoopEnter,
|
|
67
|
+
'ForInStatement:exit': onLoopExit,
|
|
68
|
+
|
|
69
|
+
WhileStatement: onLoopEnter,
|
|
70
|
+
'WhileStatement:exit': onLoopExit,
|
|
71
|
+
|
|
72
|
+
DoWhileStatement: onLoopEnter,
|
|
73
|
+
'DoWhileStatement:exit': onLoopExit,
|
|
74
|
+
|
|
75
|
+
// Array method calls (forEach, map, filter, etc.)
|
|
76
|
+
CallExpression(node) {
|
|
77
|
+
// Check if this is an array method that acts as a loop
|
|
78
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
79
|
+
const methodName = node.callee.property.name;
|
|
80
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
81
|
+
|
|
82
|
+
if (arrayMethods.includes(methodName)) {
|
|
83
|
+
onLoopEnter();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for JSON.parse calls inside loops
|
|
88
|
+
if (loopDepth > 0 && isJSONParse(node)) {
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
message: 'JSON.parse() inside loop causes repeated expensive parsing operations'
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
'CallExpression:exit'(node) {
|
|
97
|
+
// Handle array method exit
|
|
98
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
99
|
+
const methodName = node.callee.property.name;
|
|
100
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
101
|
+
|
|
102
|
+
if (arrayMethods.includes(methodName)) {
|
|
103
|
+
onLoopExit();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Detect nested loops iterating over the same array reference',
|
|
6
|
+
category: 'performance',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: []
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
const loopStack = [];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract the iteration target from a loop node
|
|
16
|
+
* @param {ASTNode} node - The loop node
|
|
17
|
+
* @returns {ASTNode|null} - The iteration target node or null
|
|
18
|
+
*/
|
|
19
|
+
function extractIterationTarget(node) {
|
|
20
|
+
switch (node.type) {
|
|
21
|
+
case 'ForOfStatement':
|
|
22
|
+
case 'ForInStatement':
|
|
23
|
+
// for (item of array) or for (key in object)
|
|
24
|
+
return node.right;
|
|
25
|
+
|
|
26
|
+
case 'ForStatement':
|
|
27
|
+
// for (let i = 0; i < array.length; i++)
|
|
28
|
+
// Look for array.length pattern in the test condition
|
|
29
|
+
if (node.test && node.test.type === 'BinaryExpression') {
|
|
30
|
+
const { left, right } = node.test;
|
|
31
|
+
|
|
32
|
+
// Check if right side is array.length
|
|
33
|
+
if (right && right.type === 'MemberExpression' &&
|
|
34
|
+
right.property && right.property.name === 'length') {
|
|
35
|
+
return right.object;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check if left side is array.length
|
|
39
|
+
if (left && left.type === 'MemberExpression' &&
|
|
40
|
+
left.property && left.property.name === 'length') {
|
|
41
|
+
return left.object;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
|
|
46
|
+
case 'WhileStatement':
|
|
47
|
+
case 'DoWhileStatement':
|
|
48
|
+
// While loops are harder to analyze, skip for now
|
|
49
|
+
return null;
|
|
50
|
+
|
|
51
|
+
case 'CallExpression':
|
|
52
|
+
// array.forEach(), array.map(), etc.
|
|
53
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
54
|
+
const methodName = node.callee.property.name;
|
|
55
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
56
|
+
|
|
57
|
+
if (arrayMethods.includes(methodName)) {
|
|
58
|
+
return node.callee.object;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Check if two AST nodes represent the same reference
|
|
70
|
+
* @param {ASTNode} node1 - First node
|
|
71
|
+
* @param {ASTNode} node2 - Second node
|
|
72
|
+
* @returns {boolean} - True if they represent the same reference
|
|
73
|
+
*/
|
|
74
|
+
function isSameReference(node1, node2) {
|
|
75
|
+
if (!node1 || !node2) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Both are simple identifiers
|
|
80
|
+
if (node1.type === 'Identifier' && node2.type === 'Identifier') {
|
|
81
|
+
return node1.name === node2.name;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Both are member expressions (e.g., obj.arr)
|
|
85
|
+
if (node1.type === 'MemberExpression' && node2.type === 'MemberExpression') {
|
|
86
|
+
return isSameReference(node1.object, node2.object) &&
|
|
87
|
+
isSameReference(node1.property, node2.property);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if the current loop iterates over the same reference as any outer loop
|
|
95
|
+
* @param {ASTNode} target - The iteration target of the current loop
|
|
96
|
+
* @returns {boolean} - True if a match is found
|
|
97
|
+
*/
|
|
98
|
+
function checkForQuadraticPattern(target) {
|
|
99
|
+
for (const outerLoop of loopStack) {
|
|
100
|
+
if (isSameReference(target, outerLoop.target)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle loop entry
|
|
109
|
+
* @param {ASTNode} node - The loop node
|
|
110
|
+
*/
|
|
111
|
+
function onLoopEnter(node) {
|
|
112
|
+
const target = extractIterationTarget(node);
|
|
113
|
+
|
|
114
|
+
if (target && checkForQuadraticPattern(target)) {
|
|
115
|
+
context.report({
|
|
116
|
+
node,
|
|
117
|
+
message: 'Nested loop iterates over the same array reference, causing O(n²) complexity'
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
loopStack.push({ node, target });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle loop exit
|
|
126
|
+
*/
|
|
127
|
+
function onLoopExit() {
|
|
128
|
+
loopStack.pop();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
// Traditional loop statements
|
|
133
|
+
ForStatement: onLoopEnter,
|
|
134
|
+
'ForStatement:exit': onLoopExit,
|
|
135
|
+
|
|
136
|
+
ForOfStatement: onLoopEnter,
|
|
137
|
+
'ForOfStatement:exit': onLoopExit,
|
|
138
|
+
|
|
139
|
+
ForInStatement: onLoopEnter,
|
|
140
|
+
'ForInStatement:exit': onLoopExit,
|
|
141
|
+
|
|
142
|
+
WhileStatement: onLoopEnter,
|
|
143
|
+
'WhileStatement:exit': onLoopExit,
|
|
144
|
+
|
|
145
|
+
DoWhileStatement: onLoopEnter,
|
|
146
|
+
'DoWhileStatement:exit': onLoopExit,
|
|
147
|
+
|
|
148
|
+
// Array method calls (forEach, map, filter, etc.)
|
|
149
|
+
CallExpression(node) {
|
|
150
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
151
|
+
const methodName = node.callee.property.name;
|
|
152
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
153
|
+
|
|
154
|
+
if (arrayMethods.includes(methodName)) {
|
|
155
|
+
onLoopEnter(node);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
'CallExpression:exit'(node) {
|
|
161
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
162
|
+
const methodName = node.callee.property.name;
|
|
163
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
164
|
+
|
|
165
|
+
if (arrayMethods.includes(methodName)) {
|
|
166
|
+
onLoopExit();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Detect synchronous blocking calls inside loops',
|
|
6
|
+
category: 'performance',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: []
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
let loopDepth = 0;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a CallExpression node is a blocking synchronous call
|
|
16
|
+
* @param {ASTNode} node - The CallExpression node
|
|
17
|
+
* @returns {boolean} - True if it's a blocking call
|
|
18
|
+
*/
|
|
19
|
+
function isBlockingCall(node) {
|
|
20
|
+
const { callee } = node;
|
|
21
|
+
|
|
22
|
+
// Must be a MemberExpression (e.g., fs.readFileSync, child_process.execSync)
|
|
23
|
+
if (!callee || callee.type !== 'MemberExpression') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get the object and property names
|
|
28
|
+
const objectNode = callee.object;
|
|
29
|
+
const propertyNode = callee.property;
|
|
30
|
+
|
|
31
|
+
// Handle simple cases: fs.readFileSync, child_process.execSync
|
|
32
|
+
if (objectNode.type === 'Identifier' && propertyNode.type === 'Identifier') {
|
|
33
|
+
const objectName = objectNode.name;
|
|
34
|
+
const propertyName = propertyNode.name;
|
|
35
|
+
|
|
36
|
+
// Check for fs.*Sync patterns
|
|
37
|
+
if (objectName === 'fs' && propertyName.endsWith('Sync')) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Check for child_process.*Sync patterns
|
|
42
|
+
if (objectName === 'child_process' && propertyName.endsWith('Sync')) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle loop entry - increment depth counter
|
|
52
|
+
*/
|
|
53
|
+
function onLoopEnter() {
|
|
54
|
+
loopDepth++;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handle loop exit - decrement depth counter
|
|
59
|
+
*/
|
|
60
|
+
function onLoopExit() {
|
|
61
|
+
loopDepth--;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
// Traditional loop statements
|
|
66
|
+
ForStatement: onLoopEnter,
|
|
67
|
+
'ForStatement:exit': onLoopExit,
|
|
68
|
+
|
|
69
|
+
ForOfStatement: onLoopEnter,
|
|
70
|
+
'ForOfStatement:exit': onLoopExit,
|
|
71
|
+
|
|
72
|
+
ForInStatement: onLoopEnter,
|
|
73
|
+
'ForInStatement:exit': onLoopExit,
|
|
74
|
+
|
|
75
|
+
WhileStatement: onLoopEnter,
|
|
76
|
+
'WhileStatement:exit': onLoopExit,
|
|
77
|
+
|
|
78
|
+
DoWhileStatement: onLoopEnter,
|
|
79
|
+
'DoWhileStatement:exit': onLoopExit,
|
|
80
|
+
|
|
81
|
+
// Array method calls (forEach, map, filter, etc.)
|
|
82
|
+
CallExpression(node) {
|
|
83
|
+
// Check if this is an array method that acts as a loop
|
|
84
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
85
|
+
const methodName = node.callee.property.name;
|
|
86
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
87
|
+
|
|
88
|
+
if (arrayMethods.includes(methodName)) {
|
|
89
|
+
onLoopEnter();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check for blocking calls inside loops
|
|
94
|
+
if (loopDepth > 0 && isBlockingCall(node)) {
|
|
95
|
+
context.report({
|
|
96
|
+
node,
|
|
97
|
+
message: 'Synchronous blocking call inside loop blocks the event loop repeatedly'
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'CallExpression:exit'(node) {
|
|
103
|
+
// Handle array method exit
|
|
104
|
+
if (node.callee && node.callee.type === 'MemberExpression') {
|
|
105
|
+
const methodName = node.callee.property.name;
|
|
106
|
+
const arrayMethods = ['forEach', 'map', 'filter', 'reduce', 'some', 'every', 'find'];
|
|
107
|
+
|
|
108
|
+
if (arrayMethods.includes(methodName)) {
|
|
109
|
+
onLoopExit();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Detect array cloning when the cloned array is never mutated',
|
|
6
|
+
category: 'performance',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: []
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
/**
|
|
13
|
+
* Check if a node represents an array cloning pattern
|
|
14
|
+
* @param {ASTNode} node - The node to check
|
|
15
|
+
* @returns {boolean} - True if it's a cloning pattern
|
|
16
|
+
*/
|
|
17
|
+
function isCloningPattern(node) {
|
|
18
|
+
if (!node) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Pattern 1: Spread operator [...arr]
|
|
23
|
+
if (node.type === 'ArrayExpression' &&
|
|
24
|
+
node.elements.length === 1 &&
|
|
25
|
+
node.elements[0] &&
|
|
26
|
+
node.elements[0].type === 'SpreadElement') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Pattern 2: Array.from(arr)
|
|
31
|
+
if (node.type === 'CallExpression' &&
|
|
32
|
+
node.callee &&
|
|
33
|
+
node.callee.type === 'MemberExpression' &&
|
|
34
|
+
node.callee.object &&
|
|
35
|
+
node.callee.object.type === 'Identifier' &&
|
|
36
|
+
node.callee.object.name === 'Array' &&
|
|
37
|
+
node.callee.property &&
|
|
38
|
+
node.callee.property.name === 'from') {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Pattern 3: arr.slice() with no arguments
|
|
43
|
+
if (node.type === 'CallExpression' &&
|
|
44
|
+
node.callee &&
|
|
45
|
+
node.callee.type === 'MemberExpression' &&
|
|
46
|
+
node.callee.property &&
|
|
47
|
+
node.callee.property.name === 'slice' &&
|
|
48
|
+
node.arguments.length === 0) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a reference represents a mutation
|
|
57
|
+
* @param {Reference} reference - The variable reference to check
|
|
58
|
+
* @returns {boolean} - True if it's a mutating reference
|
|
59
|
+
*/
|
|
60
|
+
function isMutatingReference(reference) {
|
|
61
|
+
const identifier = reference.identifier;
|
|
62
|
+
const parent = identifier.parent;
|
|
63
|
+
|
|
64
|
+
if (!parent) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pattern 1: Assignment expression (clone[0] = value)
|
|
69
|
+
if (parent.type === 'AssignmentExpression' && parent.left === identifier) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Pattern 2: Member expression for array index assignment (clone[0] = value)
|
|
74
|
+
if (parent.type === 'MemberExpression' && parent.object === identifier) {
|
|
75
|
+
const grandparent = parent.parent;
|
|
76
|
+
|
|
77
|
+
// Check if the member expression is on the left side of an assignment
|
|
78
|
+
if (grandparent &&
|
|
79
|
+
grandparent.type === 'AssignmentExpression' &&
|
|
80
|
+
grandparent.left === parent) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if it's a mutating method call
|
|
85
|
+
if (grandparent && grandparent.type === 'CallExpression') {
|
|
86
|
+
const methodName = parent.property.name;
|
|
87
|
+
const mutatingMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
|
|
88
|
+
|
|
89
|
+
if (mutatingMethods.includes(methodName)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a variable is mutated in its scope
|
|
100
|
+
* @param {Variable} variable - The variable to check
|
|
101
|
+
* @returns {boolean} - True if the variable is mutated
|
|
102
|
+
*/
|
|
103
|
+
function isVariableMutated(variable) {
|
|
104
|
+
if (!variable || !variable.references) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check all references to the variable
|
|
109
|
+
for (const reference of variable.references) {
|
|
110
|
+
// Skip the initial declaration/write
|
|
111
|
+
if (reference.init) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (isMutatingReference(reference)) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
VariableDeclarator(node) {
|
|
125
|
+
// Check if this is a cloning pattern
|
|
126
|
+
if (!isCloningPattern(node.init)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Get the variable name
|
|
131
|
+
if (!node.id || node.id.type !== 'Identifier') {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const variableName = node.id.name;
|
|
136
|
+
|
|
137
|
+
// Get the scope and find the variable
|
|
138
|
+
const sourceCode = context.sourceCode || context.getSourceCode();
|
|
139
|
+
const scope = sourceCode.getScope(node);
|
|
140
|
+
const variable = scope.set.get(variableName);
|
|
141
|
+
|
|
142
|
+
if (!variable) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if the variable is mutated
|
|
147
|
+
if (!isVariableMutated(variable)) {
|
|
148
|
+
context.report({
|
|
149
|
+
node,
|
|
150
|
+
message: 'Unnecessary array clone - array is never mutated'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description: 'Detect inline object, array, and function creation in JSX props',
|
|
6
|
+
category: 'performance',
|
|
7
|
+
recommended: true
|
|
8
|
+
},
|
|
9
|
+
schema: []
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
/**
|
|
13
|
+
* Check if an expression is an inline creation pattern
|
|
14
|
+
* @param {ASTNode} expression - The expression node to check
|
|
15
|
+
* @returns {string|null} - The type of inline creation or null
|
|
16
|
+
*/
|
|
17
|
+
function getInlineCreationType(expression) {
|
|
18
|
+
if (!expression) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
switch (expression.type) {
|
|
23
|
+
case 'ObjectExpression':
|
|
24
|
+
return 'object';
|
|
25
|
+
|
|
26
|
+
case 'ArrayExpression':
|
|
27
|
+
return 'array';
|
|
28
|
+
|
|
29
|
+
case 'ArrowFunctionExpression':
|
|
30
|
+
case 'FunctionExpression':
|
|
31
|
+
return 'function';
|
|
32
|
+
|
|
33
|
+
default:
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get appropriate message for the inline creation type
|
|
40
|
+
* @param {string} type - The type of inline creation
|
|
41
|
+
* @returns {string} - The warning message
|
|
42
|
+
*/
|
|
43
|
+
function getMessage(type) {
|
|
44
|
+
switch (type) {
|
|
45
|
+
case 'object':
|
|
46
|
+
return 'Inline object creation in JSX prop causes unnecessary re-renders';
|
|
47
|
+
|
|
48
|
+
case 'array':
|
|
49
|
+
return 'Inline array creation in JSX prop causes unnecessary re-renders';
|
|
50
|
+
|
|
51
|
+
case 'function':
|
|
52
|
+
return 'Inline function creation in JSX prop causes unnecessary re-renders';
|
|
53
|
+
|
|
54
|
+
default:
|
|
55
|
+
return 'Inline creation in JSX prop causes unnecessary re-renders';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
JSXAttribute(node) {
|
|
61
|
+
// Check if the attribute has a value
|
|
62
|
+
if (!node.value) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check if the value is a JSXExpressionContainer
|
|
67
|
+
if (node.value.type === 'JSXExpressionContainer') {
|
|
68
|
+
const expression = node.value.expression;
|
|
69
|
+
const creationType = getInlineCreationType(expression);
|
|
70
|
+
|
|
71
|
+
if (creationType) {
|
|
72
|
+
context.report({
|
|
73
|
+
node: node.value,
|
|
74
|
+
message: getMessage(creationType)
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
};
|