@blumintinc/eslint-plugin-blumint 1.18.10 → 1.18.12
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
|
@@ -170,6 +170,42 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
170
170
|
* unrelated managers, but errs on the side of safety.
|
|
171
171
|
*/
|
|
172
172
|
const COORDINATOR_PATTERN = /batch|manager|collector|transaction|tx|coordinator|unitofwork|accumulator|aggregator/i;
|
|
173
|
+
/**
|
|
174
|
+
* Matches guard/assertion callees by their leading verb. Anchored at the
|
|
175
|
+
* start so it fires on the callee's own verb (assertStartable,
|
|
176
|
+
* validateInput, ensureExists, requireAuth, checkAccess, verifyOwnership,
|
|
177
|
+
* guardAgainstX) rather than on an arbitrary substring elsewhere in the
|
|
178
|
+
* name.
|
|
179
|
+
*/
|
|
180
|
+
const GUARD_PATTERN = /^(assert|ensure|require|validate|verify|guard|check)/i;
|
|
181
|
+
/**
|
|
182
|
+
* Extracts the callee's method name (the identifier bearing the leading
|
|
183
|
+
* verb) from an await expression argument. Handles both direct
|
|
184
|
+
* CallExpressions and optional-call ChainExpressions, and both bare
|
|
185
|
+
* identifier callees and member-expression callees.
|
|
186
|
+
*/
|
|
187
|
+
function getCalleeMethodName(awaitExpr) {
|
|
188
|
+
let callExpr = null;
|
|
189
|
+
if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
190
|
+
callExpr = awaitExpr.argument;
|
|
191
|
+
}
|
|
192
|
+
else if (awaitExpr.argument.type === utils_1.AST_NODE_TYPES.ChainExpression &&
|
|
193
|
+
awaitExpr.argument.expression.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
194
|
+
callExpr = awaitExpr.argument.expression;
|
|
195
|
+
}
|
|
196
|
+
if (!callExpr) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const callee = callExpr.callee;
|
|
200
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
201
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
202
|
+
return callee.property.name;
|
|
203
|
+
}
|
|
204
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
205
|
+
return callee.name;
|
|
206
|
+
}
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
173
209
|
/**
|
|
174
210
|
* Checks if there are dependencies between await expressions
|
|
175
211
|
*/
|
|
@@ -214,28 +250,34 @@ exports.parallelizeAsyncOperations = (0, createRule_1.createRule)({
|
|
|
214
250
|
}
|
|
215
251
|
// 3. Check for operations that might have side effects
|
|
216
252
|
// If any node has a side effect, we should not parallelize the sequence
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
253
|
+
const methodName = getCalleeMethodName(awaitExpr);
|
|
254
|
+
if (methodName &&
|
|
255
|
+
sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
256
|
+
return true;
|
|
220
257
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
258
|
+
}
|
|
259
|
+
// 4. Guard-then-side-effect ordering barrier. A discarded-result await
|
|
260
|
+
// whose callee reads as a guard/assertion (assert*, ensure*, validate*,
|
|
261
|
+
// ...) is a control-flow gate: it throws to abort the run when its
|
|
262
|
+
// precondition fails, so any await after it must run ONLY if the guard
|
|
263
|
+
// resolves. Promise.all invokes every operand eagerly, so it would fire
|
|
264
|
+
// the gated side effect even when the guard rejects. Treat the guard's
|
|
265
|
+
// presence (when something follows it) as a sequencing dependency that
|
|
266
|
+
// blocks parallelizing the whole run. Only discarded-result awaits
|
|
267
|
+
// (ExpressionStatements) qualify; `const ok = await validate(x)` has a
|
|
268
|
+
// variable and is handled by the data-dependency path above.
|
|
269
|
+
for (let i = 0; i < awaitNodes.length - 1; i++) {
|
|
270
|
+
const node = awaitNodes[i];
|
|
271
|
+
if (node.type !== utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
|
272
|
+
continue;
|
|
224
273
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
else if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
233
|
-
methodName = callee.name;
|
|
234
|
-
}
|
|
235
|
-
if (methodName &&
|
|
236
|
-
sideEffectPatterns.some((pattern) => pattern.test(methodName))) {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
274
|
+
const awaitExpr = getAwaitExpression(node);
|
|
275
|
+
if (!awaitExpr) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const methodName = getCalleeMethodName(awaitExpr);
|
|
279
|
+
if (methodName && GUARD_PATTERN.test(methodName)) {
|
|
280
|
+
return true;
|
|
239
281
|
}
|
|
240
282
|
}
|
|
241
283
|
// If any node is a variable declaration with destructuring, consider it as having dependencies
|
|
@@ -53,6 +53,8 @@ function collectReferencedIdentifiers(node) {
|
|
|
53
53
|
// Record parameter names as locals
|
|
54
54
|
for (const param of fn.params) {
|
|
55
55
|
collectPatternNames(param, locals);
|
|
56
|
+
// Default values inside a parameter pattern may reference module scope
|
|
57
|
+
walkPatternDefaults(param);
|
|
56
58
|
}
|
|
57
59
|
walk(fn.body);
|
|
58
60
|
break;
|
|
@@ -60,6 +62,10 @@ function collectReferencedIdentifiers(node) {
|
|
|
60
62
|
case utils_1.AST_NODE_TYPES.VariableDeclarator: {
|
|
61
63
|
const decl = n;
|
|
62
64
|
collectPatternNames(decl.id, locals);
|
|
65
|
+
// Default values inside a destructuring pattern may reference module
|
|
66
|
+
// scope (e.g. `const { runner = runCli } = props`) — the binding names
|
|
67
|
+
// are locals, but the default expressions are real references.
|
|
68
|
+
walkPatternDefaults(decl.id);
|
|
63
69
|
walk(decl.init);
|
|
64
70
|
break;
|
|
65
71
|
}
|
|
@@ -83,6 +89,40 @@ function collectReferencedIdentifiers(node) {
|
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
}
|
|
92
|
+
// Walks only the right-hand sides of AssignmentPattern defaults nested inside
|
|
93
|
+
// a binding pattern. The binding names themselves are locals; their defaults
|
|
94
|
+
// are ordinary references that `walk` (which skips patterns via the dedicated
|
|
95
|
+
// VariableDeclarator/function cases) would otherwise never visit.
|
|
96
|
+
function walkPatternDefaults(p) {
|
|
97
|
+
if (!p)
|
|
98
|
+
return;
|
|
99
|
+
switch (p.type) {
|
|
100
|
+
case utils_1.AST_NODE_TYPES.AssignmentPattern:
|
|
101
|
+
walk(p.right);
|
|
102
|
+
walkPatternDefaults(p.left);
|
|
103
|
+
break;
|
|
104
|
+
case utils_1.AST_NODE_TYPES.ArrayPattern:
|
|
105
|
+
for (const el of p.elements) {
|
|
106
|
+
walkPatternDefaults(el);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
case utils_1.AST_NODE_TYPES.ObjectPattern:
|
|
110
|
+
for (const prop of p.properties) {
|
|
111
|
+
if (prop.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
112
|
+
walkPatternDefaults(prop.argument);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
walkPatternDefaults(prop.value);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
case utils_1.AST_NODE_TYPES.RestElement:
|
|
120
|
+
walkPatternDefaults(p.argument);
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
86
126
|
walk(node);
|
|
87
127
|
// Remove purely local names from the reference set
|
|
88
128
|
for (const local of locals) {
|
|
@@ -219,6 +259,38 @@ function isExemptFile(filename) {
|
|
|
219
259
|
return true;
|
|
220
260
|
return false;
|
|
221
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Returns true if the module top-level self-invokes one of its own functions,
|
|
264
|
+
* e.g. `void autoRunIfMain();` or `main();`. This is the signature of a CLI
|
|
265
|
+
* entry-point module whose colocated helpers ARE its purpose, not foreign
|
|
266
|
+
* utilities that should be extracted.
|
|
267
|
+
*/
|
|
268
|
+
function hasTopLevelSelfInvocation(program, topLevelFunctionNames) {
|
|
269
|
+
for (const statement of program.body) {
|
|
270
|
+
if (statement.type !== utils_1.AST_NODE_TYPES.ExpressionStatement)
|
|
271
|
+
continue;
|
|
272
|
+
// Unwrap `void expr`, `await expr`, and parenthesized wrappers to reach the
|
|
273
|
+
// underlying call (e.g. `void autoRunIfMain();`).
|
|
274
|
+
let expr = statement.expression;
|
|
275
|
+
while (expr &&
|
|
276
|
+
((expr.type === utils_1.AST_NODE_TYPES.UnaryExpression &&
|
|
277
|
+
expr.operator === 'void') ||
|
|
278
|
+
expr.type === utils_1.AST_NODE_TYPES.AwaitExpression ||
|
|
279
|
+
expr.type === 'ParenthesizedExpression')) {
|
|
280
|
+
expr =
|
|
281
|
+
expr.argument ??
|
|
282
|
+
expr.expression;
|
|
283
|
+
}
|
|
284
|
+
if (!expr || expr.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
285
|
+
continue;
|
|
286
|
+
const callee = expr.callee;
|
|
287
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
288
|
+
topLevelFunctionNames.has(callee.name)) {
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
222
294
|
exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
223
295
|
name: 'prefer-utility-function-own-file',
|
|
224
296
|
meta: {
|
|
@@ -265,6 +337,12 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
265
337
|
const basename = fileBasename(filename);
|
|
266
338
|
const topLevelFunctions = [];
|
|
267
339
|
let hasExportDefault = false;
|
|
340
|
+
// Names of every top-level binding (functions, consts, lets, destructured
|
|
341
|
+
// bindings). Used by the closure exemption: a function that references any
|
|
342
|
+
// of these closes over module scope, so extraction would sever that link.
|
|
343
|
+
const topLevelBindingNames = new Set();
|
|
344
|
+
// Whether the module references `require.main` — a CLI entry-point signal.
|
|
345
|
+
let referencesRequireMain = false;
|
|
268
346
|
// Names that appear as the handler inside `export default someWrapper(name)`
|
|
269
347
|
const wrappedDefaultHandlerNames = new Set();
|
|
270
348
|
// Names directly exported as default (export default myFunc style via ExportDefaultDeclaration referencing an identifier)
|
|
@@ -272,6 +350,16 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
272
350
|
// Names exported via `export { foo, bar }` specifiers
|
|
273
351
|
const specifierExportedNames = new Set();
|
|
274
352
|
return {
|
|
353
|
+
// Detect `require.main` references (CLI entry-point signal)
|
|
354
|
+
MemberExpression(node) {
|
|
355
|
+
if (!node.computed &&
|
|
356
|
+
node.object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
357
|
+
node.object.name === 'require' &&
|
|
358
|
+
node.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
359
|
+
node.property.name === 'main') {
|
|
360
|
+
referencesRequireMain = true;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
275
363
|
// Collect export default declarations
|
|
276
364
|
ExportDefaultDeclaration(node) {
|
|
277
365
|
hasExportDefault = true;
|
|
@@ -331,6 +419,7 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
331
419
|
if (!node.id)
|
|
332
420
|
return;
|
|
333
421
|
const name = node.id.name;
|
|
422
|
+
topLevelBindingNames.add(name);
|
|
334
423
|
const isNamedExport = parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
335
424
|
const isDefaultExport = parent?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration;
|
|
336
425
|
topLevelFunctions.push({
|
|
@@ -351,6 +440,9 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
351
440
|
return;
|
|
352
441
|
const isNamedExport = parent?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration;
|
|
353
442
|
for (const declarator of node.declarations) {
|
|
443
|
+
// Every top-level binding (including non-function consts like a
|
|
444
|
+
// registry array and destructured bindings) is a closure target.
|
|
445
|
+
collectPatternNames(declarator.id, topLevelBindingNames);
|
|
354
446
|
if (declarator.id.type !== utils_1.AST_NODE_TYPES.Identifier)
|
|
355
447
|
continue;
|
|
356
448
|
const name = declarator.id.name;
|
|
@@ -366,7 +458,17 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
366
458
|
});
|
|
367
459
|
}
|
|
368
460
|
},
|
|
369
|
-
'Program:exit'() {
|
|
461
|
+
'Program:exit'(programNode) {
|
|
462
|
+
// CLI entry-point modules deliberately colocate their parser/printer/
|
|
463
|
+
// guard/compute helpers — those functions ARE the file's purpose, not
|
|
464
|
+
// foreign utilities. Recognize them via a `require.main` reference or a
|
|
465
|
+
// top-level self-invocation of one of their own functions
|
|
466
|
+
// (`void autoRunIfMain();`) and exempt the whole file.
|
|
467
|
+
const topLevelFunctionNames = new Set(topLevelFunctions.map((f) => f.name));
|
|
468
|
+
if (referencesRequireMain ||
|
|
469
|
+
hasTopLevelSelfInvocation(programNode, topLevelFunctionNames)) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
370
472
|
// Determine the file's primary export name (basename heuristic)
|
|
371
473
|
// e.g. "modifyRoleMembers" in "modifyRoleMembers.f.ts"
|
|
372
474
|
const primaryName = basename;
|
|
@@ -419,7 +521,7 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
419
521
|
continue;
|
|
420
522
|
// --- ignoreClosures: skip if the function body references module-scoped identifiers not passed as params ---
|
|
421
523
|
if (ignoreClosures) {
|
|
422
|
-
const closesOverModuleScope = functionClosesOverModuleScope(fn,
|
|
524
|
+
const closesOverModuleScope = functionClosesOverModuleScope(fn, topLevelBindingNames);
|
|
423
525
|
if (closesOverModuleScope)
|
|
424
526
|
continue;
|
|
425
527
|
}
|
|
@@ -447,14 +549,14 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
447
549
|
* passed as parameters, making it non-trivially extractable.
|
|
448
550
|
*
|
|
449
551
|
* We identify "module-scope" identifiers as names of other top-level
|
|
450
|
-
* declarations in the file (functions, variables) that
|
|
451
|
-
* Imported names and standard globals are considered
|
|
452
|
-
* just re-import them).
|
|
552
|
+
* declarations in the file (functions, variables, destructured bindings) that
|
|
553
|
+
* are NOT imported. Imported names and standard globals are considered
|
|
554
|
+
* "extractable" (you'd just re-import them).
|
|
453
555
|
*
|
|
454
556
|
* Strategy: collect referenced identifiers in the body, subtract param names,
|
|
455
557
|
* then check if any remain that are names of other top-level declarations.
|
|
456
558
|
*/
|
|
457
|
-
function functionClosesOverModuleScope(fn,
|
|
559
|
+
function functionClosesOverModuleScope(fn, topLevelBindingNames) {
|
|
458
560
|
const body = getFunctionBody(fn);
|
|
459
561
|
if (!body)
|
|
460
562
|
return false;
|
|
@@ -469,12 +571,10 @@ function functionClosesOverModuleScope(fn, topLevelFunctions) {
|
|
|
469
571
|
for (const param of paramNames) {
|
|
470
572
|
referencedIds.delete(param);
|
|
471
573
|
}
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
// If ANY referenced identifier is a top-level sibling function name,
|
|
475
|
-
// then this function closes over a module-scope binding.
|
|
574
|
+
// If ANY referenced identifier is a top-level sibling binding name, then this
|
|
575
|
+
// function closes over module scope.
|
|
476
576
|
for (const ref of referencedIds) {
|
|
477
|
-
if (
|
|
577
|
+
if (topLevelBindingNames.has(ref)) {
|
|
478
578
|
return true;
|
|
479
579
|
}
|
|
480
580
|
}
|
|
@@ -232,18 +232,23 @@ function collectDependencies(fnNode, knownFunctionNames) {
|
|
|
232
232
|
visit(fnNode.body);
|
|
233
233
|
return [...dependencies];
|
|
234
234
|
}
|
|
235
|
-
function dependencyOrder(functions, direction) {
|
|
235
|
+
function dependencyOrder(functions, direction, groupOrder) {
|
|
236
236
|
const dependencyMap = new Map();
|
|
237
237
|
const originalIndexMap = new Map(functions.map((fn) => [fn.name, fn.originalIndex]));
|
|
238
|
+
const groupRankMap = new Map(functions.map((fn) => [fn.name, groupOrder.indexOf(classifyGroup(fn))]));
|
|
238
239
|
functions.forEach((fn) => {
|
|
239
240
|
dependencyMap.set(fn.name, fn.dependencies);
|
|
240
241
|
});
|
|
242
|
+
// Tiebreak among functions the call graph does NOT order relative to each
|
|
243
|
+
// other (independent roots, sibling callees, cycle members): configured
|
|
244
|
+
// group order first, then original source position. Both keys are intrinsic
|
|
245
|
+
// to the function rather than its current arrangement, so the traversal is a
|
|
246
|
+
// fixed point once reached — obeying a move never spawns a contradicting one.
|
|
247
|
+
const tiebreak = (a, b) => (groupRankMap.get(a) ?? 0) - (groupRankMap.get(b) ?? 0) ||
|
|
248
|
+
(originalIndexMap.get(a) ?? 0) - (originalIndexMap.get(b) ?? 0);
|
|
241
249
|
const visited = new Set();
|
|
242
250
|
const order = [];
|
|
243
|
-
const
|
|
244
|
-
.slice()
|
|
245
|
-
.sort((a, b) => a.originalIndex - b.originalIndex)
|
|
246
|
-
.map((fn) => fn.name);
|
|
251
|
+
const namesInTiebreakOrder = functions.map((fn) => fn.name).sort(tiebreak);
|
|
247
252
|
const incomingCount = new Map();
|
|
248
253
|
functions.forEach((fn) => {
|
|
249
254
|
if (!incomingCount.has(fn.name)) {
|
|
@@ -253,18 +258,13 @@ function dependencyOrder(functions, direction) {
|
|
|
253
258
|
incomingCount.set(dep, (incomingCount.get(dep) || 0) + 1);
|
|
254
259
|
});
|
|
255
260
|
});
|
|
256
|
-
const roots =
|
|
261
|
+
const roots = namesInTiebreakOrder.filter((name) => (incomingCount.get(name) || 0) === 0);
|
|
257
262
|
const visit = (name) => {
|
|
258
263
|
if (visited.has(name)) {
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
261
266
|
visited.add(name);
|
|
262
|
-
const deps = dependencyMap
|
|
263
|
-
.get(name)
|
|
264
|
-
?.slice()
|
|
265
|
-
.sort((a, b) => {
|
|
266
|
-
return ((originalIndexMap.get(a) || 0) - (originalIndexMap.get(b) || 0));
|
|
267
|
-
}) || [];
|
|
267
|
+
const deps = dependencyMap.get(name)?.slice().sort(tiebreak) || [];
|
|
268
268
|
if (direction === 'callees-first') {
|
|
269
269
|
deps.forEach(visit);
|
|
270
270
|
order.push(name);
|
|
@@ -274,12 +274,15 @@ function dependencyOrder(functions, direction) {
|
|
|
274
274
|
deps.forEach(visit);
|
|
275
275
|
}
|
|
276
276
|
};
|
|
277
|
-
|
|
277
|
+
// Depth-first from roots keeps each caller immediately above its own helper
|
|
278
|
+
// subtree (callers-first) — grouping call chains vertically. The call graph
|
|
279
|
+
// is primary; group order only breaks ties the graph leaves open.
|
|
280
|
+
[...roots, ...namesInTiebreakOrder].forEach(visit);
|
|
278
281
|
return order;
|
|
279
282
|
}
|
|
280
283
|
function computeExpectedOrder(functions, options) {
|
|
281
284
|
const groupOrder = normalizeGroupOrder(options.groupOrder);
|
|
282
|
-
const dependencySequence = dependencyOrder(functions, options.dependencyDirection);
|
|
285
|
+
const dependencySequence = dependencyOrder(functions, options.dependencyDirection, groupOrder);
|
|
283
286
|
const dependencyRank = new Map(dependencySequence.map((name, idx) => [name, idx]));
|
|
284
287
|
const exportRank = (fn) => {
|
|
285
288
|
if (options.exportPlacement === 'ignore') {
|
|
@@ -290,10 +293,12 @@ function computeExpectedOrder(functions, options) {
|
|
|
290
293
|
}
|
|
291
294
|
return fn.isExported ? 1 : 0;
|
|
292
295
|
};
|
|
293
|
-
|
|
296
|
+
// Export placement is the only concern allowed to outrank the call graph.
|
|
297
|
+
// The dependency sequence already folds group order in as a tiebreak, so a
|
|
298
|
+
// caller is never sorted below the helpers it invokes on account of its verb
|
|
299
|
+
// prefix — the defect that produced self-contradicting move instructions.
|
|
294
300
|
return functions.slice().sort((a, b) => {
|
|
295
301
|
return (exportRank(a) - exportRank(b) ||
|
|
296
|
-
groupRank(a) - groupRank(b) ||
|
|
297
302
|
(dependencyRank.get(a.name) ?? Number.MAX_SAFE_INTEGER) -
|
|
298
303
|
(dependencyRank.get(b.name) ?? Number.MAX_SAFE_INTEGER) ||
|
|
299
304
|
a.originalIndex - b.originalIndex);
|
|
@@ -415,7 +420,11 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
415
420
|
: 'exports stay at the bottom of the file';
|
|
416
421
|
const group = classifyGroup(misplacedInfo);
|
|
417
422
|
const groupOrder = normalizeGroupOrder(normalizedOptions.groupOrder);
|
|
418
|
-
|
|
423
|
+
// Group order only settles ties the call graph leaves open, so cite it
|
|
424
|
+
// as a reason only when this function has no helpers of its own — never
|
|
425
|
+
// paired with "callers should sit above the helpers they invoke".
|
|
426
|
+
const groupReason = misplacedInfo.dependencies.length === 0 &&
|
|
427
|
+
groupOrder.indexOf(group) > 0
|
|
419
428
|
? `${group.replace('-', ' ')} should follow the configured group order`
|
|
420
429
|
: '';
|
|
421
430
|
const reasons = [dependencyReason, exportReason, groupReason]
|
|
@@ -459,7 +468,16 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
459
468
|
const slice = node.body.slice(firstFunctionIndex, lastFunctionIndex + 1);
|
|
460
469
|
const blockContainsOnlyFunctions = slice.every((statement) => functionStatements.has(statement));
|
|
461
470
|
if (!blockContainsOnlyFunctions) {
|
|
462
|
-
|
|
471
|
+
// Real modules interleave type aliases, consts, and top-level
|
|
472
|
+
// calls (e.g. `void autoRunIfMain();`) between functions. Rather
|
|
473
|
+
// than bail, reorder only the function statements among their own
|
|
474
|
+
// slots, leaving every other statement exactly where it is. Plain
|
|
475
|
+
// node ranges keep the edits disjoint (no comment-span overlap
|
|
476
|
+
// with the interleaved statements).
|
|
477
|
+
return sourceOrderedInfos.map((info, idx) => {
|
|
478
|
+
const target = expectedOrderInfos[idx];
|
|
479
|
+
return fixer.replaceTextRange(info.statementNode.range, sourceCode.getText(target.statementNode));
|
|
480
|
+
});
|
|
463
481
|
}
|
|
464
482
|
const [start] = statementRanges.get(node.body[firstFunctionIndex]) ||
|
|
465
483
|
getStatementRangeWithComments(node.body[firstFunctionIndex], sourceCode);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.18.12",
|
|
4
|
+
"date": "2026-07-12T03:45:44.071Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-utility-function-own-file",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1285
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt CLI entry-point and registry modules (closes #1285)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "vertically-group-related-functions",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1286
|
|
19
|
+
],
|
|
20
|
+
"summary": "make call graph primary over name-prefix groups (closes #1286)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.18.11",
|
|
26
|
+
"date": "2026-07-11T16:25:05.418Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "parallelize-async-operations",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1284
|
|
33
|
+
],
|
|
34
|
+
"summary": "treat throw-gated guard awaits as a sequencing barrier (closes #1284)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.18.10",
|
|
4
40
|
"date": "2026-07-11T15:24:03.394Z",
|