@blumintinc/eslint-plugin-blumint 1.20.136 → 1.20.137

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.136',
226
+ version: '1.20.137',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -828,12 +828,31 @@ function truncateWithEllipsis(text, max = 60) {
828
828
  * handlers with contradictory targets, so the first handler to claim it wins and
829
829
  * the handler call order in `detectViolations` is the tie-break.
830
830
  */
831
- function record(sink, statement, messageId, data, fromIndex, toIndex) {
831
+ function record(sink, statement, messageId, data, fromIndex, toIndex, relocatable = true) {
832
832
  if (sink.flagged.has(statement)) {
833
833
  return;
834
834
  }
835
835
  sink.flagged.add(statement);
836
- sink.violations.push({ statement, messageId, data, fromIndex, toIndex });
836
+ sink.violations.push({
837
+ statement,
838
+ messageId,
839
+ data,
840
+ fromIndex,
841
+ toIndex,
842
+ relocatable,
843
+ });
844
+ }
845
+ /**
846
+ * The violations the reordering search is allowed to expand into moves, and the
847
+ * only ones its zero-violation goal test answers about.
848
+ *
849
+ * Screening here rather than at detection is what keeps the fix path identical to
850
+ * one that never saw an unrelocatable violation: the search's candidate moves, its
851
+ * budget and the order it certifies clean are all computed as if the report were
852
+ * absent, so adding a report can never redirect, weaken or block a fix.
853
+ */
854
+ function relocatableViolations(violations) {
855
+ return violations.filter((violation) => violation.relocatable);
837
856
  }
838
857
  function isGuardIfStatement(statement) {
839
858
  if (statement.type !== utils_1.AST_NODE_TYPES.IfStatement || statement.alternate) {
@@ -1058,23 +1077,58 @@ function unwrappedInitOf(declarator) {
1058
1077
  *
1059
1078
  * The classification runs on the unwrapped initializer: an assertion is erased
1060
1079
  * before the code runs, so `1 as const` is exactly the movable literal `1` is.
1080
+ *
1081
+ * Every declarator must qualify, because the statement is diagnosed — and moved —
1082
+ * as a whole. A sibling binding does not disqualify the statement: how many
1083
+ * bindings a declaration introduces decides whether the fix may relocate it, not
1084
+ * whether the declaration is far from the first use of what it declares (#1889).
1061
1085
  */
1062
- function lateDeclarationCandidateOf(statement) {
1086
+ function lateDeclarationDeclaratorsOf(statement) {
1063
1087
  const declaration = variableDeclarationOf(statement);
1064
- if (!declaration || declaration.declarations.length !== 1) {
1088
+ if (!declaration || declaration.declarations.length === 0) {
1065
1089
  return null;
1066
1090
  }
1067
- const [declarator] = declaration.declarations;
1068
- if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
1069
- return null;
1091
+ const declarators = [];
1092
+ for (const declarator of declaration.declarations) {
1093
+ if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier) {
1094
+ return null;
1095
+ }
1096
+ const init = unwrappedInitOf(declarator);
1097
+ if (init &&
1098
+ init.type !== utils_1.AST_NODE_TYPES.Identifier &&
1099
+ init.type !== utils_1.AST_NODE_TYPES.Literal) {
1100
+ return null;
1101
+ }
1102
+ declarators.push(declarator);
1070
1103
  }
1071
- const init = unwrappedInitOf(declarator);
1072
- if (init &&
1073
- init.type !== utils_1.AST_NODE_TYPES.Identifier &&
1074
- init.type !== utils_1.AST_NODE_TYPES.Literal) {
1075
- return null;
1104
+ return declarators;
1105
+ }
1106
+ /**
1107
+ * Whether the reordering fix may relocate this statement to satisfy a
1108
+ * late-declaration report.
1109
+ *
1110
+ * The fix moves whole statements, so relocating `const x = 1, y = 2;` carries `y`
1111
+ * along — past its own first use in the general case, and always further than the
1112
+ * report asked for. Splitting the declaration is the developer's call, so a
1113
+ * declaration with sibling bindings is reported and left where it stands.
1114
+ */
1115
+ function isRelocatableLateDeclaration(statement) {
1116
+ return lateDeclarationDeclaratorsOf(statement)?.length === 1;
1117
+ }
1118
+ /**
1119
+ * The binding the report names: the one whose own first use is the group's.
1120
+ *
1121
+ * A statement's bindings share its position, so the declaration is late relative to
1122
+ * whichever of them is read first; naming a sibling read later would point the
1123
+ * reader at the wrong line.
1124
+ */
1125
+ function earliestUsedDeclarator(declarators, body, afterIndex, usageIndex) {
1126
+ if (declarators.length === 1) {
1127
+ return declarators[0];
1076
1128
  }
1077
- return declarator;
1129
+ const earliest = declarators.find((declarator) => findFirstUsageIndex(body, new Set([declarator.id.name]), afterIndex) ===
1130
+ usageIndex);
1131
+ return earliest ?? declarators[0];
1078
1132
  }
1079
1133
  const LOOP_TYPES = new Set([
1080
1134
  utils_1.AST_NODE_TYPES.ForStatement,
@@ -1110,17 +1164,18 @@ function isMutatedInLoop(body, usageIndex, nameSet) {
1110
1164
  }
1111
1165
  function handleLateDeclarations(sink, body) {
1112
1166
  body.forEach((statement, index) => {
1113
- const declarator = lateDeclarationCandidateOf(statement);
1114
- if (!declarator) {
1167
+ const declarators = lateDeclarationDeclaratorsOf(statement);
1168
+ if (!declarators) {
1115
1169
  return;
1116
1170
  }
1117
- const name = declarator.id.name;
1118
1171
  const dependencies = new Set();
1119
- const init = unwrappedInitOf(declarator);
1120
- if (init && init.type === utils_1.AST_NODE_TYPES.Identifier) {
1121
- dependencies.add(init.name);
1122
- }
1123
- const nameSet = new Set([name]);
1172
+ declarators.forEach((declarator) => {
1173
+ const init = unwrappedInitOf(declarator);
1174
+ if (init && init.type === utils_1.AST_NODE_TYPES.Identifier) {
1175
+ dependencies.add(init.name);
1176
+ }
1177
+ });
1178
+ const nameSet = new Set(declarators.map((declarator) => declarator.id.name));
1124
1179
  const usageIndex = findFirstUsageIndex(body, nameSet, index + 1);
1125
1180
  if (usageIndex === -1 || usageIndex <= index + 1) {
1126
1181
  return;
@@ -1140,7 +1195,9 @@ function handleLateDeclarations(sink, body) {
1140
1195
  // Do not hop over another declaration that is used at the same index or earlier
1141
1196
  // if it is also a candidate for being moved.
1142
1197
  // This prevents circular swapping of related declarations (like resolve/reject pairs).
1143
- if (lateDeclarationCandidateOf(stmt)) {
1198
+ // The test asks about *relocation*, so a declaration the fix will never move
1199
+ // is not one of the two ends of such a swap and does not block the hop.
1200
+ if (isRelocatableLateDeclaration(stmt)) {
1144
1201
  const declaredNames = getDeclaredNames(stmt);
1145
1202
  const firstUsageOfIntervening = findFirstUsageIndex(body, declaredNames, index + 1 + i + 1);
1146
1203
  if (firstUsageOfIntervening !== -1 &&
@@ -1163,7 +1220,11 @@ function handleLateDeclarations(sink, body) {
1163
1220
  if (crossesImpureOrTracked) {
1164
1221
  return;
1165
1222
  }
1166
- record(sink, statement, 'moveDeclarationCloser', { name }, index, usageIndex);
1223
+ const subject = earliestUsedDeclarator(declarators, body, index + 1, usageIndex);
1224
+ // The same test `isRelocatableLateDeclaration` applies to an intervening
1225
+ // statement, read off the declarators already in hand.
1226
+ const relocatable = declarators.length === 1;
1227
+ record(sink, statement, 'moveDeclarationCloser', { name: subject.id.name }, index, usageIndex, relocatable);
1167
1228
  });
1168
1229
  }
1169
1230
  /**
@@ -1655,8 +1716,12 @@ function orderKey(order, indices) {
1655
1716
  return order.map((statement) => indices.get(statement)).join(',');
1656
1717
  }
1657
1718
  /**
1658
- * Shortest sequence of moves reaching an order with **zero** violations, or null when
1659
- * the search bounds contain no such order.
1719
+ * Shortest sequence of moves reaching an order with **zero** relocatable violations,
1720
+ * or null when the search bounds contain no such order.
1721
+ *
1722
+ * `violations` is pre-screened by `relocatableViolations`, and so is every detection
1723
+ * the search performs: a violation whose statement the fix may not move is neither a
1724
+ * candidate move nor a reason to reject an order, since no reordering can answer it.
1660
1725
  *
1661
1726
  * Breadth-first for two reasons: the emitted fix is then the smallest reordering that
1662
1727
  * satisfies every constraint, and a block whose single named move already suffices
@@ -1706,7 +1771,7 @@ function findResolvingMoves(sourceCode, body, violations, maxMoves) {
1706
1771
  }
1707
1772
  seen.add(key);
1708
1773
  budget -= 1;
1709
- const next = detectViolations(sourceCode, order);
1774
+ const next = relocatableViolations(detectViolations(sourceCode, order));
1710
1775
  const moves = [...node.moves, { fromIndex, toIndex }];
1711
1776
  if (next.length === 0) {
1712
1777
  return moves;
@@ -1765,10 +1830,15 @@ function buildReorderFix(body, moves, parent, sourceCode, fixer) {
1765
1830
  return fixer.replaceTextRange([bounds[first], bounds[last + 1]], reordered.slice(first, last + 1).join(''));
1766
1831
  }
1767
1832
  /**
1768
- * A fix is emitted only for a reordering the detector scores at zero violations, and
1769
- * the whole reordering ships as a single fix. Relocating one statement per report
1770
- * satisfies its own adjacency constraint while breaking another's, which under
1771
- * `--fix` oscillates or exhausts the pass budget (#1405).
1833
+ * A fix is emitted only for a reordering the detector scores at zero relocatable
1834
+ * violations, and the whole reordering ships as a single fix. Relocating one
1835
+ * statement per report satisfies its own adjacency constraint while breaking
1836
+ * another's, which under `--fix` oscillates or exhausts the pass budget (#1405).
1837
+ *
1838
+ * A violation no reordering may answer — a declaration whose sibling bindings the
1839
+ * fix would drag along — is reported and otherwise invisible here: it does not
1840
+ * carry the fix, does not veto one, and cannot keep the block from settling, since
1841
+ * the fixed text scores it exactly as the input did.
1772
1842
  *
1773
1843
  * Convergence is structural rather than argued: the emitted order is verified clean
1774
1844
  * by the same detector that produced the reports, and detection depends only on
@@ -1786,13 +1856,14 @@ function handleBlock(ruleContext, node) {
1786
1856
  if (violations.length === 0) {
1787
1857
  return;
1788
1858
  }
1789
- const moves = findResolvingMoves(sourceCode, body, violations, searchDepthFor(violations.length, body.length));
1790
- violations.forEach((violation, index) => {
1859
+ const relocatable = relocatableViolations(violations);
1860
+ const moves = findResolvingMoves(sourceCode, body, relocatable, searchDepthFor(relocatable.length, body.length));
1861
+ violations.forEach((violation) => {
1791
1862
  context.report({
1792
1863
  node: violation.statement,
1793
1864
  messageId: violation.messageId,
1794
1865
  data: violation.data,
1795
- fix: moves && index === 0
1866
+ fix: moves && violation === relocatable[0]
1796
1867
  ? (fixer) => buildReorderFix(body, moves, node, sourceCode, fixer)
1797
1868
  : null,
1798
1869
  });
@@ -84,14 +84,14 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
84
84
  const spreadTypeToPropNames = new Map();
85
85
  const processingTypeAliases = new Set();
86
86
  const componentsToCheck = [];
87
- let currentComponent = null;
87
+ let currentComponents = null;
88
88
  const clearState = () => {
89
89
  propsTypes.clear();
90
90
  componentToReferencedSpreadTypeNames.clear();
91
91
  spreadTypeToPropNames.clear();
92
92
  processingTypeAliases.clear();
93
93
  componentsToCheck.length = 0;
94
- currentComponent = null;
94
+ currentComponents = null;
95
95
  };
96
96
  const isGenericTypeSpread = (prop) => prop.startsWith('...') && prop.length === 4 && /^\.\.\.[A-Z]$/.test(prop);
97
97
  const hasRestSpreadUsage = (used, restUsed) => {
@@ -412,6 +412,63 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
412
412
  }
413
413
  });
414
414
  };
415
+ /**
416
+ * The props usage of a single declarator, or `null` when the declarator is
417
+ * not a props-typed arrow component. Each declarator answers on its own:
418
+ * this rule reports a member of a props type and rewrites nothing, so a
419
+ * sibling binding in the same statement has no bearing on the verdict
420
+ * (#1890).
421
+ */
422
+ const componentUsageOfDeclarator = (declaration) => {
423
+ if (declaration.init?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
424
+ return null;
425
+ }
426
+ const fn = declaration.init;
427
+ const param = fn.params[0];
428
+ if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
429
+ // Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
430
+ // as the plain `FooProps` annotation; an unannotated pattern under an
431
+ // FC-annotated declarator resolves from the annotation's argument.
432
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
433
+ propsArgumentOfFcAnnotation(declaration.id));
434
+ if (!typeName) {
435
+ return null;
436
+ }
437
+ const used = new Set();
438
+ const restUsed = collectUsedFromObjectPattern(param, typeName, used);
439
+ return { typeName, used, restUsed };
440
+ }
441
+ if (param?.type === utils_1.AST_NODE_TYPES.Identifier) {
442
+ // Identifier param (`props: FooProps`): the destructuring happens in
443
+ // the body. Resolve the Props type (unwrapping generic wrappers) and
444
+ // scan the body for `const { ... } = props`.
445
+ const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
446
+ propsArgumentOfFcAnnotation(declaration.id));
447
+ if (!typeName) {
448
+ return null;
449
+ }
450
+ const used = new Set();
451
+ const { destructureCount, restUsed, hasOpaqueUsage } = collectBodyDestructuring(fn.body, param.name, typeName, used);
452
+ // Only check when the body destructures the param at least once.
453
+ // Member access (`props.x`), spread (`{...props}`), passing `props`
454
+ // whole, or never referencing it (e.g. `_props`) is left to prior
455
+ // behavior (no report) — those usages can't be enumerated reliably.
456
+ if (destructureCount === 0) {
457
+ return null;
458
+ }
459
+ // Mixed usage (some destructure + opaque consumption) forwards the
460
+ // remaining props, so treat every prop as used to avoid false reports.
461
+ if (hasOpaqueUsage) {
462
+ const propsType = propsTypes.get(typeName);
463
+ if (propsType) {
464
+ Object.keys(propsType).forEach((key) => used.add(key));
465
+ }
466
+ return { typeName, used, restUsed: true };
467
+ }
468
+ return { typeName, used, restUsed };
469
+ }
470
+ return null;
471
+ };
415
472
  return {
416
473
  TSTypeAliasDeclaration(node) {
417
474
  if (!node.id.name.endsWith('Props')) {
@@ -796,64 +853,19 @@ exports.noUnusedProps = (0, createRule_1.createRule)({
796
853
  }
797
854
  },
798
855
  VariableDeclaration(node) {
799
- if (node.declarations.length !== 1) {
800
- return;
801
- }
802
- const declaration = node.declarations[0];
803
- if (declaration.init?.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
804
- return;
805
- }
806
- const fn = declaration.init;
807
- const param = fn.params[0];
808
- if (param?.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
809
- // Resolve through generic wrappers (e.g. `Readonly<FooProps>`) as well
810
- // as the plain `FooProps` annotation; an unannotated pattern under an
811
- // FC-annotated declarator resolves from the annotation's argument.
812
- const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
813
- propsArgumentOfFcAnnotation(declaration.id));
814
- if (typeName) {
815
- const used = new Set();
816
- const restUsed = collectUsedFromObjectPattern(param, typeName, used);
817
- currentComponent = { node, typeName, used, restUsed };
818
- }
819
- return;
820
- }
821
- if (param?.type === utils_1.AST_NODE_TYPES.Identifier) {
822
- // Identifier param (`props: FooProps`): the destructuring happens in
823
- // the body. Resolve the Props type (unwrapping generic wrappers) and
824
- // scan the body for `const { ... } = props`.
825
- const typeName = resolvePropsTypeName(param.typeAnnotation?.typeAnnotation ??
826
- propsArgumentOfFcAnnotation(declaration.id));
827
- if (!typeName) {
828
- return;
829
- }
830
- const used = new Set();
831
- const { destructureCount, restUsed, hasOpaqueUsage } = collectBodyDestructuring(fn.body, param.name, typeName, used);
832
- // Only check when the body destructures the param at least once.
833
- // Member access (`props.x`), spread (`{...props}`), passing `props`
834
- // whole, or never referencing it (e.g. `_props`) is left to prior
835
- // behavior (no report) — those usages can't be enumerated reliably.
836
- if (destructureCount === 0) {
837
- return;
838
- }
839
- // Mixed usage (some destructure + opaque consumption) forwards the
840
- // remaining props, so treat every prop as used to avoid false reports.
841
- if (hasOpaqueUsage) {
842
- const propsType = propsTypes.get(typeName);
843
- if (propsType) {
844
- Object.keys(propsType).forEach((key) => used.add(key));
845
- }
846
- currentComponent = { node, typeName, used, restUsed: true };
847
- return;
848
- }
849
- currentComponent = { node, typeName, used, restUsed };
856
+ const components = node.declarations
857
+ .map((declaration) => componentUsageOfDeclarator(declaration))
858
+ .filter((component) => component !== null);
859
+ // A declaration holding no component leaves any pending one untouched,
860
+ // so an inner non-component declaration cannot drop its enclosing one.
861
+ if (components.length > 0) {
862
+ currentComponents = { node, components };
850
863
  }
851
864
  },
852
865
  'VariableDeclaration:exit'(node) {
853
- if (currentComponent?.node === node) {
854
- const { typeName, used, restUsed } = currentComponent;
855
- componentsToCheck.push({ typeName, used, restUsed });
856
- currentComponent = null;
866
+ if (currentComponents?.node === node) {
867
+ componentsToCheck.push(...currentComponents.components);
868
+ currentComponents = null;
857
869
  }
858
870
  },
859
871
  JSXElement() {
@@ -95,16 +95,32 @@ function isFunctionExpressionLike(node) {
95
95
  node.type === 'FunctionExpression' ||
96
96
  node.type === 'FunctionDeclaration'));
97
97
  }
98
+ // Find the function a declaration contributes to the ordering inventory.
99
+ //
100
+ // A sibling declarator is a FIX-safety question, not a detection one: bailing
101
+ // out of the extractor drops the statement from the inventory entirely, so the
102
+ // ordering violation goes unreported instead of merely unfixed (#1891).
103
+ //
104
+ // At most ONE binding per statement is surfaced — the first function-like
105
+ // declarator — because every downstream structure is keyed on statement
106
+ // identity: the expected-order slots, the comment-inclusive statement ranges,
107
+ // and the fixer's per-statement edits. A second entry sharing a statement would
108
+ // emit two edits over one range and duplicate that statement's text. A later
109
+ // function-like sibling therefore stays invisible, which under-constrains the
110
+ // order rather than misstating it, and no ordering instruction is ever emitted
111
+ // for two functions that cannot move independently of each other.
98
112
  function extractFunctionFromVariableDeclaration(statement) {
99
- if (statement.declarations.length !== 1) {
100
- return null;
101
- }
102
- const declarator = statement.declarations[0];
103
- if (declarator.id.type !== 'Identifier' ||
104
- !isFunctionExpressionLike(declarator.init)) {
105
- return null;
113
+ for (const declarator of statement.declarations) {
114
+ if (declarator.id.type === 'Identifier' &&
115
+ isFunctionExpressionLike(declarator.init)) {
116
+ return {
117
+ name: declarator.id.name,
118
+ fnNode: declarator.init,
119
+ hasSiblingDeclarators: statement.declarations.length > 1,
120
+ };
121
+ }
106
122
  }
107
- return { name: declarator.id.name, fnNode: declarator.init };
123
+ return null;
108
124
  }
109
125
  function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
110
126
  const functions = [];
@@ -124,6 +140,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
124
140
  isUtility: utilityRegex.test(name),
125
141
  dependencies: [],
126
142
  originalIndex: index,
143
+ hasSiblingDeclarators: false,
127
144
  });
128
145
  return;
129
146
  }
@@ -142,13 +159,14 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
142
159
  isUtility: utilityRegex.test(name),
143
160
  dependencies: [],
144
161
  originalIndex: index,
162
+ hasSiblingDeclarators: false,
145
163
  });
146
164
  return;
147
165
  }
148
166
  if (statement.declaration.type === 'VariableDeclaration') {
149
167
  const extracted = extractFunctionFromVariableDeclaration(statement.declaration);
150
168
  if (extracted) {
151
- const { name, fnNode } = extracted;
169
+ const { name, fnNode, hasSiblingDeclarators } = extracted;
152
170
  functions.push({
153
171
  name,
154
172
  fnNode,
@@ -158,6 +176,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
158
176
  isUtility: utilityRegex.test(name),
159
177
  dependencies: [],
160
178
  originalIndex: index,
179
+ hasSiblingDeclarators,
161
180
  });
162
181
  }
163
182
  }
@@ -177,13 +196,14 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
177
196
  isUtility: utilityRegex.test(name),
178
197
  dependencies: [],
179
198
  originalIndex: index,
199
+ hasSiblingDeclarators: false,
180
200
  });
181
201
  return;
182
202
  }
183
203
  if (statement.type === 'VariableDeclaration') {
184
204
  const extracted = extractFunctionFromVariableDeclaration(statement);
185
205
  if (extracted) {
186
- const { name, fnNode } = extracted;
206
+ const { name, fnNode, hasSiblingDeclarators } = extracted;
187
207
  const isExported = ASTHelpers_1.ASTHelpers.isNodeExported(statement);
188
208
  functions.push({
189
209
  name,
@@ -194,6 +214,7 @@ function collectFunctions(programBody, eventHandlerRegex, utilityRegex) {
194
214
  isUtility: utilityRegex.test(name),
195
215
  dependencies: [],
196
216
  originalIndex: index,
217
+ hasSiblingDeclarators,
197
218
  });
198
219
  }
199
220
  }
@@ -686,6 +707,16 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
686
707
  firstFunctionIndex > lastFunctionIndex) {
687
708
  return null;
688
709
  }
710
+ // Both reorder paths rewrite whole statements, so moving a function
711
+ // that shares its declaration with other bindings would carry those
712
+ // siblings with it — or, split apart, would leave the sibling's
713
+ // initializer evaluating somewhere it was never written to. Neither
714
+ // is a rewrite this rule is entitled to make on the reader's behalf.
715
+ // The misorderedFunction report still fires; only the rewrite of the
716
+ // region containing such a declaration is withheld.
717
+ if (functions.some((fn) => fn.hasSiblingDeclarators)) {
718
+ return null;
719
+ }
689
720
  const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
690
721
  const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
691
722
  // Decline a reorder that would hoist a function above an interleaved
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.136",
3
+ "version": "1.20.137",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "version": "1.20.137",
4
+ "date": "2026-08-08T22:49:24.968Z",
5
+ "rules": [
6
+ {
7
+ "name": "logical-top-to-bottom-grouping",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1889
11
+ ],
12
+ "summary": "report a late declaration that carries a sibling binding (closes #1889)"
13
+ },
14
+ {
15
+ "name": "no-unused-props",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1890
19
+ ],
20
+ "summary": "analyse every declarator, not just the first (closes #1890)"
21
+ },
22
+ {
23
+ "name": "vertically-group-related-functions",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1891
27
+ ],
28
+ "summary": "report a misordered function that carries a sibling binding (closes #1891)"
29
+ }
30
+ ]
31
+ },
2
32
  {
3
33
  "version": "1.20.136",
4
34
  "date": "2026-08-08T14:34:06.547Z",