@blumintinc/eslint-plugin-blumint 1.19.17 → 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.17',
225
+ version: '1.19.18',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -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.17",
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,18 @@
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
+ },
2
16
  {
3
17
  "version": "1.19.17",
4
18
  "date": "2026-07-18T07:27:36.853Z",