@blumintinc/eslint-plugin-blumint 1.19.6 → 1.19.8
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
|
@@ -77,6 +77,47 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
77
77
|
}
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
|
+
function isBindingExportedInScope(scopeBody, bindingName) {
|
|
81
|
+
// A class bound at module (or namespace) scope can be exported by a
|
|
82
|
+
// statement separate from its declaration — `export { Foo }`,
|
|
83
|
+
// `export { Foo as Bar }`, `export default Foo`, or `export = Foo`. Any of
|
|
84
|
+
// these exposes it to subclassing from another file just as an inline
|
|
85
|
+
// export does.
|
|
86
|
+
for (const statement of scopeBody) {
|
|
87
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration &&
|
|
88
|
+
!statement.source) {
|
|
89
|
+
for (const specifier of statement.specifiers) {
|
|
90
|
+
if (specifier.local.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
91
|
+
specifier.local.name === bindingName) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (statement.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration &&
|
|
97
|
+
statement.declaration.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
98
|
+
statement.declaration.name === bindingName) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (statement.type === utils_1.AST_NODE_TYPES.TSExportAssignment &&
|
|
102
|
+
statement.expression.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
103
|
+
statement.expression.name === bindingName) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
function getEnclosingScopeBody(node) {
|
|
110
|
+
if (!node) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
if (node.type === utils_1.AST_NODE_TYPES.Program) {
|
|
114
|
+
return node.body;
|
|
115
|
+
}
|
|
116
|
+
if (node.type === utils_1.AST_NODE_TYPES.TSModuleBlock) {
|
|
117
|
+
return node.body;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
80
121
|
function isExportedClass(node) {
|
|
81
122
|
const parent = node.parent;
|
|
82
123
|
if (!parent) {
|
|
@@ -86,14 +127,33 @@ exports.noRedundantThisParams = (0, createRule_1.createRule)({
|
|
|
86
127
|
parent.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
87
128
|
return true;
|
|
88
129
|
}
|
|
89
|
-
// `
|
|
90
|
-
//
|
|
130
|
+
// A `class Foo {}` declaration exported by a later `export { Foo }` /
|
|
131
|
+
// `export default Foo` in the same scope is still reachable cross-file.
|
|
132
|
+
if (node.type === utils_1.AST_NODE_TYPES.ClassDeclaration &&
|
|
133
|
+
node.id &&
|
|
134
|
+
(parent.type === utils_1.AST_NODE_TYPES.Program ||
|
|
135
|
+
parent.type === utils_1.AST_NODE_TYPES.TSModuleBlock)) {
|
|
136
|
+
const scopeBody = getEnclosingScopeBody(parent);
|
|
137
|
+
if (scopeBody && isBindingExportedInScope(scopeBody, node.id.name)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// `export const Foo = class { ... }`, or `const Foo = class {}` exported by
|
|
142
|
+
// a later statement, both expose the class expression like an exported
|
|
143
|
+
// declaration does.
|
|
91
144
|
if (node.type === utils_1.AST_NODE_TYPES.ClassExpression &&
|
|
92
|
-
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator
|
|
145
|
+
parent.type === utils_1.AST_NODE_TYPES.VariableDeclarator &&
|
|
146
|
+
parent.id.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
93
147
|
const declaration = parent.parent;
|
|
94
148
|
const exportNode = declaration?.parent;
|
|
95
|
-
|
|
96
|
-
exportNode?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration)
|
|
149
|
+
if (exportNode?.type === utils_1.AST_NODE_TYPES.ExportNamedDeclaration ||
|
|
150
|
+
exportNode?.type === utils_1.AST_NODE_TYPES.ExportDefaultDeclaration) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const scopeBody = getEnclosingScopeBody(exportNode);
|
|
154
|
+
if (scopeBody && isBindingExportedInScope(scopeBody, parent.id.name)) {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
97
157
|
}
|
|
98
158
|
return false;
|
|
99
159
|
}
|
|
@@ -31,6 +31,20 @@ const ASTHelpers_1 = require("../utils/ASTHelpers");
|
|
|
31
31
|
const DEFAULT_MIN_STATEMENTS = 8;
|
|
32
32
|
const DEFAULT_MIN_LINES = 12;
|
|
33
33
|
const DEFAULT_IGNORE_CLOSURES = true;
|
|
34
|
+
/**
|
|
35
|
+
* Next.js framework-reserved page exports. Next.js only recognizes these when
|
|
36
|
+
* they are exported from the page file itself, so they categorically cannot be
|
|
37
|
+
* moved to a `util/` file and re-imported — the rule's suggested remediation is
|
|
38
|
+
* impossible to follow for them. Mirrors the exemption precedent set in
|
|
39
|
+
* `semantic-function-prefixes` (issue #333).
|
|
40
|
+
*/
|
|
41
|
+
const NEXTJS_RESERVED_PAGE_EXPORTS = new Set([
|
|
42
|
+
'getServerSideProps',
|
|
43
|
+
'getStaticProps',
|
|
44
|
+
'getStaticPaths',
|
|
45
|
+
'middleware',
|
|
46
|
+
'config',
|
|
47
|
+
]);
|
|
34
48
|
/**
|
|
35
49
|
* Collects all identifiers referenced in a node body (for closure detection).
|
|
36
50
|
* Returns the set of identifier names referenced anywhere inside the node,
|
|
@@ -289,6 +303,23 @@ function isExemptFile(filename) {
|
|
|
289
303
|
return true;
|
|
290
304
|
return false;
|
|
291
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Returns true if the candidate is a Next.js framework-reserved page export
|
|
308
|
+
* (`getServerSideProps`, `getStaticProps`, `getStaticPaths`, `middleware`,
|
|
309
|
+
* `config`) living in a file under a `pages/` directory. Keying off the
|
|
310
|
+
* `pages/` path segment covers both the `src/pages/**` and bare `pages/**`
|
|
311
|
+
* Pages Router layouts. These exports must stay in the page file — Next.js only
|
|
312
|
+
* recognizes them there — so they can never be extracted to their own util
|
|
313
|
+
* file, regardless of size or closure geometry.
|
|
314
|
+
*/
|
|
315
|
+
function isNextReservedPageExport(name, filename) {
|
|
316
|
+
if (!NEXTJS_RESERVED_PAGE_EXPORTS.has(name))
|
|
317
|
+
return false;
|
|
318
|
+
if (!filename)
|
|
319
|
+
return false;
|
|
320
|
+
const normalized = filename.replace(/\\/g, '/');
|
|
321
|
+
return /(^|\/)pages\//.test(normalized);
|
|
322
|
+
}
|
|
292
323
|
/**
|
|
293
324
|
* Returns true if the module top-level self-invokes one of its own functions,
|
|
294
325
|
* e.g. `void autoRunIfMain();` or `main();`. This is the signature of a CLI
|
|
@@ -538,7 +569,15 @@ exports.preferUtilityFunctionOwnFile = (0, createRule_1.createRule)({
|
|
|
538
569
|
return;
|
|
539
570
|
// For each candidate, determine whether to flag
|
|
540
571
|
for (const info of topLevelFunctions) {
|
|
541
|
-
const { node, name, fn, isDefaultExport } = info;
|
|
572
|
+
const { node, name, fn, isDefaultExport, isNamedExport } = info;
|
|
573
|
+
// --- Exclusion: Next.js reserved page export ---
|
|
574
|
+
// `getServerSideProps`/`getStaticProps`/`getStaticPaths`/`middleware`/
|
|
575
|
+
// `config` are framework-reserved: Next.js only recognizes them when
|
|
576
|
+
// exported from the page file itself, so they can never move to a
|
|
577
|
+
// util/ file. Scoped to named exports under a `pages/` directory so it
|
|
578
|
+
// does not over-broaden to same-named functions elsewhere.
|
|
579
|
+
if (isNamedExport && isNextReservedPageExport(name, filename))
|
|
580
|
+
continue;
|
|
542
581
|
// --- Exclusion: is a hook ---
|
|
543
582
|
if (isHookName(name))
|
|
544
583
|
continue;
|
|
@@ -323,9 +323,19 @@ function computeExpectedOrder(functions, options) {
|
|
|
323
323
|
}
|
|
324
324
|
function getStatementRangeWithComments(statement, sourceCode, consumedComments, nextStatement) {
|
|
325
325
|
const filterComments = (comments) => (comments || []).filter((comment) => !consumedComments || !consumedComments.has(comment));
|
|
326
|
-
|
|
326
|
+
// A comment sharing a line with the code token immediately before it is a
|
|
327
|
+
// trailing comment of that preceding statement (e.g. `const x = 2; // note`),
|
|
328
|
+
// not a leading comment of the node beneath it. Attributing it to the node
|
|
329
|
+
// below would drag an interleaved statement's own end-of-line comment along
|
|
330
|
+
// when the following function is relocated. Only own-line comments count as
|
|
331
|
+
// leading comments.
|
|
332
|
+
const leadingCommentsOf = (target) => filterComments(sourceCode.getCommentsBefore(target) || []).filter((comment) => {
|
|
333
|
+
const tokenBefore = sourceCode.getTokenBefore(comment);
|
|
334
|
+
return (!tokenBefore || tokenBefore.loc.end.line !== comment.loc.start.line);
|
|
335
|
+
});
|
|
336
|
+
const commentsBefore = leadingCommentsOf(statement);
|
|
327
337
|
const nextLeadingComments = nextStatement
|
|
328
|
-
? new Set(
|
|
338
|
+
? new Set(leadingCommentsOf(nextStatement))
|
|
329
339
|
: new Set();
|
|
330
340
|
const trailingCandidates = filterComments(sourceCode.getCommentsAfter(statement) || []).filter((comment) => !nextLeadingComments.has(comment));
|
|
331
341
|
const start = commentsBefore.length > 0
|
|
@@ -457,9 +467,15 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
457
467
|
.sort((a, b) => a.originalIndex - b.originalIndex);
|
|
458
468
|
const consumedComments = new Set();
|
|
459
469
|
const statementRanges = new Map();
|
|
460
|
-
sourceOrderedInfos.forEach((info
|
|
461
|
-
|
|
462
|
-
|
|
470
|
+
sourceOrderedInfos.forEach((info) => {
|
|
471
|
+
// Bound each function's trailing comments by the statement that
|
|
472
|
+
// physically follows it in the block — which may be an interleaved
|
|
473
|
+
// non-function statement, not the next function — so an interleaved
|
|
474
|
+
// statement's own leading comment is never swallowed into the
|
|
475
|
+
// function above it.
|
|
476
|
+
const bodyIndex = node.body.indexOf(info.statementNode);
|
|
477
|
+
const nextStatement = bodyIndex >= 0 ? node.body[bodyIndex + 1] : undefined;
|
|
478
|
+
const [rangeStart, rangeEnd] = getStatementRangeWithComments(info.statementNode, sourceCode, consumedComments, nextStatement);
|
|
463
479
|
statementRanges.set(info.statementNode, [rangeStart, rangeEnd]);
|
|
464
480
|
});
|
|
465
481
|
const firstFunctionIndex = node.body.findIndex((statement) => functionStatements.has(statement));
|
|
@@ -488,12 +504,22 @@ exports.verticallyGroupRelatedFunctions = (0, createRule_1.createRule)({
|
|
|
488
504
|
// Real modules interleave type aliases, consts, and top-level
|
|
489
505
|
// calls (e.g. `void autoRunIfMain();`) between functions. Rather
|
|
490
506
|
// than bail, reorder only the function statements among their own
|
|
491
|
-
// slots, leaving every other statement exactly where it is.
|
|
492
|
-
//
|
|
493
|
-
//
|
|
507
|
+
// slots, leaving every other statement exactly where it is. Both
|
|
508
|
+
// the destination slot and the source text use the same
|
|
509
|
+
// comment-inclusive ranges Path A relies on, so each function's
|
|
510
|
+
// leading JSDoc travels with it instead of being left orphaned in
|
|
511
|
+
// its old slot. The precomputed `statementRanges` already exclude
|
|
512
|
+
// each interleaved statement's own leading comments (they are
|
|
513
|
+
// treated as the next function's leading comments), so the widened
|
|
514
|
+
// edits stay disjoint from one another and from the interleaved
|
|
515
|
+
// statements — no comment-span overlap.
|
|
494
516
|
return sourceOrderedInfos.map((info, idx) => {
|
|
495
517
|
const target = expectedOrderInfos[idx];
|
|
496
|
-
|
|
518
|
+
const destRange = statementRanges.get(info.statementNode) ||
|
|
519
|
+
getStatementRangeWithComments(info.statementNode, sourceCode);
|
|
520
|
+
const [targetStart, targetEnd] = statementRanges.get(target.statementNode) ||
|
|
521
|
+
getStatementRangeWithComments(target.statementNode, sourceCode);
|
|
522
|
+
return fixer.replaceTextRange(destRange, sourceCode.text.slice(targetStart, targetEnd));
|
|
497
523
|
});
|
|
498
524
|
}
|
|
499
525
|
const [start] = statementRanges.get(node.body[firstFunctionIndex]) ||
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.8",
|
|
4
|
+
"date": "2026-07-17T00:25:07.461Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-utility-function-own-file",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1311
|
|
11
|
+
],
|
|
12
|
+
"summary": "exempt Next.js reserved page exports (closes #1311)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "vertically-group-related-functions",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1310
|
|
19
|
+
],
|
|
20
|
+
"summary": "keep interleaved statements' own comments in place when reordering (refs #1310); carry leading JSDoc with reordered functions across interleaved statements (closes #1310)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.19.7",
|
|
26
|
+
"date": "2026-07-16T01:36:53.555Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "no-redundant-this-params",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1309
|
|
33
|
+
],
|
|
34
|
+
"summary": "treat trailing-export classes as reachable"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.19.6",
|
|
4
40
|
"date": "2026-07-16T01:27:07.130Z",
|