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 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
+ };