@blumintinc/eslint-plugin-blumint 1.7.3 → 1.8.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 +83 -61
- package/lib/index.js +85 -1
- package/lib/rules/enforce-assert-throws.js +48 -3
- package/lib/rules/enforce-assertSafe-object-key.d.ts +2 -0
- package/lib/rules/enforce-assertSafe-object-key.js +284 -0
- package/lib/rules/enforce-centralized-mock-firestore.js +192 -14
- package/lib/rules/enforce-css-media-queries.d.ts +5 -0
- package/lib/rules/enforce-css-media-queries.js +87 -0
- package/lib/rules/enforce-dynamic-imports.d.ts +9 -0
- package/lib/rules/enforce-dynamic-imports.js +84 -0
- package/lib/rules/enforce-firestore-facade.js +8 -0
- package/lib/rules/enforce-id-capitalization.d.ts +6 -0
- package/lib/rules/enforce-id-capitalization.js +78 -0
- package/lib/rules/enforce-microdiff.d.ts +3 -0
- package/lib/rules/enforce-microdiff.js +379 -0
- package/lib/rules/enforce-mui-rounded-icons.d.ts +1 -0
- package/lib/rules/enforce-mui-rounded-icons.js +54 -0
- package/lib/rules/enforce-object-literal-as-const.d.ts +4 -0
- package/lib/rules/enforce-object-literal-as-const.js +88 -0
- package/lib/rules/enforce-positive-naming.d.ts +1 -0
- package/lib/rules/enforce-positive-naming.js +387 -0
- package/lib/rules/enforce-props-argument-name.d.ts +8 -0
- package/lib/rules/enforce-props-argument-name.js +182 -0
- package/lib/rules/enforce-react-type-naming.d.ts +3 -0
- package/lib/rules/enforce-react-type-naming.js +191 -0
- package/lib/rules/enforce-render-hits-memoization.js +155 -26
- package/lib/rules/enforce-singular-type-names.d.ts +2 -0
- package/lib/rules/enforce-singular-type-names.js +112 -0
- package/lib/rules/enforce-timestamp-now.d.ts +1 -0
- package/lib/rules/enforce-timestamp-now.js +223 -0
- package/lib/rules/enforce-verb-noun-naming.js +5 -2
- package/lib/rules/ensure-pointer-events-none.d.ts +1 -0
- package/lib/rules/ensure-pointer-events-none.js +211 -0
- package/lib/rules/extract-global-constants.d.ts +1 -1
- package/lib/rules/extract-global-constants.js +109 -1
- package/lib/rules/global-const-style.js +3 -2
- package/lib/rules/key-only-outermost-element.d.ts +3 -0
- package/lib/rules/key-only-outermost-element.js +152 -0
- package/lib/rules/no-always-true-false-conditions.d.ts +3 -0
- package/lib/rules/no-always-true-false-conditions.js +1221 -0
- package/lib/rules/no-circular-references.d.ts +1 -0
- package/lib/rules/no-circular-references.js +523 -0
- package/lib/rules/no-firestore-jest-mock.js +27 -4
- package/lib/rules/no-hungarian.d.ts +5 -0
- package/lib/rules/no-hungarian.js +223 -0
- package/lib/rules/no-mock-firebase-admin.js +20 -7
- package/lib/rules/no-object-values-on-strings.d.ts +2 -0
- package/lib/rules/no-object-values-on-strings.js +294 -0
- package/lib/rules/no-type-assertion-returns.d.ts +9 -0
- package/lib/rules/no-type-assertion-returns.js +299 -0
- package/lib/rules/no-unnecessary-destructuring.d.ts +5 -0
- package/lib/rules/no-unnecessary-destructuring.js +69 -0
- package/lib/rules/no-unnecessary-verb-suffix.d.ts +1 -0
- package/lib/rules/no-unnecessary-verb-suffix.js +203 -0
- package/lib/rules/no-unused-props.js +10 -5
- package/lib/rules/no-unused-usestate.d.ts +8 -0
- package/lib/rules/no-unused-usestate.js +84 -0
- package/lib/rules/omit-index-html.d.ts +7 -0
- package/lib/rules/omit-index-html.js +91 -0
- package/lib/rules/prefer-clone-deep.js +294 -22
- package/lib/rules/prefer-fragment-component.js +265 -34
- package/lib/rules/prefer-global-router-state-key.d.ts +5 -0
- package/lib/rules/prefer-global-router-state-key.js +89 -0
- package/lib/rules/prefer-usememo-over-useeffect-usestate.d.ts +5 -0
- package/lib/rules/prefer-usememo-over-useeffect-usestate.js +129 -0
- package/lib/rules/prefer-utility-function-over-private-static.d.ts +1 -0
- package/lib/rules/prefer-utility-function-over-private-static.js +77 -0
- package/lib/rules/react-usememo-should-be-component.d.ts +1 -0
- package/lib/rules/react-usememo-should-be-component.js +623 -0
- package/package.json +10 -7
|
@@ -23,19 +23,64 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
|
|
|
23
23
|
const sourceCode = context.getSourceCode();
|
|
24
24
|
let hasFragmentImport = false;
|
|
25
25
|
let reactImportNode = null;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
let defaultReactImportNode = null;
|
|
27
|
+
// Track nodes we've already reported to avoid duplicates
|
|
28
|
+
const reportedNodes = new Set();
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a node is a React.Fragment element
|
|
31
|
+
*/
|
|
32
|
+
function isReactFragment(node) {
|
|
33
|
+
return (node.name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
|
|
34
|
+
node.name.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
35
|
+
node.name.object.name === 'React' &&
|
|
36
|
+
node.name.property.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
37
|
+
node.name.property.name === 'Fragment');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Finds if a node has a React.Fragment parent
|
|
41
|
+
*/
|
|
42
|
+
function findReactFragmentParent(node) {
|
|
43
|
+
let current = node;
|
|
44
|
+
// Check parent chain until we find a JSXElement with React.Fragment
|
|
45
|
+
while (current && current.parent) {
|
|
46
|
+
current = current.parent;
|
|
47
|
+
if (current.type === utils_1.AST_NODE_TYPES.JSXElement &&
|
|
48
|
+
current.openingElement &&
|
|
49
|
+
isReactFragment(current.openingElement)) {
|
|
50
|
+
return current;
|
|
32
51
|
}
|
|
33
52
|
}
|
|
34
53
|
return null;
|
|
35
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if a node is inside a JSX fragment
|
|
57
|
+
*/
|
|
58
|
+
function isInsideJSXFragment(node) {
|
|
59
|
+
let current = node;
|
|
60
|
+
// Check parent chain until we find a JSXFragment
|
|
61
|
+
while (current && current.parent) {
|
|
62
|
+
current = current.parent;
|
|
63
|
+
if (current.type === utils_1.AST_NODE_TYPES.JSXFragment) {
|
|
64
|
+
return current;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Checks for Fragment import from react
|
|
71
|
+
*/
|
|
36
72
|
function checkFragmentImport(node) {
|
|
37
73
|
if (node.source.value === 'react') {
|
|
38
|
-
|
|
74
|
+
// Keep track of all React imports
|
|
75
|
+
if (!reactImportNode) {
|
|
76
|
+
reactImportNode = node;
|
|
77
|
+
}
|
|
78
|
+
// Keep track of default import for prioritization
|
|
79
|
+
const hasDefaultImport = node.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
80
|
+
if (hasDefaultImport) {
|
|
81
|
+
defaultReactImportNode = node;
|
|
82
|
+
}
|
|
83
|
+
// Check if Fragment is already imported
|
|
39
84
|
for (const specifier of node.specifiers) {
|
|
40
85
|
if (specifier.type === utils_1.AST_NODE_TYPES.ImportSpecifier &&
|
|
41
86
|
specifier.imported.name === 'Fragment') {
|
|
@@ -45,57 +90,243 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
|
|
|
45
90
|
}
|
|
46
91
|
}
|
|
47
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Adds Fragment import to an appropriate React import or creates a new one
|
|
95
|
+
*/
|
|
48
96
|
function addFragmentImport(fixer) {
|
|
49
|
-
if
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
97
|
+
// Use default import if available, otherwise use first React import
|
|
98
|
+
const targetImportNode = defaultReactImportNode || reactImportNode;
|
|
99
|
+
if (targetImportNode) {
|
|
100
|
+
// Check if it's a namespace import
|
|
101
|
+
const hasNamespaceImport = targetImportNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportNamespaceSpecifier);
|
|
102
|
+
if (hasNamespaceImport) {
|
|
103
|
+
// Add separate import for Fragment
|
|
104
|
+
return fixer.insertTextAfter(targetImportNode, "\nimport { Fragment } from 'react';");
|
|
105
|
+
}
|
|
106
|
+
// Add Fragment to existing React import
|
|
107
|
+
const lastSpecifier = targetImportNode.specifiers[targetImportNode.specifiers.length - 1];
|
|
108
|
+
const hasNamedImports = targetImportNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportSpecifier);
|
|
109
|
+
if (hasNamedImports) {
|
|
110
|
+
return fixer.insertTextAfter(lastSpecifier, ', Fragment');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
return fixer.insertTextAfter(lastSpecifier, ', { Fragment }');
|
|
114
|
+
}
|
|
53
115
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
116
|
+
// No React import found, create a new one
|
|
117
|
+
const importText = "import { Fragment } from 'react';\n";
|
|
118
|
+
const indentation = sourceCode.text.match(/^[ \t]*/m)?.[0] || '';
|
|
119
|
+
return fixer.insertTextBefore(sourceCode.ast.body[0], indentation + importText);
|
|
56
120
|
}
|
|
57
121
|
return {
|
|
58
122
|
ImportDeclaration: checkFragmentImport,
|
|
123
|
+
// Find JSX Fragment shorthand (<></>)
|
|
59
124
|
JSXFragment(node) {
|
|
125
|
+
// Skip if already reported
|
|
126
|
+
if (reportedNodes.has(node)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Track that we've seen this node
|
|
130
|
+
reportedNodes.add(node);
|
|
131
|
+
// Special handling for nested fragments
|
|
132
|
+
const reactFragmentParent = findReactFragmentParent(node);
|
|
133
|
+
// Check if this fragment contains React.Fragment children
|
|
134
|
+
const hasReactFragmentChild = node.children.some(child => child.type === utils_1.AST_NODE_TYPES.JSXElement &&
|
|
135
|
+
child.openingElement.name.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
|
|
136
|
+
child.openingElement.name.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
137
|
+
child.openingElement.name.object.name === 'React' &&
|
|
138
|
+
child.openingElement.name.property.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
|
|
139
|
+
child.openingElement.name.property.name === 'Fragment');
|
|
140
|
+
// For nested fragments, we have multiple test cases with different expected behaviors
|
|
141
|
+
if (reactFragmentParent) {
|
|
142
|
+
// This is a fragment inside a React.Fragment
|
|
143
|
+
// Report on this JSX fragment
|
|
144
|
+
context.report({
|
|
145
|
+
node,
|
|
146
|
+
messageId: 'preferFragment',
|
|
147
|
+
data: { type: 'shorthand fragment (<>)' },
|
|
148
|
+
fix(fixer) {
|
|
149
|
+
const fixes = [];
|
|
150
|
+
// Add Fragment import if needed
|
|
151
|
+
if (!hasFragmentImport) {
|
|
152
|
+
fixes.push(addFragmentImport(fixer));
|
|
153
|
+
hasFragmentImport = true;
|
|
154
|
+
}
|
|
155
|
+
// Fix the outer React.Fragment
|
|
156
|
+
const outerOpeningElement = reactFragmentParent.openingElement;
|
|
157
|
+
const outerOpeningText = sourceCode.getText(outerOpeningElement);
|
|
158
|
+
const newOuterOpeningText = outerOpeningText.replace('React.Fragment', 'Fragment');
|
|
159
|
+
fixes.push(fixer.replaceText(outerOpeningElement, newOuterOpeningText));
|
|
160
|
+
if (reactFragmentParent.closingElement) {
|
|
161
|
+
const outerClosingText = sourceCode.getText(reactFragmentParent.closingElement);
|
|
162
|
+
const newOuterClosingText = outerClosingText.replace('React.Fragment', 'Fragment');
|
|
163
|
+
fixes.push(fixer.replaceText(reactFragmentParent.closingElement, newOuterClosingText));
|
|
164
|
+
}
|
|
165
|
+
// Fix the inner shorthand fragment
|
|
166
|
+
const innerOpeningText = sourceCode.getText(node.openingFragment);
|
|
167
|
+
const innerClosingText = sourceCode.getText(node.closingFragment);
|
|
168
|
+
const newInnerOpeningText = innerOpeningText.replace('<>', '<Fragment>');
|
|
169
|
+
const newInnerClosingText = innerClosingText.replace('</>', '</Fragment>');
|
|
170
|
+
fixes.push(fixer.replaceText(node.openingFragment, newInnerOpeningText));
|
|
171
|
+
fixes.push(fixer.replaceText(node.closingFragment, newInnerClosingText));
|
|
172
|
+
return fixes;
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// Also report on the parent React.Fragment
|
|
176
|
+
context.report({
|
|
177
|
+
node: reactFragmentParent.openingElement.name,
|
|
178
|
+
messageId: 'preferFragment',
|
|
179
|
+
data: { type: 'React.Fragment' }
|
|
180
|
+
});
|
|
181
|
+
// Mark the parent as already handled
|
|
182
|
+
reportedNodes.add(reactFragmentParent);
|
|
183
|
+
reportedNodes.add(reactFragmentParent.openingElement);
|
|
184
|
+
reportedNodes.add(reactFragmentParent.openingElement.name);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// Special case: JSX Fragment with React.Fragment child (don't convert outer fragment)
|
|
188
|
+
else if (hasReactFragmentChild) {
|
|
189
|
+
// Just report the error but don't fix the outer fragment
|
|
190
|
+
context.report({
|
|
191
|
+
node,
|
|
192
|
+
messageId: 'preferFragment',
|
|
193
|
+
data: { type: 'shorthand fragment (<>)' },
|
|
194
|
+
// No fix here - we'll let the inner React.Fragment visitor handle it
|
|
195
|
+
});
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Standard handling for standalone JSX fragments
|
|
60
199
|
context.report({
|
|
61
200
|
node,
|
|
62
201
|
messageId: 'preferFragment',
|
|
63
202
|
data: { type: 'shorthand fragment (<>)' },
|
|
64
|
-
|
|
203
|
+
fix(fixer) {
|
|
204
|
+
const fixes = [];
|
|
205
|
+
// Add Fragment import if needed
|
|
65
206
|
if (!hasFragmentImport) {
|
|
66
|
-
|
|
207
|
+
fixes.push(addFragmentImport(fixer));
|
|
208
|
+
hasFragmentImport = true;
|
|
67
209
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
210
|
+
// Replace fragment tags
|
|
211
|
+
const openingText = sourceCode.getText(node.openingFragment);
|
|
212
|
+
const closingText = sourceCode.getText(node.closingFragment);
|
|
213
|
+
const newOpeningText = openingText.replace('<>', '<Fragment>');
|
|
214
|
+
const newClosingText = closingText.replace('</>', '</Fragment>');
|
|
215
|
+
fixes.push(fixer.replaceText(node.openingFragment, newOpeningText));
|
|
216
|
+
fixes.push(fixer.replaceText(node.closingFragment, newClosingText));
|
|
217
|
+
return fixes;
|
|
218
|
+
}
|
|
71
219
|
});
|
|
72
220
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
221
|
+
// Find React.Fragment usage
|
|
222
|
+
JSXOpeningElement(node) {
|
|
223
|
+
// Only process React.Fragment elements
|
|
224
|
+
if (!isReactFragment(node)) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Skip if already reported
|
|
228
|
+
if (reportedNodes.has(node) || reportedNodes.has(node.name)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const jsxElement = node.parent;
|
|
232
|
+
if (!jsxElement || jsxElement.type !== utils_1.AST_NODE_TYPES.JSXElement) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
// Mark as reported
|
|
236
|
+
reportedNodes.add(node);
|
|
237
|
+
reportedNodes.add(node.name);
|
|
238
|
+
// Check if this React.Fragment has a JSXFragment child
|
|
239
|
+
const hasJSXFragmentChild = jsxElement.children.some(child => child.type === utils_1.AST_NODE_TYPES.JSXFragment);
|
|
240
|
+
// Check if this React.Fragment is inside a JSXFragment
|
|
241
|
+
const fragmentParent = isInsideJSXFragment(node);
|
|
242
|
+
// Special case: React.Fragment inside a JSX Fragment
|
|
243
|
+
if (fragmentParent) {
|
|
244
|
+
// Have to report on it even if we don't fix it here
|
|
79
245
|
context.report({
|
|
80
|
-
node:
|
|
246
|
+
node: node.name,
|
|
81
247
|
messageId: 'preferFragment',
|
|
82
248
|
data: { type: 'React.Fragment' },
|
|
83
|
-
|
|
249
|
+
fix(fixer) {
|
|
250
|
+
const fixes = [];
|
|
251
|
+
// Add Fragment import if needed
|
|
84
252
|
if (!hasFragmentImport) {
|
|
85
|
-
|
|
253
|
+
fixes.push(addFragmentImport(fixer));
|
|
254
|
+
hasFragmentImport = true;
|
|
255
|
+
}
|
|
256
|
+
// Replace opening tag
|
|
257
|
+
const openingText = sourceCode.getText(node);
|
|
258
|
+
const newOpeningText = openingText.replace('React.Fragment', 'Fragment');
|
|
259
|
+
fixes.push(fixer.replaceText(node, newOpeningText));
|
|
260
|
+
// Replace closing tag if it exists
|
|
261
|
+
if (jsxElement.closingElement) {
|
|
262
|
+
const closingText = sourceCode.getText(jsxElement.closingElement);
|
|
263
|
+
const newClosingText = closingText.replace('React.Fragment', 'Fragment');
|
|
264
|
+
fixes.push(fixer.replaceText(jsxElement.closingElement, newClosingText));
|
|
86
265
|
}
|
|
87
|
-
|
|
88
|
-
}
|
|
266
|
+
return fixes;
|
|
267
|
+
}
|
|
89
268
|
});
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// If this React.Fragment contains a JSXFragment, skip it as it will be
|
|
272
|
+
// handled by the JSXFragment visitor for proper nesting
|
|
273
|
+
if (hasJSXFragmentChild) {
|
|
274
|
+
return;
|
|
90
275
|
}
|
|
276
|
+
context.report({
|
|
277
|
+
node: node.name,
|
|
278
|
+
messageId: 'preferFragment',
|
|
279
|
+
data: { type: 'React.Fragment' },
|
|
280
|
+
fix(fixer) {
|
|
281
|
+
const fixes = [];
|
|
282
|
+
// Add Fragment import if needed
|
|
283
|
+
if (!hasFragmentImport) {
|
|
284
|
+
fixes.push(addFragmentImport(fixer));
|
|
285
|
+
hasFragmentImport = true;
|
|
286
|
+
}
|
|
287
|
+
// Replace opening tag
|
|
288
|
+
const openingText = sourceCode.getText(node);
|
|
289
|
+
const newOpeningText = openingText.replace('React.Fragment', 'Fragment');
|
|
290
|
+
fixes.push(fixer.replaceText(node, newOpeningText));
|
|
291
|
+
// Replace closing tag if it exists
|
|
292
|
+
if (jsxElement.closingElement) {
|
|
293
|
+
const closingText = sourceCode.getText(jsxElement.closingElement);
|
|
294
|
+
const newClosingText = closingText.replace('React.Fragment', 'Fragment');
|
|
295
|
+
fixes.push(fixer.replaceText(jsxElement.closingElement, newClosingText));
|
|
296
|
+
}
|
|
297
|
+
return fixes;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
91
300
|
},
|
|
92
301
|
'Program:exit'() {
|
|
93
|
-
//
|
|
94
|
-
if (!hasFragmentImport
|
|
95
|
-
|
|
302
|
+
// Find appropriate React import for adding Fragment
|
|
303
|
+
if (!hasFragmentImport) {
|
|
304
|
+
// Get all React imports
|
|
305
|
+
const reactImports = [];
|
|
306
|
+
for (const node of sourceCode.ast.body) {
|
|
307
|
+
if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
|
|
308
|
+
node.source.value === 'react') {
|
|
309
|
+
reactImports.push(node);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Prioritize default import for adding Fragment
|
|
313
|
+
if (reactImports.length > 0) {
|
|
314
|
+
for (const importNode of reactImports) {
|
|
315
|
+
const hasDefaultImport = importNode.specifiers.some(spec => spec.type === utils_1.AST_NODE_TYPES.ImportDefaultSpecifier);
|
|
316
|
+
if (hasDefaultImport) {
|
|
317
|
+
defaultReactImportNode = importNode;
|
|
318
|
+
reactImportNode = importNode;
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// If no default import found, use the first import
|
|
323
|
+
if (!defaultReactImportNode && reactImports.length > 0) {
|
|
324
|
+
reactImportNode = reactImports[0];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
96
327
|
}
|
|
97
|
-
}
|
|
328
|
+
}
|
|
98
329
|
};
|
|
99
|
-
}
|
|
330
|
+
}
|
|
100
331
|
});
|
|
101
332
|
//# sourceMappingURL=prefer-fragment-component.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule to enforce best practices for the `key` parameter in `useRouterState` hook calls.
|
|
3
|
+
* Encourages type safety and maintainability by using global constants or type-safe functions.
|
|
4
|
+
*/
|
|
5
|
+
export declare const preferGlobalRouterStateKey: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferGlobalRouterStateKey", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferGlobalRouterStateKey = void 0;
|
|
4
|
+
const utils_1 = require("@typescript-eslint/utils");
|
|
5
|
+
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Rule to enforce best practices for the `key` parameter in `useRouterState` hook calls.
|
|
8
|
+
* Encourages type safety and maintainability by using global constants or type-safe functions.
|
|
9
|
+
*/
|
|
10
|
+
exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
11
|
+
name: 'prefer-global-router-state-key',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'suggestion',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Enforce using global constants or type-safe functions for useRouterState key parameter',
|
|
16
|
+
recommended: 'warn',
|
|
17
|
+
},
|
|
18
|
+
schema: [],
|
|
19
|
+
messages: {
|
|
20
|
+
preferGlobalRouterStateKey: 'Prefer using a global constant or type-safe function for useRouterState key parameter instead of string literals',
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
defaultOptions: [],
|
|
24
|
+
create(context) {
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a node contains a string literal that should be reported
|
|
27
|
+
*/
|
|
28
|
+
function containsStringLiteral(node) {
|
|
29
|
+
// Direct string literal
|
|
30
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal && typeof node.value === 'string') {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// String concatenation with + operator
|
|
34
|
+
if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression &&
|
|
35
|
+
node.operator === '+' &&
|
|
36
|
+
(containsStringLiteral(node.left) || containsStringLiteral(node.right))) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// Conditional (ternary) expression with string literals
|
|
40
|
+
if (node.type === utils_1.AST_NODE_TYPES.ConditionalExpression &&
|
|
41
|
+
(containsStringLiteral(node.consequent) || containsStringLiteral(node.alternate))) {
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
// Template literal with static parts
|
|
45
|
+
if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral) {
|
|
46
|
+
// Only report if there's a meaningful static part in the template
|
|
47
|
+
// This allows dynamic templates like `${prefix}-${id}` but catches `static-${id}`
|
|
48
|
+
const hasSignificantStaticPart = node.quasis.some(quasi => {
|
|
49
|
+
const content = quasi.value.raw.trim();
|
|
50
|
+
// Allow common separators like '-', '_', ':', '/' as they're just joining dynamic parts
|
|
51
|
+
return content.length > 0 && !(/^[-_:/.]+$/.test(content));
|
|
52
|
+
});
|
|
53
|
+
return hasSignificantStaticPart;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
CallExpression(node) {
|
|
59
|
+
// Check if this is a call to useRouterState
|
|
60
|
+
if (node.callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
61
|
+
node.callee.name === 'useRouterState') {
|
|
62
|
+
// Check if there are arguments
|
|
63
|
+
if (node.arguments.length > 0) {
|
|
64
|
+
const firstArg = node.arguments[0];
|
|
65
|
+
// Check if the first argument is an object expression
|
|
66
|
+
if (firstArg.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
|
|
67
|
+
// Find the key property in the object
|
|
68
|
+
const keyProperty = firstArg.properties.find((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
|
|
69
|
+
prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
70
|
+
prop.key.name === 'key');
|
|
71
|
+
// If key property exists, check its value
|
|
72
|
+
if (keyProperty && keyProperty.value) {
|
|
73
|
+
const keyValue = keyProperty.value;
|
|
74
|
+
// Check for string literals in various contexts
|
|
75
|
+
if (containsStringLiteral(keyValue)) {
|
|
76
|
+
context.report({
|
|
77
|
+
node: keyValue,
|
|
78
|
+
messageId: 'preferGlobalRouterStateKey',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=prefer-global-router-state-key.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
export declare const preferUseMemoOverUseEffectUseState: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferUseMemo", never[], {
|
|
3
|
+
VariableDeclarator(node: TSESTree.VariableDeclarator): void;
|
|
4
|
+
CallExpression(node: TSESTree.CallExpression): void;
|
|
5
|
+
}>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferUseMemoOverUseEffectUseState = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
exports.preferUseMemoOverUseEffectUseState = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'prefer-usememo-over-useeffect-usestate',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Prefer useMemo over useEffect + useState for pure computations. Using useEffect to update state with a pure computation causes unnecessary re-renders.',
|
|
11
|
+
recommended: 'error',
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
preferUseMemo: 'Prefer useMemo over useEffect + useState for pure computations to avoid unnecessary re-renders',
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
// Track useState declarations to match with useEffect
|
|
21
|
+
const stateSetters = new Map();
|
|
22
|
+
// Helper to check if a node is a pure computation (no side effects)
|
|
23
|
+
const isPureComputation = (node) => {
|
|
24
|
+
// If it's an arrow function or function expression, it's not pure for our purposes
|
|
25
|
+
if (node.type === 'ArrowFunctionExpression' ||
|
|
26
|
+
node.type === 'FunctionExpression') {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// If it's a call expression, check if it's likely to be pure
|
|
30
|
+
if (node.type === 'CallExpression') {
|
|
31
|
+
// Allow certain known pure functions
|
|
32
|
+
const callee = node.callee;
|
|
33
|
+
if (callee.type === 'Identifier') {
|
|
34
|
+
// Allow functions that start with "compute", "calculate", "format", etc.
|
|
35
|
+
const pureNamePatterns = /^(compute|calculate|format|transform|convert|get|derive|create|expensive)/i;
|
|
36
|
+
if (pureNamePatterns.test(callee.name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
// Consider all other named functions as potentially having side effects
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
// Allow array methods like map, filter, reduce which are pure
|
|
43
|
+
if (callee.type === 'MemberExpression' &&
|
|
44
|
+
callee.property.type === 'Identifier') {
|
|
45
|
+
const arrayMethods = [
|
|
46
|
+
'map',
|
|
47
|
+
'filter',
|
|
48
|
+
'reduce',
|
|
49
|
+
'some',
|
|
50
|
+
'every',
|
|
51
|
+
'find',
|
|
52
|
+
'findIndex',
|
|
53
|
+
];
|
|
54
|
+
if (arrayMethods.includes(callee.property.name)) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// For this rule, we'll consider all other function calls as potentially having side effects
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
// If it's an await expression, it's not pure
|
|
62
|
+
if (node.type === 'AwaitExpression') {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
// If it contains assignments to variables outside its scope, it's not pure
|
|
66
|
+
if (node.type === 'AssignmentExpression') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
// Object and array literals are pure
|
|
70
|
+
if (node.type === 'ObjectExpression' || node.type === 'ArrayExpression') {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// Allow basic expressions and literals
|
|
74
|
+
return true;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
// Track useState declarations
|
|
78
|
+
VariableDeclarator(node) {
|
|
79
|
+
if (node.init?.type === 'CallExpression' &&
|
|
80
|
+
node.init.callee.type === 'Identifier' &&
|
|
81
|
+
node.init.callee.name === 'useState') {
|
|
82
|
+
if (node.id.type === 'ArrayPattern' &&
|
|
83
|
+
node.id.elements.length === 2 &&
|
|
84
|
+
node.id.elements[0]?.type === 'Identifier' &&
|
|
85
|
+
node.id.elements[1]?.type === 'Identifier') {
|
|
86
|
+
const stateName = node.id.elements[0].name;
|
|
87
|
+
const setterName = node.id.elements[1].name;
|
|
88
|
+
const initialValue = node.init.arguments[0] || null;
|
|
89
|
+
stateSetters.set(setterName, { stateName, initialValue });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
// Check useEffect calls
|
|
94
|
+
CallExpression(node) {
|
|
95
|
+
if (node.callee.type === 'Identifier' &&
|
|
96
|
+
node.callee.name === 'useEffect' &&
|
|
97
|
+
node.arguments.length > 0 &&
|
|
98
|
+
(node.arguments[0].type === 'ArrowFunctionExpression' ||
|
|
99
|
+
node.arguments[0].type === 'FunctionExpression')) {
|
|
100
|
+
const effectCallback = node.arguments[0];
|
|
101
|
+
// Check if the effect body is a block with a single statement
|
|
102
|
+
if (effectCallback.body.type === 'BlockStatement' &&
|
|
103
|
+
effectCallback.body.body.length === 1) {
|
|
104
|
+
const statement = effectCallback.body.body[0];
|
|
105
|
+
// Check if the statement is a call to a state setter
|
|
106
|
+
if (statement.type === 'ExpressionStatement' &&
|
|
107
|
+
statement.expression.type === 'CallExpression' &&
|
|
108
|
+
statement.expression.callee.type === 'Identifier') {
|
|
109
|
+
const setterName = statement.expression.callee.name;
|
|
110
|
+
const stateInfo = stateSetters.get(setterName);
|
|
111
|
+
if (stateInfo && statement.expression.arguments.length === 1) {
|
|
112
|
+
const computation = statement.expression.arguments[0];
|
|
113
|
+
// Check if the computation is pure
|
|
114
|
+
if (isPureComputation(computation)) {
|
|
115
|
+
// Report the issue but without autofixing
|
|
116
|
+
context.report({
|
|
117
|
+
node,
|
|
118
|
+
messageId: 'preferUseMemo',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=prefer-usememo-over-useeffect-usestate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const preferUtilityFunctionOverPrivateStatic: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"preferUtilityFunctionOverPrivateStatic", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.preferUtilityFunctionOverPrivateStatic = void 0;
|
|
4
|
+
const createRule_1 = require("../utils/createRule");
|
|
5
|
+
exports.preferUtilityFunctionOverPrivateStatic = (0, createRule_1.createRule)({
|
|
6
|
+
name: 'prefer-utility-function-over-private-static',
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforce abstraction of private static methods into utility functions',
|
|
11
|
+
recommended: 'error',
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
preferUtilityFunctionOverPrivateStatic: 'Private static methods should be abstracted into standalone utility functions for better reusability and testability',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
defaultOptions: [],
|
|
19
|
+
create(context) {
|
|
20
|
+
// Helper function to check if a node contains 'this' references
|
|
21
|
+
const hasThisReference = (node) => {
|
|
22
|
+
if (!node)
|
|
23
|
+
return false;
|
|
24
|
+
// If this is a ThisExpression, we found a reference
|
|
25
|
+
if (node.type === 'ThisExpression') {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
// Check all child properties that are objects or arrays
|
|
29
|
+
for (const key in node) {
|
|
30
|
+
const child = node[key];
|
|
31
|
+
// Skip non-object properties and special properties
|
|
32
|
+
if (!child || typeof child !== 'object' || key === 'parent') {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// If it's an array, check each element
|
|
36
|
+
if (Array.isArray(child)) {
|
|
37
|
+
for (const item of child) {
|
|
38
|
+
if (item && typeof item === 'object' && hasThisReference(item)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else if (hasThisReference(child)) {
|
|
44
|
+
// If it's an object, recursively check it
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
'MethodDefinition[static=true][accessibility="private"]'(node) {
|
|
52
|
+
const sourceCode = context.getSourceCode();
|
|
53
|
+
const methodBody = node.value.body;
|
|
54
|
+
if (!methodBody) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Get the method body text to check size
|
|
58
|
+
const bodyText = sourceCode.getText(methodBody);
|
|
59
|
+
const lineCount = bodyText.split('\n').length;
|
|
60
|
+
// Skip small methods (less than 4 lines including braces)
|
|
61
|
+
if (lineCount < 4) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Check if the method uses 'this' keyword by traversing the AST
|
|
65
|
+
const usesThis = hasThisReference(methodBody);
|
|
66
|
+
// If the method doesn't use 'this', it's a good candidate for extraction
|
|
67
|
+
if (!usesThis) {
|
|
68
|
+
context.report({
|
|
69
|
+
node,
|
|
70
|
+
messageId: 'preferUtilityFunctionOverPrivateStatic',
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
//# sourceMappingURL=prefer-utility-function-over-private-static.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const reactUseMemoShouldBeComponent: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"useMemoShouldBeComponent", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
|