@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.
Files changed (70) hide show
  1. package/README.md +83 -61
  2. package/lib/index.js +85 -1
  3. package/lib/rules/enforce-assert-throws.js +48 -3
  4. package/lib/rules/enforce-assertSafe-object-key.d.ts +2 -0
  5. package/lib/rules/enforce-assertSafe-object-key.js +284 -0
  6. package/lib/rules/enforce-centralized-mock-firestore.js +192 -14
  7. package/lib/rules/enforce-css-media-queries.d.ts +5 -0
  8. package/lib/rules/enforce-css-media-queries.js +87 -0
  9. package/lib/rules/enforce-dynamic-imports.d.ts +9 -0
  10. package/lib/rules/enforce-dynamic-imports.js +84 -0
  11. package/lib/rules/enforce-firestore-facade.js +8 -0
  12. package/lib/rules/enforce-id-capitalization.d.ts +6 -0
  13. package/lib/rules/enforce-id-capitalization.js +78 -0
  14. package/lib/rules/enforce-microdiff.d.ts +3 -0
  15. package/lib/rules/enforce-microdiff.js +379 -0
  16. package/lib/rules/enforce-mui-rounded-icons.d.ts +1 -0
  17. package/lib/rules/enforce-mui-rounded-icons.js +54 -0
  18. package/lib/rules/enforce-object-literal-as-const.d.ts +4 -0
  19. package/lib/rules/enforce-object-literal-as-const.js +88 -0
  20. package/lib/rules/enforce-positive-naming.d.ts +1 -0
  21. package/lib/rules/enforce-positive-naming.js +387 -0
  22. package/lib/rules/enforce-props-argument-name.d.ts +8 -0
  23. package/lib/rules/enforce-props-argument-name.js +182 -0
  24. package/lib/rules/enforce-react-type-naming.d.ts +3 -0
  25. package/lib/rules/enforce-react-type-naming.js +191 -0
  26. package/lib/rules/enforce-render-hits-memoization.js +155 -26
  27. package/lib/rules/enforce-singular-type-names.d.ts +2 -0
  28. package/lib/rules/enforce-singular-type-names.js +112 -0
  29. package/lib/rules/enforce-timestamp-now.d.ts +1 -0
  30. package/lib/rules/enforce-timestamp-now.js +223 -0
  31. package/lib/rules/enforce-verb-noun-naming.js +5 -2
  32. package/lib/rules/ensure-pointer-events-none.d.ts +1 -0
  33. package/lib/rules/ensure-pointer-events-none.js +211 -0
  34. package/lib/rules/extract-global-constants.d.ts +1 -1
  35. package/lib/rules/extract-global-constants.js +109 -1
  36. package/lib/rules/global-const-style.js +3 -2
  37. package/lib/rules/key-only-outermost-element.d.ts +3 -0
  38. package/lib/rules/key-only-outermost-element.js +152 -0
  39. package/lib/rules/no-always-true-false-conditions.d.ts +3 -0
  40. package/lib/rules/no-always-true-false-conditions.js +1221 -0
  41. package/lib/rules/no-circular-references.d.ts +1 -0
  42. package/lib/rules/no-circular-references.js +523 -0
  43. package/lib/rules/no-firestore-jest-mock.js +27 -4
  44. package/lib/rules/no-hungarian.d.ts +5 -0
  45. package/lib/rules/no-hungarian.js +223 -0
  46. package/lib/rules/no-mock-firebase-admin.js +20 -7
  47. package/lib/rules/no-object-values-on-strings.d.ts +2 -0
  48. package/lib/rules/no-object-values-on-strings.js +294 -0
  49. package/lib/rules/no-type-assertion-returns.d.ts +9 -0
  50. package/lib/rules/no-type-assertion-returns.js +299 -0
  51. package/lib/rules/no-unnecessary-destructuring.d.ts +5 -0
  52. package/lib/rules/no-unnecessary-destructuring.js +69 -0
  53. package/lib/rules/no-unnecessary-verb-suffix.d.ts +1 -0
  54. package/lib/rules/no-unnecessary-verb-suffix.js +203 -0
  55. package/lib/rules/no-unused-props.js +10 -5
  56. package/lib/rules/no-unused-usestate.d.ts +8 -0
  57. package/lib/rules/no-unused-usestate.js +84 -0
  58. package/lib/rules/omit-index-html.d.ts +7 -0
  59. package/lib/rules/omit-index-html.js +91 -0
  60. package/lib/rules/prefer-clone-deep.js +294 -22
  61. package/lib/rules/prefer-fragment-component.js +265 -34
  62. package/lib/rules/prefer-global-router-state-key.d.ts +5 -0
  63. package/lib/rules/prefer-global-router-state-key.js +89 -0
  64. package/lib/rules/prefer-usememo-over-useeffect-usestate.d.ts +5 -0
  65. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +129 -0
  66. package/lib/rules/prefer-utility-function-over-private-static.d.ts +1 -0
  67. package/lib/rules/prefer-utility-function-over-private-static.js +77 -0
  68. package/lib/rules/react-usememo-should-be-component.d.ts +1 -0
  69. package/lib/rules/react-usememo-should-be-component.js +623 -0
  70. 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
- function getReactImportNode() {
27
- const program = sourceCode.ast;
28
- for (const node of program.body) {
29
- if (node.type === utils_1.AST_NODE_TYPES.ImportDeclaration &&
30
- node.source.value === 'react') {
31
- return node;
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
- reactImportNode = node;
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 (reactImportNode) {
50
- // Add Fragment to existing react import
51
- const lastSpecifier = reactImportNode.specifiers[reactImportNode.specifiers.length - 1];
52
- return fixer.insertTextAfter(lastSpecifier, ', Fragment');
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
- // Add new react import with Fragment
55
- return fixer.insertTextBefore(sourceCode.ast.body[0], 'import { Fragment } from \'react\';\n');
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
- *fix(fixer) {
203
+ fix(fixer) {
204
+ const fixes = [];
205
+ // Add Fragment import if needed
65
206
  if (!hasFragmentImport) {
66
- yield addFragmentImport(fixer);
207
+ fixes.push(addFragmentImport(fixer));
208
+ hasFragmentImport = true;
67
209
  }
68
- yield fixer.replaceText(node.openingFragment, '<Fragment>');
69
- yield fixer.replaceText(node.closingFragment, '</Fragment>');
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
- JSXIdentifier(node) {
74
- if (node.name === 'Fragment' &&
75
- node.parent?.type === utils_1.AST_NODE_TYPES.JSXMemberExpression &&
76
- node.parent.object.type === utils_1.AST_NODE_TYPES.JSXIdentifier &&
77
- node.parent.object.name === 'React') {
78
- const memberExpr = node.parent;
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: memberExpr,
246
+ node: node.name,
81
247
  messageId: 'preferFragment',
82
248
  data: { type: 'React.Fragment' },
83
- *fix(fixer) {
249
+ fix(fixer) {
250
+ const fixes = [];
251
+ // Add Fragment import if needed
84
252
  if (!hasFragmentImport) {
85
- yield addFragmentImport(fixer);
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
- yield fixer.replaceText(memberExpr, 'Fragment');
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
- // If we found any violations but no Fragment import, we need to add it
94
- if (!hasFragmentImport && !reactImportNode) {
95
- reactImportNode = getReactImportNode();
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>;