@blumintinc/eslint-plugin-blumint 1.12.6 → 1.13.0

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 (141) hide show
  1. package/README.md +140 -99
  2. package/lib/index.js +111 -17
  3. package/lib/rules/array-methods-this-context.js +20 -2
  4. package/lib/rules/avoid-utils-directory.js +11 -2
  5. package/lib/rules/dynamic-https-errors.d.ts +3 -1
  6. package/lib/rules/dynamic-https-errors.js +48 -10
  7. package/lib/rules/enforce-assert-safe-object-key.d.ts +8 -0
  8. package/lib/rules/{enforce-assertSafe-object-key.js → enforce-assert-safe-object-key.js} +51 -95
  9. package/lib/rules/enforce-assert-throws.d.ts +3 -1
  10. package/lib/rules/enforce-assert-throws.js +493 -51
  11. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +3 -1
  12. package/lib/rules/enforce-boolean-naming-prefixes.js +832 -39
  13. package/lib/rules/enforce-callable-types.js +4 -4
  14. package/lib/rules/enforce-callback-memo.js +143 -18
  15. package/lib/rules/enforce-centralized-mock-firestore.js +4 -1
  16. package/lib/rules/enforce-console-error.d.ts +3 -0
  17. package/lib/rules/enforce-console-error.js +250 -0
  18. package/lib/rules/enforce-css-media-queries.js +24 -46
  19. package/lib/rules/enforce-dynamic-firebase-imports.js +100 -19
  20. package/lib/rules/enforce-dynamic-imports.js +1 -1
  21. package/lib/rules/enforce-exported-function-types.js +3 -3
  22. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.d.ts +2 -0
  23. package/lib/rules/enforce-fieldpath-syntax-in-docsetter.js +212 -0
  24. package/lib/rules/enforce-firestore-doc-ref-generic.js +335 -0
  25. package/lib/rules/enforce-firestore-facade.js +347 -157
  26. package/lib/rules/enforce-firestore-rules-get-access.d.ts +3 -0
  27. package/lib/rules/enforce-firestore-rules-get-access.js +109 -0
  28. package/lib/rules/enforce-global-constants.d.ts +4 -1
  29. package/lib/rules/enforce-global-constants.js +240 -12
  30. package/lib/rules/enforce-memoize-async.d.ts +2 -1
  31. package/lib/rules/enforce-memoize-async.js +96 -27
  32. package/lib/rules/enforce-memoize-getters.d.ts +3 -0
  33. package/lib/rules/enforce-memoize-getters.js +123 -0
  34. package/lib/rules/enforce-microdiff.js +2 -1
  35. package/lib/rules/enforce-positive-naming.js +5 -7
  36. package/lib/rules/enforce-props-argument-name.d.ts +2 -7
  37. package/lib/rules/enforce-props-argument-name.js +165 -151
  38. package/lib/rules/enforce-props-naming-consistency.d.ts +2 -0
  39. package/lib/rules/enforce-props-naming-consistency.js +145 -0
  40. package/lib/rules/enforce-querykey-ts.d.ts +9 -0
  41. package/lib/rules/enforce-querykey-ts.js +274 -0
  42. package/lib/rules/enforce-singular-type-names.js +9 -5
  43. package/lib/rules/enforce-typescript-markdown-code-blocks.d.ts +1 -0
  44. package/lib/rules/enforce-typescript-markdown-code-blocks.js +96 -0
  45. package/lib/rules/enforce-verb-noun-naming.js +85 -10
  46. package/lib/rules/extract-global-constants.js +57 -45
  47. package/lib/rules/fast-deep-equal-over-microdiff.d.ts +2 -1
  48. package/lib/rules/fast-deep-equal-over-microdiff.js +356 -78
  49. package/lib/rules/firestore-transaction-reads-before-writes.d.ts +1 -0
  50. package/lib/rules/firestore-transaction-reads-before-writes.js +231 -0
  51. package/lib/rules/generic-starts-with-t.js +8 -2
  52. package/lib/rules/memoize-root-level-hocs.d.ts +7 -0
  53. package/lib/rules/memoize-root-level-hocs.js +242 -0
  54. package/lib/rules/no-array-length-in-deps.d.ts +12 -0
  55. package/lib/rules/no-array-length-in-deps.js +308 -0
  56. package/lib/rules/no-async-array-filter.js +5 -2
  57. package/lib/rules/no-complex-cloud-params.js +15 -2
  58. package/lib/rules/no-compositing-layer-props.js +4 -2
  59. package/lib/rules/no-entire-object-hook-deps.d.ts +3 -1
  60. package/lib/rules/no-entire-object-hook-deps.js +317 -63
  61. package/lib/rules/no-excessive-parent-chain.d.ts +5 -0
  62. package/lib/rules/no-excessive-parent-chain.js +273 -0
  63. package/lib/rules/no-firestore-object-arrays.js +345 -30
  64. package/lib/rules/no-hungarian.js +13 -13
  65. package/lib/rules/no-margin-properties.d.ts +7 -1
  66. package/lib/rules/no-margin-properties.js +16 -3
  67. package/lib/rules/no-misleading-boolean-prefixes.d.ts +7 -0
  68. package/lib/rules/no-misleading-boolean-prefixes.js +327 -0
  69. package/lib/rules/no-overridable-method-calls-in-constructor.d.ts +1 -0
  70. package/lib/rules/no-overridable-method-calls-in-constructor.js +260 -0
  71. package/lib/rules/no-passthrough-getters.d.ts +4 -0
  72. package/lib/rules/no-passthrough-getters.js +170 -0
  73. package/lib/rules/no-redundant-usecallback-wrapper.d.ts +8 -0
  74. package/lib/rules/no-redundant-usecallback-wrapper.js +315 -0
  75. package/lib/rules/no-restricted-properties-fix.d.ts +12 -0
  76. package/lib/rules/no-restricted-properties-fix.js +172 -0
  77. package/lib/rules/no-separate-loading-state.d.ts +8 -0
  78. package/lib/rules/no-separate-loading-state.js +142 -0
  79. package/lib/rules/no-stale-state-across-await.d.ts +1 -0
  80. package/lib/rules/no-stale-state-across-await.js +185 -0
  81. package/lib/rules/no-static-constants-in-dynamic-files.d.ts +3 -0
  82. package/lib/rules/no-static-constants-in-dynamic-files.js +149 -0
  83. package/lib/rules/no-try-catch-already-exists-in-transaction.d.ts +1 -0
  84. package/lib/rules/no-try-catch-already-exists-in-transaction.js +342 -0
  85. package/lib/rules/no-type-assertion-returns.js +6 -0
  86. package/lib/rules/no-undefined-null-passthrough.js +166 -19
  87. package/lib/rules/no-unnecessary-verb-suffix.js +0 -2
  88. package/lib/rules/no-unsafe-firestore-spread.js +8 -5
  89. package/lib/rules/no-unused-props.d.ts +3 -0
  90. package/lib/rules/no-unused-props.js +213 -78
  91. package/lib/rules/optimize-object-boolean-conditions.d.ts +1 -0
  92. package/lib/rules/optimize-object-boolean-conditions.js +234 -0
  93. package/lib/rules/parallelize-async-operations.d.ts +8 -0
  94. package/lib/rules/parallelize-async-operations.js +375 -0
  95. package/lib/rules/prefer-batch-operations.js +199 -33
  96. package/lib/rules/prefer-block-comments-for-declarations.js +13 -3
  97. package/lib/rules/prefer-document-flattening.d.ts +4 -0
  98. package/lib/rules/prefer-document-flattening.js +239 -0
  99. package/lib/rules/prefer-field-paths-in-transforms.d.ts +7 -0
  100. package/lib/rules/prefer-field-paths-in-transforms.js +250 -0
  101. package/lib/rules/prefer-fragment-component.js +3 -3
  102. package/lib/rules/prefer-global-router-state-key.d.ts +7 -3
  103. package/lib/rules/prefer-global-router-state-key.js +230 -31
  104. package/lib/rules/prefer-next-dynamic.d.ts +9 -0
  105. package/lib/rules/prefer-next-dynamic.js +366 -0
  106. package/lib/rules/prefer-nullish-coalescing-boolean-props.d.ts +1 -0
  107. package/lib/rules/prefer-nullish-coalescing-boolean-props.js +430 -0
  108. package/lib/rules/prefer-nullish-coalescing-override.d.ts +6 -0
  109. package/lib/rules/prefer-nullish-coalescing-override.js +182 -0
  110. package/lib/rules/prefer-params-over-parent-id.d.ts +5 -0
  111. package/lib/rules/prefer-params-over-parent-id.js +228 -0
  112. package/lib/rules/prefer-settings-object.js +23 -15
  113. package/lib/rules/prefer-type-alias-over-typeof-constant.d.ts +3 -0
  114. package/lib/rules/prefer-type-alias-over-typeof-constant.js +259 -0
  115. package/lib/rules/prefer-url-tostring-over-tojson.d.ts +2 -0
  116. package/lib/rules/prefer-url-tostring-over-tojson.js +166 -0
  117. package/lib/rules/prefer-use-deep-compare-memo.d.ts +4 -0
  118. package/lib/rules/prefer-use-deep-compare-memo.js +428 -0
  119. package/lib/rules/prefer-usecallback-over-usememo-for-functions.js +24 -1
  120. package/lib/rules/prefer-usememo-over-useeffect-usestate.js +24 -5
  121. package/lib/rules/react-usememo-should-be-component.d.ts +2 -1
  122. package/lib/rules/react-usememo-should-be-component.js +144 -78
  123. package/lib/rules/require-dynamic-firebase-imports.js +2 -1
  124. package/lib/rules/require-image-optimized.js +1 -1
  125. package/lib/rules/require-memo.js +11 -1
  126. package/lib/rules/require-usememo-object-literals.js +7 -1
  127. package/lib/rules/semantic-function-prefixes.js +38 -18
  128. package/lib/rules/sync-onwrite-name-func.js +5 -1
  129. package/lib/rules/test-file-location-enforcement.d.ts +1 -0
  130. package/lib/rules/test-file-location-enforcement.js +57 -0
  131. package/lib/rules/use-custom-link.js +6 -1
  132. package/lib/rules/use-custom-router.js +6 -1
  133. package/lib/rules/use-latest-callback.d.ts +3 -0
  134. package/lib/rules/use-latest-callback.js +270 -0
  135. package/lib/utils/ASTHelpers.js +10 -0
  136. package/lib/utils/createRule.js +1 -1
  137. package/lib/utils/graph/ClassGraphSorterReadability.js +1 -1
  138. package/lib/utils/ruleTester.d.ts +1 -0
  139. package/lib/utils/ruleTester.js +4 -1
  140. package/package.json +21 -14
  141. package/lib/rules/enforce-assertSafe-object-key.d.ts +0 -2
