@blumintinc/eslint-plugin-blumint 1.20.135 → 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 +1 -1
- package/lib/rules/consistent-callback-naming.js +16 -0
- package/lib/rules/enforce-assert-safe-object-key.js +72 -9
- package/lib/rules/enforce-props-argument-name.js +28 -2
- package/lib/rules/enforce-props-naming-consistency.js +28 -2
- package/lib/rules/logical-top-to-bottom-grouping.js +104 -33
- package/lib/rules/no-redundant-annotation-assertion.js +611 -38
- package/lib/rules/no-unnecessary-verb-suffix.js +46 -4
- package/lib/rules/no-unused-props.js +69 -57
- package/lib/rules/vertically-group-related-functions.js +41 -10
- package/package.json +1 -1
- package/release-manifest.json +87 -0
package/lib/index.js
CHANGED
|
@@ -607,11 +607,27 @@ module.exports = (0, createRule_1.createRule)({
|
|
|
607
607
|
globalVar.references.forEach((ref) => references.add(ref));
|
|
608
608
|
}
|
|
609
609
|
}
|
|
610
|
+
// A binding that leaves the module is one end of a cross-file
|
|
611
|
+
// contract. Renaming `export const handleClick` to `click` strands
|
|
612
|
+
// every `import { handleClick }` with TS2724, and a single-file fixer
|
|
613
|
+
// cannot reach those importers — the same reasoning that already
|
|
614
|
+
// withholds the JSX prop rename and the destructured one, which is
|
|
615
|
+
// where `isExportedBinding` was first needed. The violation still
|
|
616
|
+
// reports; only the rename is withheld.
|
|
617
|
+
const declaredVariable = context
|
|
618
|
+
.getDeclaredVariables(node)
|
|
619
|
+
.find((v) => v.identifiers.includes(node.id));
|
|
620
|
+
const leavesModule = declaredVariable
|
|
621
|
+
? isExportedBinding(declaredVariable)
|
|
622
|
+
: isExportedDeclaration(node);
|
|
610
623
|
context.report({
|
|
611
624
|
node,
|
|
612
625
|
messageId: 'callbackFunctionPrefix',
|
|
613
626
|
data: { functionName },
|
|
614
627
|
fix(fixer) {
|
|
628
|
+
if (leavesModule) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
615
631
|
// Remove 'handle' prefix and convert first character to lowercase
|
|
616
632
|
const newName = stripHandlePrefix(functionName);
|
|
617
633
|
// `const handleDelete = fn` would become `const delete = fn`,
|
|
@@ -256,6 +256,54 @@ function isNumericCall(node) {
|
|
|
256
256
|
callee.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
257
257
|
callee.object.name === 'Math');
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The property names `assertSafe` exists to reject. A key that provably cannot
|
|
261
|
+
* spell one of these cannot reach the prototype surface, which is the entire
|
|
262
|
+
* hazard — so proving it is what earns an exemption, the same standard the
|
|
263
|
+
* numeric analysis already meets.
|
|
264
|
+
*/
|
|
265
|
+
const PROTOTYPE_REACHING_KEYS = ['__proto__', 'constructor', 'prototype'];
|
|
266
|
+
/**
|
|
267
|
+
* Whether a template's FIXED text still leaves room to spell `target`.
|
|
268
|
+
*
|
|
269
|
+
* The producible set is `q0 + * + q1 + * + … + * + qN`, each `*` an arbitrary
|
|
270
|
+
* substitution. `target` is producible iff it starts with `q0`, ends with `qN`,
|
|
271
|
+
* and the interior quasis occur in order in between without overlapping. So
|
|
272
|
+
* `` `user-${id}` `` can never be `__proto__` (no such prefix) while
|
|
273
|
+
* `` `__pro${x}` `` can — with `x` = `'to__'`, which resolves to
|
|
274
|
+
* `Object.prototype` at runtime.
|
|
275
|
+
*
|
|
276
|
+
* Interior quasis are matched greedily from the left. That is sufficient
|
|
277
|
+
* because they are fixed strings: taking the earliest occurrence never consumes
|
|
278
|
+
* a character a later quasi needed, so no backtracking can succeed where the
|
|
279
|
+
* greedy pass fails.
|
|
280
|
+
*
|
|
281
|
+
* A template with no substitutions produces exactly one string and is a static
|
|
282
|
+
* key like any other string literal, so it is never treated as reaching.
|
|
283
|
+
*/
|
|
284
|
+
function templateCanSpell(quasis, target) {
|
|
285
|
+
if (quasis.length < 2) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
const first = quasis[0];
|
|
289
|
+
const last = quasis[quasis.length - 1];
|
|
290
|
+
if (!target.startsWith(first) || !target.endsWith(last)) {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
const limit = target.length - last.length;
|
|
294
|
+
let cursor = first.length;
|
|
295
|
+
if (cursor > limit) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
for (const middle of quasis.slice(1, -1)) {
|
|
299
|
+
const at = target.indexOf(middle, cursor);
|
|
300
|
+
if (at < 0 || at + middle.length > limit) {
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
cursor = at + middle.length;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
259
307
|
/**
|
|
260
308
|
* A `: number` annotation on a binding name. Parameters and variable
|
|
261
309
|
* declarators are the bindings that carry one, and TypeScript checks every
|
|
@@ -1133,19 +1181,34 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1133
1181
|
if (isLikelyArray) {
|
|
1134
1182
|
return;
|
|
1135
1183
|
}
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1184
|
+
// A template whose every substitution is provably numeric can only
|
|
1185
|
+
// widen into digits, and no dangerous property name is the string
|
|
1186
|
+
// form of a number — the same proof the identifier path accepts.
|
|
1187
|
+
const canWidenToText = property.expressions.some((expr) => !isStaticallyNumeric(expr));
|
|
1188
|
+
const quasis = property.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw);
|
|
1189
|
+
const reachesPrototype = canWidenToText &&
|
|
1190
|
+
PROTOTYPE_REACHING_KEYS.some((key) => templateCanSpell(quasis, key));
|
|
1191
|
+
// Fixed text on either side of the substitution can rule a property
|
|
1192
|
+
// name out — `user-${id}` is never `__proto__` — and the rule skips
|
|
1193
|
+
// a key it can prove harmless. What it must NOT do is assume that:
|
|
1194
|
+
// `__pro${x}` carries fixed text too and still reaches the
|
|
1195
|
+
// prototype (#1880).
|
|
1196
|
+
if (!reachesPrototype) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
// `${id}` alone is the whole key, so the remedy names the inner
|
|
1200
|
+
// expression and the fix wraps it directly. A template carrying
|
|
1201
|
+
// fixed text has no such inner key — the string it builds is the
|
|
1202
|
+
// key — so that whole template is what gets wrapped, which is the
|
|
1203
|
+
// shape the docs show for `assertSafe(`${id}_suffix`)`.
|
|
1138
1204
|
const isSimpleVarInterpolation = property.expressions.length === 1 &&
|
|
1139
1205
|
property.quasis.length === 2 &&
|
|
1140
1206
|
property.quasis[0].value.raw === '' &&
|
|
1141
1207
|
property.quasis[1].value.raw === '';
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
const expr = property.expressions[0];
|
|
1147
|
-
const exprText = context.sourceCode.getText(expr);
|
|
1148
|
-
reportWrittenKey(written, property, exprText);
|
|
1208
|
+
const unwrapped = isSimpleVarInterpolation
|
|
1209
|
+
? property.expressions[0]
|
|
1210
|
+
: property;
|
|
1211
|
+
reportWrittenKey(written, property, context.sourceCode.getText(unwrapped));
|
|
1149
1212
|
return;
|
|
1150
1213
|
}
|
|
1151
1214
|
// Check for direct variable usage (identifiers)
|
|
@@ -340,6 +340,33 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
340
340
|
}
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* The member name a `this.<x>` access reads, whatever its spelling.
|
|
345
|
+
*
|
|
346
|
+
* Keying the check on the dot spelling alone left `this['settings']`
|
|
347
|
+
* invisible, so the rename shipped and stranded it — the class no longer had
|
|
348
|
+
* the member the getter reads (#1881). A computed access with a static
|
|
349
|
+
* string is the SAME member as the dot form, and the fixer cannot rewrite it
|
|
350
|
+
* either, so it has to count. `null` marks a genuinely dynamic key, which
|
|
351
|
+
* names no member statically.
|
|
352
|
+
*/
|
|
353
|
+
function staticMemberName(node) {
|
|
354
|
+
if (!node.computed) {
|
|
355
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
356
|
+
? node.property.name
|
|
357
|
+
: null;
|
|
358
|
+
}
|
|
359
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
360
|
+
typeof node.property.value === 'string') {
|
|
361
|
+
return node.property.value;
|
|
362
|
+
}
|
|
363
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
364
|
+
node.property.expressions.length === 0 &&
|
|
365
|
+
node.property.quasis.length === 1) {
|
|
366
|
+
return node.property.quasis[0].value.cooked;
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
343
370
|
// Determine whether renaming a constructor parameter property is unsafe to
|
|
344
371
|
// autofix. A parameter property (`private readonly foo: T`) creates BOTH a
|
|
345
372
|
// constructor-local binding and a `this.foo` class field, so a
|
|
@@ -355,8 +382,7 @@ exports.enforcePropsArgumentName = (0, createRule_1.createRule)({
|
|
|
355
382
|
}
|
|
356
383
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
357
384
|
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
358
|
-
node
|
|
359
|
-
node.property.name === name) {
|
|
385
|
+
staticMemberName(node) === name) {
|
|
360
386
|
unsafe = true;
|
|
361
387
|
return;
|
|
362
388
|
}
|
|
@@ -42,6 +42,33 @@ const getEnclosingClass = (node) => {
|
|
|
42
42
|
* cannot be resolved through scope analysis, the fix is withheld whenever the
|
|
43
43
|
* name occurs anywhere in the class other than at its declaration.
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* The member name a `this.<x>` access reads, whatever its spelling.
|
|
47
|
+
*
|
|
48
|
+
* Keying the check on the dot spelling alone left `this['settings']` invisible,
|
|
49
|
+
* so the rename shipped and stranded it — the class no longer had the member the
|
|
50
|
+
* getter reads (#1882, the sibling of #1881). A computed access with a static
|
|
51
|
+
* string is the SAME member as the dot form, and the fixer cannot rewrite it
|
|
52
|
+
* either, so it has to count. `null` marks a genuinely dynamic key, which names
|
|
53
|
+
* no member statically and therefore strands nothing.
|
|
54
|
+
*/
|
|
55
|
+
const staticMemberName = (node) => {
|
|
56
|
+
if (!node.computed) {
|
|
57
|
+
return node.property.type === utils_1.AST_NODE_TYPES.Identifier
|
|
58
|
+
? node.property.name
|
|
59
|
+
: null;
|
|
60
|
+
}
|
|
61
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
62
|
+
typeof node.property.value === 'string') {
|
|
63
|
+
return node.property.value;
|
|
64
|
+
}
|
|
65
|
+
if (node.property.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
66
|
+
node.property.expressions.length === 0 &&
|
|
67
|
+
node.property.quasis.length === 1) {
|
|
68
|
+
return node.property.quasis[0].value.cooked;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
45
72
|
const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
|
|
46
73
|
let unsafe = false;
|
|
47
74
|
const visit = (node) => {
|
|
@@ -50,8 +77,7 @@ const parameterPropertyRenameIsUnsafe = (classNode, name, declarationId) => {
|
|
|
50
77
|
}
|
|
51
78
|
if (node.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
52
79
|
node.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
53
|
-
node
|
|
54
|
-
node.property.name === name) {
|
|
80
|
+
staticMemberName(node) === name) {
|
|
55
81
|
unsafe = true;
|
|
56
82
|
return;
|
|
57
83
|
}
|
|
@@ -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
|
});
|