@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
|
@@ -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({
|
|
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
|
|
1086
|
+
function lateDeclarationDeclaratorsOf(statement) {
|
|
1063
1087
|
const declaration = variableDeclarationOf(statement);
|
|
1064
|
-
if (!declaration || declaration.declarations.length
|
|
1088
|
+
if (!declaration || declaration.declarations.length === 0) {
|
|
1065
1089
|
return null;
|
|
1066
1090
|
}
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
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
|
|
1114
|
-
if (!
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
1769
|
-
* the whole reordering ships as a single fix. Relocating one
|
|
1770
|
-
* satisfies its own adjacency constraint while breaking
|
|
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
|
|
1790
|
-
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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 (
|
|
854
|
-
|
|
855
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|