@@ -3,4 +3,7 @@ export declare const noUnusedProps: import("@typescript-eslint/utils/dist/ts-esl
3
3
  TSTypeAliasDeclaration(node: TSESTree.TSTypeAliasDeclaration): void;
4
4
  VariableDeclaration(node: TSESTree.VariableDeclaration): void;
5
5
  'VariableDeclaration:exit'(node: never): void;
6
+ JSXElement(): void;
7
+ JSXFragment(): void;
8
+ 'Program:exit'(): void;
6
9
  }>;
@@ -19,17 +19,177 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
19
19
  },
20
20
  defaultOptions: [],
21
21
  create(context) {
22
+ const filename = context.filename ??
23
+ context.getFilename();
24
+ const ruleSettings = (context.settings && context.settings['no-unused-props']) ?? {};
25
+ const reactLikeExtensions = (ruleSettings.reactLikeExtensions ??
26
+ ['.tsx']).map((ext) => ext.toLowerCase());
27
+ const fileExtension = filename.includes('.')
28
+ ? filename.slice(filename.lastIndexOf('.')).toLowerCase()
29
+ : '';
30
+ const isTsxFile = reactLikeExtensions.includes(fileExtension);
31
+ let hasJsxInFile = false;
32
+ const shouldCheckFile = () => isTsxFile || hasJsxInFile;
33
+ const UTILITY_TYPES = new Set([
34
+ 'Pick',
35
+ 'Omit',
36
+ 'Partial',
37
+ 'Required',
38
+ 'Record',
39
+ 'Exclude',
40
+ 'Extract',
41
+ 'NonNullable',
42
+ 'ReturnType',
43
+ 'InstanceType',
44
+ 'ThisType',
45
+ ]);
22
46
  const propsTypes = new Map();
23
- const usedProps = new Map();
24
47
  // Track which spread types have been used in a component
25
48
  const usedSpreadTypes = new Map();
49
+ const componentsToCheck = [];
26
50
  let currentComponent = null;
51
+ const clearState = () => {
52
+ propsTypes.clear();
53
+ usedSpreadTypes.clear();
54
+ componentsToCheck.length = 0;
55
+ currentComponent = null;
56
+ };
57
+ const isGenericTypeSpread = (prop) => prop.startsWith('...') && prop.length === 4 && /^\.\.\.[A-Z]$/.test(prop);
58
+ const hasRestSpreadUsage = (used, restUsed) => {
59
+ if (restUsed) {
60
+ return true;
61
+ }
62
+ for (const usedProp of used) {
63
+ if (usedProp.startsWith('...')) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ };
69
+ const isAnyPropFromSpreadTypeUsed = (spreadTypeName, used) => {
70
+ const spreadTypeProps = usedSpreadTypes.get(spreadTypeName);
71
+ if (!spreadTypeProps) {
72
+ return false;
73
+ }
74
+ for (const spreadProp of spreadTypeProps) {
75
+ if (used.has(spreadProp)) {
76
+ return true;
77
+ }
78
+ }
79
+ return false;
80
+ };
81
+ const shouldSkipSpreadType = (prop, hasRestSpread, used) => {
82
+ if (hasRestSpread) {
83
+ return true;
84
+ }
85
+ const spreadTypeName = prop.substring(3);
86
+ if (UTILITY_TYPES.has(spreadTypeName)) {
87
+ return true;
88
+ }
89
+ return isAnyPropFromSpreadTypeUsed(spreadTypeName, used);
90
+ };
91
+ const shouldSkipPropFromSpreadType = (prop, currentTypeName, used) => {
92
+ for (const [spreadType, props] of usedSpreadTypes.entries()) {
93
+ if (spreadType === currentTypeName)
94
+ continue;
95
+ if (!props.has(prop)) {
96
+ continue;
97
+ }
98
+ for (const spreadProp of props) {
99
+ if (used.has(spreadProp)) {
100
+ return true;
101
+ }
102
+ }
103
+ }
104
+ return false;
105
+ };
106
+ const reportUnusedProps = (typeName, used, restUsed) => {
107
+ const propsType = propsTypes.get(typeName);
108
+ if (!propsType) {
109
+ return;
110
+ }
111
+ const hasRestSpread = hasRestSpreadUsage(used, restUsed);
112
+ Object.keys(propsType).forEach((prop) => {
113
+ if (!used.has(prop)) {
114
+ // For imported types (props that start with '...'), only report if there's no rest spread operator
115
+ // This allows imported types to be used without being flagged when properly forwarded
116
+ // Don't report unused props if:
117
+ // 1. It's a spread type and there's a rest spread operator, OR
118
+ // 2. It's a property from a spread type and any property from that spread type is used, OR
119
+ // 3. It's a spread type and any of its properties are used in the component
120
+ let shouldReport = true;
121
+ // Skip reporting for generic type parameters (T, K, etc.)
122
+ if (isGenericTypeSpread(prop)) {
123
+ shouldReport = false;
124
+ }
125
+ else if (prop.startsWith('...')) {
126
+ shouldReport = !shouldSkipSpreadType(prop, hasRestSpread, used);
127
+ }
128
+ else {
129
+ shouldReport = !shouldSkipPropFromSpreadType(prop, typeName, used);
130
+ }
131
+ if (shouldReport) {
132
+ context.report({
133
+ node: propsType[prop],
134
+ messageId: 'unusedProp',
135
+ data: { propName: prop },
136
+ });
137
+ }
138
+ }
139
+ });
140
+ };
27
141
  return {
28
142
  TSTypeAliasDeclaration(node) {
29
143
  if (node.id.name.endsWith('Props')) {
30
144
  const props = {};
31
145
  // Track which properties come from which spread type
32
146
  const spreadTypeProps = {};
147
+ const addBaseTypeProps = (typeNode, shouldInclude = () => true) => {
148
+ if (typeNode.type !== utils_1.AST_NODE_TYPES.TSTypeLiteral) {
149
+ return;
150
+ }
151
+ typeNode.members.forEach((member) => {
152
+ if (member.type === utils_1.AST_NODE_TYPES.TSPropertySignature &&
153
+ member.key.type === utils_1.AST_NODE_TYPES.Identifier &&
154
+ shouldInclude(member.key.name)) {
155
+ props[member.key.name] = member.key;
156
+ }
157
+ });
158
+ };
159
+ const handleOmitType = (baseType, omittedProps) => {
160
+ if (baseType.type !== utils_1.AST_NODE_TYPES.TSTypeReference ||
161
+ baseType.typeName.type !== utils_1.AST_NODE_TYPES.Identifier) {
162
+ return;
163
+ }
164
+ const baseTypeName = baseType.typeName.name;
165
+ const omittedPropNames = new Set();
166
+ if (omittedProps.type === utils_1.AST_NODE_TYPES.TSUnionType) {
167
+ omittedProps.types.forEach((type) => {
168
+ if (type.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
169
+ type.literal.type === utils_1.AST_NODE_TYPES.Literal &&
170
+ typeof type.literal.value === 'string') {
171
+ omittedPropNames.add(type.literal.value);
172
+ }
173
+ });
174
+ }
175
+ else if (omittedProps.type === utils_1.AST_NODE_TYPES.TSLiteralType &&
176
+ omittedProps.literal.type === utils_1.AST_NODE_TYPES.Literal &&
177
+ typeof omittedProps.literal.value === 'string') {
178
+ omittedPropNames.add(omittedProps.literal.value);
179
+ }
180
+ const scope = context.getScope();
181
+ const variable = scope.variables.find((v) => v.name === baseTypeName);
182
+ if (variable &&
183
+ variable.defs[0]?.node.type === utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
184
+ addBaseTypeProps(variable.defs[0].node.typeAnnotation, (name) => !omittedPropNames.has(name));
185
+ }
186
+ else {
187
+ props[`...${baseTypeName}`] = baseType.typeName;
188
+ if (!spreadTypeProps[baseTypeName]) {
189
+ spreadTypeProps[baseTypeName] = [];
190
+ }
191
+ }
192
+ };
33
193
  function extractProps(typeNode) {
34
194
  if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeLiteral) {
35
195
  typeNode.members.forEach((member) => {
@@ -81,6 +241,13 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
81
241
  }
82
242
  }
83
243
  }
244
+ else if (typeName.name === 'Omit' &&
245
+ type.typeParameters &&
246
+ type.typeParameters.params.length === 2) {
247
+ // Handle Omit utility type in intersection
248
+ const [baseType, omittedProps] = type.typeParameters.params;
249
+ handleOmitType(baseType, omittedProps);
250
+ }
84
251
  else {
85
252
  // For referenced types in intersections, we need to find their type declaration
86
253
  const scope = context.getScope();
@@ -111,10 +278,9 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
111
278
  }
112
279
  else if (typeNode.type === utils_1.AST_NODE_TYPES.TSTypeReference) {
113
280
  if (typeNode.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
114
- // List of TypeScript utility types that transform other types
115
- const utilityTypes = ['Pick', 'Omit', 'Partial', 'Required', 'Record', 'Exclude', 'Extract', 'NonNullable', 'ReturnType', 'InstanceType', 'ThisType'];
116
281
  // Skip checking for utility type parameters (T, K, etc.) as they're not actual props
117
- if (typeNode.typeName.name.length === 1 && /^[A-Z]$/.test(typeNode.typeName.name)) {
282
+ if (typeNode.typeName.name.length === 1 &&
283
+ /^[A-Z]$/.test(typeNode.typeName.name)) {
118
284
  // This is likely a generic type parameter (T, K, etc.), not a real type
119
285
  // Skip it to avoid false positives
120
286
  return;
@@ -157,20 +323,37 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
157
323
  }
158
324
  }
159
325
  }
326
+ else if (typeNode.typeName.name === 'Omit' &&
327
+ typeNode.typeParameters &&
328
+ typeNode.typeParameters.params.length === 2) {
329
+ // Handle Omit<T, K> utility type
330
+ const [baseType, omittedProps] = typeNode.typeParameters.params;
331
+ handleOmitType(baseType, omittedProps);
332
+ }
160
333
  else if (
161
334
  // Handle other utility types like Required, Partial, etc.
162
- utilityTypes.includes(typeNode.typeName.name) &&
335
+ UTILITY_TYPES.has(typeNode.typeName.name) &&
163
336
  typeNode.typeParameters) {
164
- // For utility types like Required<T, K>, we need to handle the base type
337
+ // For utility types like Required<T>, Partial<T>, we need to handle the base type
165
338
  const baseType = typeNode.typeParameters.params[0];
166
339
  if (baseType.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
167
340
  baseType.typeName.type === utils_1.AST_NODE_TYPES.Identifier) {
168
- // Mark the base type as used via the utility type
169
341
  const baseTypeName = baseType.typeName.name;
170
- props[`...${baseTypeName}`] = baseType.typeName;
171
- // For utility types, we need to track individual properties that might be used
172
- if (!spreadTypeProps[baseTypeName]) {
173
- spreadTypeProps[baseTypeName] = [];
342
+ // Find the base type definition
343
+ const scope = context.getScope();
344
+ const variable = scope.variables.find((v) => v.name === baseTypeName);
345
+ if (variable &&
346
+ variable.defs[0]?.node.type ===
347
+ utils_1.AST_NODE_TYPES.TSTypeAliasDeclaration) {
348
+ // For Partial<T>, Required<T>, etc., add all properties from the base type
349
+ addBaseTypeProps(variable.defs[0].node.typeAnnotation);
350
+ }
351
+ else {
352
+ // If we can't find the base type definition, treat it as a spread type
353
+ props[`...${baseTypeName}`] = baseType.typeName;
354
+ if (!spreadTypeProps[baseTypeName]) {
355
+ spreadTypeProps[baseTypeName] = [];
356
+ }
174
357
  }
175
358
  }
176
359
  }
@@ -216,8 +399,8 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
216
399
  utils_1.AST_NODE_TYPES.Identifier) {
217
400
  const typeName = param.typeAnnotation.typeAnnotation.typeName.name;
218
401
  if (typeName.endsWith('Props')) {
219
- currentComponent = { node, typeName };
220
402
  const used = new Set();
403
+ let restUsed = false;
221
404
  param.properties.forEach((prop) => {
222
405
  if (prop.type === utils_1.AST_NODE_TYPES.Property &&
223
406
  prop.key.type === utils_1.AST_NODE_TYPES.Identifier) {
@@ -227,6 +410,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
227
410
  prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
228
411
  // Handle rest spread operator {...rest}
229
412
  // When a rest operator is used, all remaining props are considered used
413
+ restUsed = true;
230
414
  const propsType = propsTypes.get(typeName);
231
415
  if (propsType) {
232
416
  Object.keys(propsType).forEach((key) => {
@@ -237,7 +421,7 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
237
421
  }
238
422
  }
239
423
  });
240
- usedProps.set(typeName, used);
424
+ currentComponent = { node, typeName, used, restUsed };
241
425
  }
242
426
  }
243
427
  }
@@ -245,74 +429,25 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
245
429
  },
246
430
  'VariableDeclaration:exit'(node) {
247
431
  if (currentComponent?.node === node) {
248
- const { typeName } = currentComponent;
249
- const propsType = propsTypes.get(typeName);
250
- const used = usedProps.get(typeName);
251
- if (propsType && used) {
252
- Object.keys(propsType).forEach((prop) => {
253
- if (!used.has(prop)) {
254
- // For imported types (props that start with '...'), only report if there's no rest spread operator
255
- // This allows imported types to be used without being flagged when properly forwarded
256
- const hasRestSpread = Array.from(used.values()).some((usedProp) => usedProp.startsWith('...'));
257
- // Don't report unused props if:
258
- // 1. It's a spread type and there's a rest spread operator, OR
259
- // 2. It's a property from a spread type and any property from that spread type is used, OR
260
- // 3. It's a spread type and any of its properties are used in the component
261
- let shouldReport = true;
262
- // Skip reporting for generic type parameters (T, K, etc.)
263
- if (prop.startsWith('...') && prop.length === 4 && /^\.\.\.([A-Z])$/.test(prop)) {
264
- // This is a generic type parameter like ...T, ...K, etc.
265
- shouldReport = false;
266
- }
267
- else if (prop.startsWith('...') && hasRestSpread) {
268
- shouldReport = false;
269
- }
270
- else if (prop.startsWith('...')) {
271
- // For spread types like "...GroupInfoBasic", check if any properties from this type are used
272
- const spreadTypeName = prop.substring(3); // Remove the "..." prefix
273
- // Get the properties that belong to this spread type
274
- const spreadTypeProps = usedSpreadTypes.get(spreadTypeName);
275
- if (spreadTypeProps) {
276
- // Check if any property from this spread type is being used in the component
277
- const anyPropFromSpreadTypeUsed = Array.from(spreadTypeProps).some((spreadProp) => used.has(spreadProp));
278
- if (anyPropFromSpreadTypeUsed) {
279
- shouldReport = false;
280
- }
281
- }
282
- }
283
- else {
284
- // Check if this prop might be from a spread type that has other properties being used
285
- for (const [spreadType, props] of usedSpreadTypes.entries()) {
286
- // Skip the current props type
287
- if (spreadType === typeName)
288
- continue;
289
- // If this prop is from a spread type
290
- if (props.has(prop)) {
291
- // Check if any other prop from this spread type is being used
292
- const anyPropFromSpreadTypeUsed = Array.from(props).some((spreadProp) => used.has(spreadProp));
293
- if (anyPropFromSpreadTypeUsed) {
294
- shouldReport = false;
295
- break;
296
- }
297
- }
298
- }
299
- }
300
- if (shouldReport) {
301
- context.report({
302
- node: propsType[prop],
303
- messageId: 'unusedProp',
304
- data: { propName: prop },
305
- });
306
- }
307
- }
308
- });
309
- }
310
- // Reset state for this component
311
- propsTypes.delete(typeName);
312
- usedProps.delete(typeName);
432
+ const { typeName, used, restUsed } = currentComponent;
433
+ componentsToCheck.push({ typeName, used, restUsed });
313
434
  currentComponent = null;
314
435
  }
315
436
  },
437
+ JSXElement() {
438
+ hasJsxInFile = true;
439
+ },
440
+ JSXFragment() {
441
+ hasJsxInFile = true;
442
+ },
443
+ 'Program:exit'() {
444
+ if (!shouldCheckFile()) {
445
+ clearState();
446
+ return;
447
+ }
448
+ componentsToCheck.forEach(({ typeName, used, restUsed }) => reportUnusedProps(typeName, used, restUsed));
449
+ clearState();
450
+ },
316
451
  };
317
452
  },
318
453
  });
@@ -0,0 +1 @@
1
+ export declare const optimizeObjectBooleanConditions: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"extractBooleanCondition", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.optimizeObjectBooleanConditions = void 0;
4
+ const utils_1 = require("@typescript-eslint/utils");
5
+ const createRule_1 = require("../utils/createRule");
6
+ const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
7
+ function isHookCall(node) {
8
+ const callee = node.callee;
9
+ return ((callee.type === utils_1.AST_NODE_TYPES.Identifier && HOOK_NAMES.has(callee.name)) ||
10
+ (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
11
+ !callee.computed &&
12
+ callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
13
+ callee.object.name === 'React' &&
14
+ callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
15
+ HOOK_NAMES.has(callee.property.name)));
16
+ }
17
+ function generateBooleanVariableName(objectName, conditionType, isNegated) {
18
+ const base = capitalize(objectName);
19
+ if (conditionType === 'existence') {
20
+ return isNegated ? `is${base}Missing` : `has${base}`;
21
+ }
22
+ if (conditionType === 'keyCount') {
23
+ return isNegated ? `is${base}Empty` : `has${base}`;
24
+ }
25
+ // complex: default to hasX to avoid over-guessing semantics
26
+ return `has${base}`;
27
+ }
28
+ function capitalize(str) {
29
+ return str.charAt(0).toUpperCase() + str.slice(1);
30
+ }
31
+ function isObjectExistenceCheck(node) {
32
+ // Check for patterns like !obj
33
+ if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
34
+ // !obj
35
+ if (node.operator === '!' && node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
36
+ return true;
37
+ }
38
+ // !!obj
39
+ if (node.operator === '!' &&
40
+ node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
41
+ node.argument.operator === '!' &&
42
+ node.argument.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
43
+ return true;
44
+ }
45
+ }
46
+ return false;
47
+ }
48
+ function isObjectKeyCountCheck(node) {
49
+ // Check for patterns like Object.keys(obj).length === 0 or Object.keys(obj).length > 0
50
+ if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
51
+ const { left, operator, right } = node;
52
+ // Check if left side is Object.keys(obj).length
53
+ if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
54
+ left.property.type === utils_1.AST_NODE_TYPES.Identifier &&
55
+ left.property.name === 'length' &&
56
+ left.object.type === utils_1.AST_NODE_TYPES.CallExpression) {
57
+ const callExpr = left.object;
58
+ if (callExpr.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
59
+ callExpr.callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
60
+ callExpr.callee.object.name === 'Object' &&
61
+ callExpr.callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
62
+ callExpr.callee.property.name === 'keys' &&
63
+ callExpr.arguments.length === 1 &&
64
+ callExpr.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
65
+ // Check if right side is a number
66
+ if (right.type === utils_1.AST_NODE_TYPES.Literal &&
67
+ typeof right.value === 'number' &&
68
+ ['===', '!==', '>', '<', '>=', '<='].includes(operator)) {
69
+ return true;
70
+ }
71
+ }
72
+ }
73
+ }
74
+ return false;
75
+ }
76
+ function isComplexBooleanExpression(node) {
77
+ // Check for complex expressions involving objects
78
+ if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
79
+ return (containsObjectCondition(node.left) || containsObjectCondition(node.right));
80
+ }
81
+ return false;
82
+ }
83
+ function containsObjectCondition(node) {
84
+ return (isObjectExistenceCheck(node) ||
85
+ isObjectKeyCountCheck(node) ||
86
+ isComplexBooleanExpression(node));
87
+ }
88
+ function extractObjectName(node) {
89
+ if (node.type === utils_1.AST_NODE_TYPES.UnaryExpression) {
90
+ if (node.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
91
+ return node.argument.name;
92
+ }
93
+ }
94
+ else if (node.type === utils_1.AST_NODE_TYPES.BinaryExpression) {
95
+ const { left } = node;
96
+ if (left.type === utils_1.AST_NODE_TYPES.MemberExpression &&
97
+ left.object.type === utils_1.AST_NODE_TYPES.CallExpression &&
98
+ left.object.callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
99
+ left.object.arguments.length === 1 &&
100
+ left.object.arguments[0].type === utils_1.AST_NODE_TYPES.Identifier) {
101
+ return left.object.arguments[0].name;
102
+ }
103
+ }
104
+ else if (node.type === utils_1.AST_NODE_TYPES.LogicalExpression) {
105
+ // For complex expressions, try to extract from the first object condition found
106
+ const leftObj = extractObjectName(node.left);
107
+ if (leftObj)
108
+ return leftObj;
109
+ return extractObjectName(node.right);
110
+ }
111
+ return null;
112
+ }
113
+ function analyzeBooleanCondition(node, context) {
114
+ const sourceCode = context.getSourceCode();
115
+ const expression = sourceCode.getText(node);
116
+ if (isObjectExistenceCheck(node)) {
117
+ const objectName = extractObjectName(node);
118
+ if (objectName) {
119
+ const isNegated = node.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
120
+ node.operator === '!' &&
121
+ !(node.argument.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
122
+ node.argument.operator === '!');
123
+ return {
124
+ type: 'existence',
125
+ objectName,
126
+ expression,
127
+ suggestedName: generateBooleanVariableName(objectName, 'existence', isNegated),
128
+ node,
129
+ };
130
+ }
131
+ }
132
+ else if (isObjectKeyCountCheck(node)) {
133
+ const objectName = extractObjectName(node);
134
+ if (objectName) {
135
+ const isNegated = expression.includes('=== 0') || expression.includes('<= 0');
136
+ return {
137
+ type: 'keyCount',
138
+ objectName,
139
+ expression,
140
+ suggestedName: generateBooleanVariableName(objectName, 'keyCount', isNegated),
141
+ node,
142
+ };
143
+ }
144
+ }
145
+ else if (isComplexBooleanExpression(node)) {
146
+ const objectName = extractObjectName(node);
147
+ if (objectName) {
148
+ return {
149
+ type: 'complex',
150
+ objectName,
151
+ expression,
152
+ suggestedName: generateBooleanVariableName(objectName, 'complex', false),
153
+ node,
154
+ };
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ function findBooleanConditionsInDependencies(depsArray, context) {
160
+ const patterns = [];
161
+ for (const element of depsArray.elements) {
162
+ if (!element)
163
+ continue; // Skip holes in array
164
+ const pattern = analyzeBooleanCondition(element, context);
165
+ if (pattern) {
166
+ patterns.push(pattern);
167
+ }
168
+ }
169
+ return patterns;
170
+ }
171
+ function isAlreadyBooleanVariable(node) {
172
+ // Check if the dependency is already a boolean variable (starts with is, has, can, etc.)
173
+ if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
174
+ const name = node.name;
175
+ const booleanPrefixes = [
176
+ 'is',
177
+ 'has',
178
+ 'can',
179
+ 'should',
180
+ 'will',
181
+ 'was',
182
+ 'were',
183
+ ];
184
+ return booleanPrefixes.some((prefix) => name.toLowerCase().startsWith(prefix.toLowerCase()));
185
+ }
186
+ return false;
187
+ }
188
+ exports.optimizeObjectBooleanConditions = (0, createRule_1.createRule)({
189
+ name: 'optimize-object-boolean-conditions',
190
+ meta: {
191
+ type: 'suggestion',
192
+ docs: {
193
+ description: 'Detects and suggests optimizations for boolean conditions formed over objects in React hook dependencies. Suggests extracting boolean conditions into separate variables to reduce unnecessary re-computations when objects change frequently but the boolean condition changes less frequently.',
194
+ recommended: 'error',
195
+ },
196
+ schema: [],
197
+ messages: {
198
+ extractBooleanCondition: 'Extract boolean condition "{{expression}}" into a separate variable "{{suggestedName}}" to optimize hook re-runs. The condition depends on object "{{objectName}}" which may change frequently, but the boolean result changes less often.',
199
+ },
200
+ },
201
+ defaultOptions: [],
202
+ create(context) {
203
+ return {
204
+ CallExpression(node) {
205
+ if (!isHookCall(node)) {
206
+ return;
207
+ }
208
+ // Get the dependency array argument (last argument for hooks)
209
+ const depsArg = node.arguments[node.arguments.length - 1];
210
+ if (!depsArg || depsArg.type !== utils_1.AST_NODE_TYPES.ArrayExpression) {
211
+ return;
212
+ }
213
+ // Find boolean conditions in dependencies
214
+ const patterns = findBooleanConditionsInDependencies(depsArg, context);
215
+ for (const pattern of patterns) {
216
+ // Skip if it's already a boolean variable
217
+ if (isAlreadyBooleanVariable(pattern.node)) {
218
+ continue;
219
+ }
220
+ context.report({
221
+ node: pattern.node,
222
+ messageId: 'extractBooleanCondition',
223
+ data: {
224
+ expression: pattern.expression,
225
+ suggestedName: pattern.suggestedName,
226
+ objectName: pattern.objectName,
227
+ },
228
+ });
229
+ }
230
+ },
231
+ };
232
+ },
233
+ });
234
+ //# sourceMappingURL=optimize-object-boolean-conditions.js.map
@@ -0,0 +1,8 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ type Options = [
3
+ {
4
+ sideEffectPatterns?: Array<string | RegExp>;
5
+ }
6
+ ];
7
+ export declare const parallelizeAsyncOperations: TSESLint.RuleModule<"parallelizeAsyncOperations", Options, TSESLint.RuleListener>;
8
+ export {};