@blumintinc/eslint-plugin-blumint 1.15.0 → 1.16.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 (37) hide show
  1. package/README.md +28 -5
  2. package/lib/index.js +7 -1
  3. package/lib/rules/consistent-callback-naming.js +26 -0
  4. package/lib/rules/dynamic-https-errors.js +5 -26
  5. package/lib/rules/enforce-boolean-naming-prefixes.d.ts +1 -0
  6. package/lib/rules/enforce-boolean-naming-prefixes.js +86 -119
  7. package/lib/rules/enforce-dynamic-imports.d.ts +2 -1
  8. package/lib/rules/enforce-dynamic-imports.js +42 -21
  9. package/lib/rules/enforce-memoize-async.js +1 -4
  10. package/lib/rules/enforce-mui-rounded-icons.js +42 -1
  11. package/lib/rules/enforce-verb-noun-naming.js +3 -0
  12. package/lib/rules/global-const-style.js +9 -0
  13. package/lib/rules/logical-top-to-bottom-grouping.js +80 -2
  14. package/lib/rules/memo-compare-deeply-complex-props.js +167 -3
  15. package/lib/rules/memo-nested-react-components.js +143 -8
  16. package/lib/rules/no-array-length-in-deps.js +74 -3
  17. package/lib/rules/no-circular-references.js +145 -482
  18. package/lib/rules/no-compositing-layer-props.js +31 -0
  19. package/lib/rules/no-entire-object-hook-deps.js +132 -97
  20. package/lib/rules/no-explicit-return-type.js +6 -0
  21. package/lib/rules/no-hungarian.js +119 -24
  22. package/lib/rules/no-margin-properties.js +7 -38
  23. package/lib/rules/no-unnecessary-verb-suffix.js +79 -0
  24. package/lib/rules/no-unused-props.js +215 -37
  25. package/lib/rules/no-useless-fragment.js +10 -2
  26. package/lib/rules/parallelize-async-operations.js +1 -3
  27. package/lib/rules/prefer-type-alias-over-typeof-constant.js +73 -11
  28. package/lib/rules/prefer-use-deep-compare-memo.js +8 -14
  29. package/lib/rules/react-memoize-literals.js +87 -1
  30. package/lib/rules/require-migration-script-metadata.d.ts +9 -0
  31. package/lib/rules/require-migration-script-metadata.js +206 -0
  32. package/lib/rules/warn-https-error-message-user-friendly.d.ts +1 -0
  33. package/lib/rules/warn-https-error-message-user-friendly.js +239 -0
  34. package/lib/utils/ASTHelpers.d.ts +15 -0
  35. package/lib/utils/ASTHelpers.js +48 -0
  36. package/package.json +7 -6
  37. package/release-manifest.json +166 -0
@@ -252,18 +252,134 @@ const getVariableName = (node) => {
252
252
  }
253
253
  return null;
254
254
  };
