@blumintinc/eslint-plugin-blumint 1.19.16 → 1.19.18

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/lib/index.js CHANGED
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.16',
225
+ version: '1.19.18',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -138,6 +138,37 @@ function isIterationMethodCallback(node) {
138
138
  callee.property.type === utils_1.AST_NODE_TYPES.Identifier &&
139
139
  ITERATION_METHODS.has(callee.property.name));
140
140
  }
141
+ /**
142
+ * True when the NEAREST enclosing function of `node` is itself an Array
143
+ * iteration-method callback (`arr.map((x) => ({ … }))`, `.filter`, `.reduce`,
144
+ * `.forEach`, `.sort`, ...). This extends the `isIterationMethodCallback`
145
+ * exemption (issue #1290) from the callback function itself down to the
146
+ * object/array literals created directly inside its body (issue #1319): such a
147
+ * literal is a per-iteration value discarded along with the mapped result, so
148
+ * its identity is never observed. The rule's own remediation is unfollowable at
149
+ * the literal — `useMemo` cannot run per-iteration inside a `.map` loop, and a
150
+ * literal closing over the callback parameter cannot be hoisted to module
151
+ * scope. The memoizable unit is the whole `.map()` call:
152
+ * `const tabs = useMemo(() => arr.map((x) => ({ … })), [arr])`.
153
+ *
154
+ * The walk starts at `node` and stops at the FIRST function encountered
155
+ * (including `node` itself when `node` is a function). Keying on the nearest
156
+ * function — not any ancestor — is what preserves the #1290 scope guard: a
157
+ * nested, non-iteration callback inside the map body (e.g. an `onClick` handler
158
+ * that persists as a JSX prop) is the nearest function for any literal in its
159
+ * body, so those literals stay flagged, and the handler itself (its own nearest
160
+ * function is itself) stays flagged too.
161
+ */
162
+ function isInsideIterationMethodCallback(node) {
163
+ let current = node;
164
+ while (current) {
165
+ if (isFunctionNode(current)) {
166
+ return isIterationMethodCallback(current);
167
+ }
168
+ current = current.parent;
169
+ }
170
+ return false;
171
+ }
141
172
  /**
142
173
  * Type guard for parenthesized expressions to unwrap safely.
143
174
  * @param node Node to evaluate.
@@ -838,7 +869,15 @@ exports.reactMemoizeLiterals = (0, createRule_1.createRule)({
838
869
  // to module level, and useCallback can't run inside a .map loop. Inline
839
870
  // functions passed as JSX-attribute props *inside* the callback body are
840
871
  // separate nodes and remain flagged.
841
- if (isIterationMethodCallback(node)) {
872
+ //
873
+ // The same rationale exempts object/array literals created directly
874
+ // inside such a callback (issue #1319): they are per-iteration values
875
+ // discarded with the mapped result, `useMemo` cannot run inside the loop,
876
+ // and the memoizable unit is the enclosing `.map()` call. The guard keys
877
+ // on the NEAREST enclosing function, so a literal inside a nested,
878
+ // non-iteration callback (e.g. an `onClick` handler) is NOT exempted.
879
+ if (isIterationMethodCallback(node) ||
880
+ isInsideIterationMethodCallback(node)) {
842
881
  return;
843
882
  }
844
883
  // Inline literals that resolve to a style JSX attribute (sx, style),
@@ -348,6 +348,179 @@ function getStatementRangeWithComments(statement, sourceCode, consumedComments,
348
348
  trailingCandidates.forEach((comment) => consumedComments?.add(comment));
349
349
  return [start, end];
350
350
  }
351
+ // Collect every identifier a statement references anywhere in its subtree,
352
+ // including type-annotation positions (e.g. `WidgetProps` inside
353
+ // `FC<WidgetProps>`) and default-parameter values (e.g. `DEFAULT_LABEL` in
354
+ // `{ label = DEFAULT_LABEL }`). Identifiers that are member-access properties or
355
+ // object/type member keys are skipped: `obj.OFFSET` is not a reference to a
356
+ // top-level `OFFSET` binding, so counting it would spuriously constrain the
357
+ // reorder.
358
+ function collectReferencedNames(node) {
359
+ const names = new Set();
360
+ const visit = (current, parent) => {
361
+ if (!current || !ASTHelpers_1.ASTHelpers.isNode(current)) {
362
+ return;
363
+ }
364
+ if (current.type === 'Identifier') {
365
+ const isMemberProperty = parent?.type === 'MemberExpression' &&
366
+ parent.property === current &&
367
+ !parent.computed;
368
+ const isObjectKey = parent?.type === 'Property' &&
369
+ parent.key === current &&
370
+ !parent.computed;
371
+ const isMemberKey = (parent?.type === 'TSPropertySignature' ||
372
+ parent?.type === 'TSMethodSignature') &&
373
+ parent.key === current &&
374
+ !parent.computed;
375
+ if (!isMemberProperty && !isObjectKey && !isMemberKey) {
376
+ names.add(current.name);
377
+ }
378
+ }
379
+ Object.values(current).forEach((value) => {
380
+ if (!value || value === current || current.parent === value) {
381
+ return;
382
+ }
383
+ if (Array.isArray(value)) {
384
+ value.forEach((child) => {
385
+ if (ASTHelpers_1.ASTHelpers.isNode(child)) {
386
+ visit(child, current);
387
+ }
388
+ });
389
+ }
390
+ else if (ASTHelpers_1.ASTHelpers.isNode(value)) {
391
+ visit(value, current);
392
+ }
393
+ });
394
+ };
395
+ visit(node, null);
396
+ return names;
397
+ }
398
+ // Names bound by an interleaved VALUE declaration (const/let/var) — but not by a
399
+ // reorderable function. Hoisting a function above a value binding it references
400
+ // is a genuine runtime declare-before-use. Type aliases are tracked separately
401
+ // (interleavedTypeAliasNamesOf), because their hoist hazard is not runtime — it
402
+ // is the companion prefer-type-alias-over-typeof-constant rule, which fires only
403
+ // for a const declarator whose own type annotation names a later alias.
404
+ function interleavedValueNamesOf(regionStatements, functionStatements) {
405
+ const names = new Set();
406
+ regionStatements.forEach((statement) => {
407
+ if (functionStatements.has(statement)) {
408
+ return;
409
+ }
410
+ if (statement.type === 'VariableDeclaration') {
411
+ statement.declarations.forEach((declarator) => {
412
+ if (declarator.id.type === 'Identifier') {
413
+ names.add(declarator.id.name);
414
+ }
415
+ });
416
+ }
417
+ });
418
+ return names;
419
+ }
420
+ // Names bound by an interleaved `type` alias in the reordered region.
421
+ function interleavedTypeAliasNamesOf(regionStatements, functionStatements) {
422
+ const names = new Set();
423
+ regionStatements.forEach((statement) => {
424
+ if (functionStatements.has(statement)) {
425
+ return;
426
+ }
427
+ if (statement.type === 'TSTypeAliasDeclaration') {
428
+ names.add(statement.id.name);
429
+ }
430
+ else if (statement.type === 'ExportNamedDeclaration' &&
431
+ statement.declaration?.type === 'TSTypeAliasDeclaration') {
432
+ names.add(statement.declaration.id.name);
433
+ }
434
+ });
435
+ return names;
436
+ }
437
+ // Type names referenced in the annotation of a function-holding const's own
438
+ // binding identifier (e.g. `WidgetProps` in `const W: FC<WidgetProps> = ...`).
439
+ // This mirrors exactly what prefer-type-alias-over-typeof-constant's
440
+ // defineTypeBeforeConstant keys on: only a const declarator's `id` annotation.
441
+ // A function *declaration*'s return type (`function f(): Marker`) and an arrow
442
+ // parameter's annotation (`const f = (p: Props) => ...`) are deliberately NOT
443
+ // counted — neither trips the companion rule, so constraining them would decline
444
+ // safe reorders.
445
+ function constAnnotationTypeNamesOf(statementNode) {
446
+ const names = new Set();
447
+ const declaration = statementNode.type === 'ExportNamedDeclaration'
448
+ ? statementNode.declaration
449
+ : statementNode;
450
+ if (declaration?.type === 'VariableDeclaration') {
451
+ declaration.declarations.forEach((declarator) => {
452
+ if (declarator.id.type === 'Identifier' && declarator.id.typeAnnotation) {
453
+ collectReferencedNames(declarator.id.typeAnnotation).forEach((name) => names.add(name));
454
+ }
455
+ });
456
+ }
457
+ return names;
458
+ }
459
+ // Decline (return true) any reorder that would place a function ABOVE an
460
+ // interleaved declaration it depends on. The fixer pins interleaved statements in
461
+ // place and only swaps functions between their own slots, so such a reorder
462
+ // produces a fresh declare-before-use — trading one violation for another instead
463
+ // of converging. Two hazards are guarded:
464
+ // * value binding (const/let/var): a runtime declare-before-use if a function
465
+ // references it and is hoisted above it.
466
+ // * `type` alias: hoisting a function-holding const whose OWN binding
467
+ // annotation names the alias above that alias trips the companion
468
+ // prefer-type-alias-over-typeof-constant rule (defineTypeBeforeConstant).
469
+ // Walks the post-reorder sequence front-to-back: each function slot is filled
470
+ // with the next expected function; interleaved statements keep their slot. A
471
+ // function that references a not-yet-declared interleaved dependency is being
472
+ // hoisted above it.
473
+ function reorderHoistsFunctionAboveDependency(regionStatements, functionStatements, expectedOrderInfos) {
474
+ const interleavedValueNames = interleavedValueNamesOf(regionStatements, functionStatements);
475
+ const interleavedTypeNames = interleavedTypeAliasNamesOf(regionStatements, functionStatements);
476
+ if (interleavedValueNames.size === 0 && interleavedTypeNames.size === 0) {
477
+ return false;
478
+ }
479
+ const declaredValueNames = new Set();
480
+ const declaredTypeNames = new Set();
481
+ let slotCursor = 0;
482
+ for (const statement of regionStatements) {
483
+ if (functionStatements.has(statement)) {
484
+ const occupant = expectedOrderInfos[slotCursor];
485
+ slotCursor += 1;
486
+ if (!occupant) {
487
+ continue;
488
+ }
489
+ if (interleavedValueNames.size > 0) {
490
+ const referenced = collectReferencedNames(occupant.statementNode);
491
+ for (const name of referenced) {
492
+ if (interleavedValueNames.has(name) &&
493
+ !declaredValueNames.has(name)) {
494
+ return true;
495
+ }
496
+ }
497
+ }
498
+ if (interleavedTypeNames.size > 0) {
499
+ const annotationTypes = constAnnotationTypeNamesOf(occupant.statementNode);
500
+ for (const name of annotationTypes) {
501
+ if (interleavedTypeNames.has(name) && !declaredTypeNames.has(name)) {
502
+ return true;
503
+ }
504
+ }
505
+ }
506
+ }
507
+ else if (statement.type === 'VariableDeclaration') {
508
+ statement.declarations.forEach((declarator) => {
509
+ if (declarator.id.type === 'Identifier') {
510
+ declaredValueNames.add(declarator.id.name);
511
+ }
512
+ });
513
+ }
514
+ else if (statement.type === 'TSTypeAliasDeclaration') {
515
+ declaredTypeNames.add(statement.id.name);
516
+ }
517
+ else if (statement.type === 'ExportNamedDeclaration' &&
518
+ statement.declaration?.type === 'TSTypeAliasDeclaration') {
519
+ declaredTypeNames.add(statement.declaration.id.name);
520
+ }
521
+ }
522
+ return false;
523
+ }
351
524
  exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
352
525
  name: 'vertically-group-related-functions',
353
526
  meta: {
@@ -500,6 +673,16 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
500
673
  }
501
674
  const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
502
675
  const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
676
+ // Decline a reorder that would hoist a function above an interleaved
677
+ // declaration it depends on (a value binding it references, or the
678
+ // `type` alias named in its own const annotation). Applies to both
679
+ // paths, though it can only trigger in the interleaved-statement
680
+ // branch below (Path A reorders a block with no interleaved
681
+ // declarations). The misorderedFunction report still fires; only the
682
+ // harmful, non-converging autofix is suppressed.
683
+ if (reorderHoistsFunctionAboveDependency(slice, functionStatements, expectedOrderInfos)) {
684
+ return null;
685
+ }
503
686
  if (!blockContainsOnlyFunctions) {
504
687
  // Real modules interleave type aliases, consts, and top-level
505
688
  // calls (e.g. `void autoRunIfMain();`) between functions. Rather
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.16",
3
+ "version": "1.19.18",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.19.18",
4
+ "date": "2026-07-18T13:43:27.635Z",
5
+ "rules": [
6
+ {
7
+ "name": "vertically-group-related-functions",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1320
11
+ ],
12
+ "summary": "decline reorder that hoists a function above its interleaved type/const dependency (closes #1320)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.17",
18
+ "date": "2026-07-18T07:27:36.853Z",
19
+ "rules": [
20
+ {
21
+ "name": "react-memoize-literals",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1319
25
+ ],
26
+ "summary": "exempt literals inside iteration-method callbacks (closes #1319)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.19.16",
4
32
  "date": "2026-07-18T06:29:03.568Z",