@blumintinc/eslint-plugin-blumint 1.19.7 → 1.19.9

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
@@ -222,7 +222,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
222
222
  module.exports = {
223
223
  meta: {
224
224
  name: '@blumintinc/eslint-plugin-blumint',
225
- version: '1.19.7',
225
+ version: '1.19.9',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -43,6 +43,29 @@ const SESSION_MATCHER = {
43
43
  function isDirectiveComment(comment) {
44
44
  return DIRECTIVE_PREFIX.test(comment.value.trim());
45
45
  }
46
+ /**
47
+ * Pointer phrasing by which a directive DEFERS its rationale to an adjacent
48
+ * comment: "see above", "per the note below", "as noted in the comment".
49
+ * Up to two intervening words ("the note", "the preceding") are tolerated so
50
+ * natural phrasings still register as deferrals.
51
+ */
52
+ const DEFERRAL_PATTERN = /\b(?:see|per|as)\s+(?:\w+\s+){0,2}(?:above|below|note|comment|preceding)\b/i;
53
+ /**
54
+ * A directive defers to its preceding comment only when its own `--` text is a
55
+ * mere pointer to that comment ("see above") or carries no substantive words at
56
+ * all (a bare "^" gesture). This is the discriminating signal for #1296's
57
+ * split-justification support: a directive that already states a substantive,
58
+ * self-contained reason owns its rationale outright, so an unrelated docblock or
59
+ * line comment that merely sits above it — documenting the declaration, not the
60
+ * suppression — must never contribute its incidental harness keywords (#1312).
61
+ */
62
+ function defersToPreceding(justification) {
63
+ const text = justification.trim();
64
+ if (DEFERRAL_PATTERN.test(text)) {
65
+ return true;
66
+ }
67
+ return text.replace(/[^a-z]/gi, '') === '';
68
+ }
46
69
  /**
47
70
  * Isolates the justification text of a directive comment: everything after the
48
71
  * first `--` separator, spanning every line of a multi-line block body. The
@@ -109,12 +132,16 @@ exports.noHarnessCoupledDisables = (0, createRule_1.createRule)({
109
132
  }
110
133
  // Split-justification style: an immediately-adjacent preceding
111
134
  // non-directive comment (no intervening blank line or code) is part
112
- // of the directive's rationale when the directive defers to it.
135
+ // of the directive's rationale ONLY when the directive defers to it.
136
+ // Without the deferral gate, an unrelated docblock above the
137
+ // declaration bleeds its incidental harness words into a directive
138
+ // that already carries a self-contained code-level reason (#1312).
113
139
  let scanned = justification;
114
140
  const previous = comments[index - 1];
115
141
  if (previous &&
116
142
  !isDirectiveComment(previous) &&
117
- comment.loc.start.line - previous.loc.end.line <= 1) {
143
+ comment.loc.start.line - previous.loc.end.line <= 1 &&
144
+ defersToPreceding(justification)) {
118
145
  scanned = `${previous.value}\n${scanned}`;
119
146
  }
120
147
  const matchedTerm = findHarnessTerm(scanned);
@@ -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
- const commentsBefore = filterComments(sourceCode.getCommentsBefore(statement) || []);
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(filterComments(sourceCode.getCommentsBefore(nextStatement) || []))
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, idx) => {
461
- const nextInfo = sourceOrderedInfos[idx + 1];
462
- const [rangeStart, rangeEnd] = getStatementRangeWithComments(info.statementNode, sourceCode, consumedComments, nextInfo?.statementNode);
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. Plain
492
- // node ranges keep the edits disjoint (no comment-span overlap
493
- // with the interleaved statements).
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
- return fixer.replaceTextRange(info.statementNode.range, sourceCode.getText(target.statementNode));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.7",
3
+ "version": "1.19.9",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,40 @@
1
1
  [
2
+ {
3
+ "version": "1.19.9",
4
+ "date": "2026-07-17T01:27:12.035Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-harness-coupled-disables",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1312
11
+ ],
12
+ "summary": "only merge preceding comment when directive defers to it (closes #1312)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.8",
18
+ "date": "2026-07-17T00:25:07.461Z",
19
+ "rules": [
20
+ {
21
+ "name": "prefer-utility-function-own-file",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1311
25
+ ],
26
+ "summary": "exempt Next.js reserved page exports (closes #1311)"
27
+ },
28
+ {
29
+ "name": "vertically-group-related-functions",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1310
33
+ ],
34
+ "summary": "keep interleaved statements' own comments in place when reordering (refs #1310); carry leading JSDoc with reordered functions across interleaved statements (closes #1310)"
35
+ }
36
+ ]
37
+ },
2
38
  {
3
39
  "version": "1.19.7",
4
40
  "date": "2026-07-16T01:36:53.555Z",