255
- const isInsideFunction = (node) => {
255
+ const isEnclosingFunction = (node) => {
256
+ return (node.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
257
+ node.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
258
+ node.type === utils_1.AST_NODE_TYPES.FunctionDeclaration);
259
+ };
260
+ /**
261
+ * Nearest ancestor function whose body directly contains `node`. Climbs from
262
+ * the node's parent so a function node itself is not treated as its own
263
+ * enclosing function.
264
+ */
265
+ const findEnclosingFunction = (node) => {
256
266
  let parent = node.parent;
257
267
  while (parent) {
258
- if (parent.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
259
- parent.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
260
- parent.type === utils_1.AST_NODE_TYPES.FunctionDeclaration) {
261
- return true;
268
+ if (isEnclosingFunction(parent)) {
269
+ return parent;
262
270
  }
263
271
  parent = parent.parent;
264
272
  }
273
+ return null;
274
+ };
275
+ const isInsideFunction = (node) => {
276
+ return findEnclosingFunction(node) !== null;
277
+ };
278
+ const isPascalCaseName = (name) => /^[A-Z]/.test(name);
279
+ /**
280
+ * A return value is a *component* (vs. rendered output) when it is a
281
+ * memo()/forwardRef() call, a PascalCase identifier (returned component
282
+ * reference), or an inline function that itself creates a component. Such a
283
+ * value identifies the surrounding function as an HOC factory rather than a
284
+ * render body.
285
+ */
286
+ const returnExpressionIsComponent = (expression, reactImports) => {
287
+ if (!expression) {
288
+ return false;
289
+ }
290
+ const unwrapped = unwrapExpression(expression);
291
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.CallExpression) {
292
+ if (isMemoCall(unwrapped, reactImports) ||
293
+ isForwardRefCall(unwrapped, reactImports)) {
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ if (unwrapped.type === utils_1.AST_NODE_TYPES.Identifier) {
299
+ return isPascalCaseName(unwrapped.name);
300
+ }
301
+ if (isFunctionExpression(unwrapped)) {
302
+ return Boolean(functionCreatesComponent(unwrapped, reactImports));
303
+ }
265
304
  return false;
266
305
  };
306
+ const returnExpressionIsJsx = (expression) => {
307
+ if (!expression) {
308
+ return false;
309
+ }
310
+ const unwrapped = unwrapExpression(expression);
311
+ return (unwrapped.type === utils_1.AST_NODE_TYPES.JSXElement ||
312
+ unwrapped.type === utils_1.AST_NODE_TYPES.JSXFragment);
313
+ };
314
+ /**
315
+ * Direct return-statement arguments of a function, traversing control flow
316
+ * (if/switch/try/loops) but NOT descending into nested function definitions—
317
+ * returns inside nested functions belong to those functions, not this one.
318
+ */
319
+ const collectDirectReturnExpressions = (fn) => {
320
+ if (fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
321
+ return [fn.body];
322
+ }
323
+ const returns = [];
324
+ const visitStatement = (statement) => {
325
+ switch (statement.type) {
326
+ case utils_1.AST_NODE_TYPES.ReturnStatement:
327
+ returns.push(statement.argument);
328
+ break;
329
+ case utils_1.AST_NODE_TYPES.BlockStatement:
330
+ statement.body.forEach(visitStatement);
331
+ break;
332
+ case utils_1.AST_NODE_TYPES.IfStatement:
333
+ visitStatement(statement.consequent);
334
+ if (statement.alternate) {
335
+ visitStatement(statement.alternate);
336
+ }
337
+ break;
338
+ case utils_1.AST_NODE_TYPES.SwitchStatement:
339
+ for (const switchCase of statement.cases) {
340
+ switchCase.consequent.forEach(visitStatement);
341
+ }
342
+ break;
343
+ case utils_1.AST_NODE_TYPES.ForStatement:
344
+ case utils_1.AST_NODE_TYPES.ForInStatement:
345
+ case utils_1.AST_NODE_TYPES.ForOfStatement:
346
+ case utils_1.AST_NODE_TYPES.WhileStatement:
347
+ case utils_1.AST_NODE_TYPES.DoWhileStatement:
348
+ visitStatement(statement.body);
349
+ break;
350
+ case utils_1.AST_NODE_TYPES.TryStatement:
351
+ visitStatement(statement.block);
352
+ if (statement.handler) {
353
+ visitStatement(statement.handler.body);
354
+ }
355
+ if (statement.finalizer) {
356
+ visitStatement(statement.finalizer);
357
+ }
358
+ break;
359
+ default:
360
+ break;
361
+ }
362
+ };
363
+ fn.body.body.forEach(visitStatement);
364
+ return returns;
365
+ };
366
+ /**
367
+ * True when the nearest enclosing function is an HOC factory—it returns a
368
+ * component (memo/forwardRef/component reference) and never returns JSX. Such
369
+ * a function runs once per call, so components defined inside it have stable
370
+ * identities and must NOT be flagged. A function that returns JSX is a render
371
+ * body, where nested components DO remount and remain flagged.
372
+ */
373
+ const isInsideHocFactory = (node, reactImports) => {
374
+ const enclosing = findEnclosingFunction(node);
375
+ if (!enclosing) {
376
+ return false;
377
+ }
378
+ const returns = collectDirectReturnExpressions(enclosing);
379
+ const returnsComponent = returns.some((expression) => returnExpressionIsComponent(expression, reactImports));
380
+ const returnsJsx = returns.some((expression) => returnExpressionIsJsx(expression));
381
+ return returnsComponent && !returnsJsx;
382
+ };
267
383
  exports.memoNestedReactComponents = (0, createRule_1.createRule)({
268
384
  name: 'memo-nested-react-components',
269
385
  meta: {
@@ -343,7 +459,18 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
343
459
  return;
344
460
  }
345
461
  }
346
- const componentName = getVariableName(node) ??
462
+ const variableName = getVariableName(node);
463
+ // A non-PascalCase binding (e.g. renderHit) is a render callback used
464
+ // with a render={...} prop, not a component—skip it.
465
+ if (variableName && !isPascalCaseName(variableName)) {
466
+ return;
467
+ }
468
+ // Inside an HOC factory the binding has a stable identity, so it does
469
+ // not remount on re-render and must not be flagged.
470
+ if (isInsideHocFactory(node, reactImports)) {
471
+ return;
472
+ }
473
+ const componentName = variableName ??
347
474
  (isFunctionExpression(callback) &&
348
475
  callback.id?.type === utils_1.AST_NODE_TYPES.Identifier
349
476
  ? callback.id.name
@@ -356,10 +483,14 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
356
483
  if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier)
357
484
  return;
358
485
  // Only check if name starts with uppercase (convention for components)
359
- if (!/^[A-Z]/.test(node.id.name))
486
+ if (!isPascalCaseName(node.id.name))
360
487
  return;
361
488
  if (!isInsideFunction(node))
362
489
  return;
490
+ // Inside an HOC factory the component has a stable identity (the factory
491
+ // runs once per call), so it does not remount and must not be flagged.
492
+ if (isInsideHocFactory(node, reactImports))
493
+ return;
363
494
  // Skip if it's already a hook call (handled by CallExpression visitor)
364
495
  if (node.init.type === utils_1.AST_NODE_TYPES.CallExpression) {
365
496
  const hook = isHookCall(node.init.callee);
@@ -375,10 +506,14 @@ See: https://react.dev/learn/your-first-component#nesting-and-organizing-compone
375
506
  reportNestedComponentViolation(node, node.id.name, 'a render body');
376
507
  },
377
508
  FunctionDeclaration(node) {
378
- if (!node.id || !/^[A-Z]/.test(node.id.name))
509
+ if (!node.id || !isPascalCaseName(node.id.name))
379
510
  return;
380
511
  if (!isInsideFunction(node))
381
512
  return;
513
+ // Inside an HOC factory the component has a stable identity (the factory
514
+ // runs once per call), so it does not remount and must not be flagged.
515
+ if (isInsideHocFactory(node, reactImports))
516
+ return;
382
517
  const componentMatch = functionCreatesComponent(node, reactImports);
383
518
  if (!componentMatch)
384
519
  return;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noArrayLengthInDeps = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const ASTHelpers_1 = require("../utils/ASTHelpers");
6
7
  // React hooks to check
7
8
  const HOOK_NAMES = new Set(['useEffect', 'useCallback', 'useMemo']);
8
9
  const DEFAULT_HASH_IMPORT = {
@@ -58,7 +59,7 @@ function getLastPropertyName(expr) {
58
59
  return null;
59
60
  }
60
61
  function generateUniqueName(base, taken) {
61
- let candidate = `${base}Hash`;
62
+ const candidate = `${base}Hash`;
62
63
  if (!taken.has(candidate))
63
64
  return candidate;
64
65
  let i = 2;
@@ -120,6 +121,69 @@ function isUseMemoImported(sourceCode) {
120
121
  }
121
122
  return false;
122
123
  }
124
+ /**
125
+ * Hook callback = first function-typed argument (the effect/factory/memo fn).
126
+ * Deps array is the LAST argument; the callback precedes it.
127
+ */
128
+ function getHookCallback(node) {
129
+ for (const arg of node.arguments) {
130
+ if (arg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
131
+ arg.type === utils_1.AST_NODE_TYPES.FunctionExpression) {
132
+ return arg;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ function rangeContains(outer, inner) {
138
+ return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1];
139
+ }
140
+ /**
141
+ * True when `identifier` is the object of a non-computed `.length` access,
142
+ * i.e. the reference reads only the array's length, not its contents.
143
+ */
144
+ function isLengthAccessOf(identifier) {
145
+ const parent = identifier.parent;
146
+ return (!!parent &&
147
+ parent.type === utils_1.AST_NODE_TYPES.MemberExpression &&
148
+ !parent.computed &&
149
+ parent.object === identifier &&
150
+ parent.property.type === utils_1.AST_NODE_TYPES.Identifier &&
151
+ parent.property.name === 'length');
152
+ }
153
+ /**
154
+ * Decide whether a `<array>.length` dependency is safe to keep (suppress the
155
+ * report) by inspecting how the hook callback body uses the array binding.
156
+ *
157
+ * Safe (suppress) only when the array is referenced at least once inside the
158
+ * callback body AND every such reference is the object of a `.length` access —
159
+ * then depending on `.length` correctly avoids reruns on content changes.
160
+ *
161
+ * Returns false (keep reporting) for any non-`.length` use (element access,
162
+ * spread, iteration/method calls, bare reference, passed as an argument), when
163
+ * the array is never referenced in the body, and whenever the base cannot be
164
+ * confidently resolved (complex member-chain bases) — never hide a real bug.
165
+ */
166
+ function isLengthOnlyUsage(context, callback, baseExpr) {
167
+ // Only resolvable for a bare identifier base (e.g. `items` in `items.length`).
168
+ // Member-chain bases (`a.b`, `data?.items`) have no single binding to track.
169
+ if (baseExpr.type !== utils_1.AST_NODE_TYPES.Identifier) {
170
+ return false;
171
+ }
172
+ // Resolve the binding the DEP refers to (from the deps-array identifier's
173
+ // scope), so a binding shadowed inside the callback body is not mistaken for
174
+ // it — the outer binding then has zero body references and we keep reporting.
175
+ const scope = ASTHelpers_1.ASTHelpers.getScope(context, baseExpr);
176
+ const variable = ASTHelpers_1.ASTHelpers.findVariableInScope(scope, baseExpr.name);
177
+ if (!variable) {
178
+ return false;
179
+ }
180
+ const bodyReferences = variable.references.filter((ref) => rangeContains(callback.body, ref.identifier));
181
+ // No body usage → not the content-vs-length bug, but keep current behavior.
182
+ if (bodyReferences.length === 0) {
183
+ return false;
184
+ }
185
+ return bodyReferences.every((ref) => isLengthAccessOf(ref.identifier));
186
+ }
123
187
  function isStableHashImported(sourceCode, hashSource, hashImportName) {
124
188
  const program = sourceCode.ast;
125
189
  for (const node of program.body) {
@@ -188,15 +252,22 @@ exports.noArrayLengthInDeps = (0, createRule_1.createRule)({
188
252
  return;
189
253
  // Collect .length deps
190
254
  const lengthDeps = [];
255
+ const callback = getHookCallback(node);
191
256
  for (const el of depsArg.elements) {
192
257
  if (!el)
193
258
  continue;
194
259
  if (el.type === utils_1.AST_NODE_TYPES.SpreadElement)
195
260
  continue;
196
261
  const member = getLengthMember(el);
197
- if (member) {
198
- lengthDeps.push({ element: el, member });
262
+ if (!member)
263
+ continue;
264
+ // Suppress when the callback body reads only `<array>.length`, never
265
+ // the array's contents — then `.length` is the correct dependency.
266
+ if (callback &&
267
+ isLengthOnlyUsage(context, callback, getBaseExpression(member))) {
268
+ continue;
199
269
  }
270
+ lengthDeps.push({ element: el, member });
200
271
  }
201
272
  if (lengthDeps.length === 0)
202
273
  return;