@blumintinc/eslint-plugin-blumint 1.20.140 → 1.20.141

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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.140',
226
+ version: '1.20.141',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -218,19 +218,14 @@ function editOffset(anchor) {
218
218
  *
219
219
  * Widening to the anchor's line start is what lets the emitted
220
220
  * `${indent}import …\n` leave the displaced statement on the indentation it
221
- * already had, but that is sound only while whitespace is all that precedes
222
- * the anchor: a `'use client';` sharing the anchor's line would be demoted by
223
- * an insertion at column 0. When the resulting position still falls inside a
224
- * retirement the anchor statement is itself the declaration being retired,
225
- * and the retirement claims the indentation ahead of it — the insertion moves
226
- * to that edit's start, since ESLint rejects a fix nested inside another.
221
+ * already had; `importAnchorLineStartIfOwned` restricts that to the anchors it
222
+ * is sound for. When the resulting position still falls inside a retirement
223
+ * the anchor statement is itself the declaration being retired, and the
224
+ * retirement claims the indentation ahead of it the insertion moves to that
225
+ * edit's start, since ESLint rejects a fix nested inside another.
227
226
  */
228
227
  function importPlacement(sourceCode, anchor, edits) {
229
- const anchorStart = editOffset(anchor);
230
- const lineStart = sourceCode.text.lastIndexOf('\n', anchorStart - 1) + 1;
231
- const placement = /^[ \t]*$/.test(sourceCode.text.slice(lineStart, anchorStart))
232
- ? (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor)
233
- : anchor;
228
+ const placement = (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor);
234
229
  const offset = editOffset(placement);
235
230
  const enclosing = edits.find((edit) => edit.start < offset && offset < edit.end);
236
231
  return enclosing ? { kind: 'index', index: enclosing.start } : placement;
@@ -894,7 +894,7 @@ function generateHoistingFixes(groups, callback, depsArray, depTexts, insertionS
894
894
  for (const group of groups.values()) {
895
895
  const sortedProps = Array.from(group.properties.values()).sort((a, b) => a.order - b.order);
896
896
  const pattern = `{ ${sortedProps.map((p) => p.text).join(', ')} }`;
897
- hoistedLines.push(`${indent}const ${pattern} = ${nullishSourceText(group.objectText, group.inits[0])} ?? {};`);
897
+ hoistedLines.push(`const ${pattern} = ${nullishSourceText(group.objectText, group.inits[0])} ?? {};`);
898
898
  }
899
899
  reservedNamesByScope.set(scope, updatedReservedNames);
900
900
  const newDepSet = new Set(newDepTexts);
@@ -904,9 +904,18 @@ function generateHoistingFixes(groups, callback, depsArray, depTexts, insertionS
904
904
  newDepSet.add(name);
905
905
  }
906
906
  }
907
- const insertAt = sourceCode.getText().lastIndexOf('\n', insertionStatement.range[0]) + 1;
907
+ // Anchoring to the start of the statement's LINE is the same offset as the
908
+ // statement itself only while the statement opens that line. When it does not
909
+ // — a body collapsed onto one line, or a sibling declared ahead of it — that
910
+ // offset sits outside the enclosing function, so a declaration reading the
911
+ // function's own parameters would be hoisted out of the scope that binds them.
912
+ const text = sourceCode.getText();
913
+ const lineStart = text.lastIndexOf('\n', insertionStatement.range[0]) + 1;
914
+ const ownsItsLine = /^[\t ]*$/.test(text.slice(lineStart, insertionStatement.range[0]));
908
915
  const fixes = [
909
- fixer.insertTextBeforeRange([insertAt, insertAt], `${hoistedLines.join('\n')}\n`),
916
+ ownsItsLine
917
+ ? fixer.insertTextBeforeRange([lineStart, lineStart], `${hoistedLines.map((line) => `${indent}${line}`).join('\n')}\n`)
918
+ : fixer.insertTextBefore(insertionStatement, `${hoistedLines.join(' ')} `),
910
919
  fixer.replaceText(depsArray, `[${newDepTexts.join(', ')}]`),
911
920
  ];
912
921
  for (const decl of declarationsToRemove) {
@@ -459,7 +459,7 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
459
459
  // a header comment — while still placing it above the first
460
460
  // existing import.
461
461
  const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
462
- fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor), `${(0, importInsertion_1.importAnchorIndent)(sourceCode, anchor)}${importStatement}\n`));
462
+ fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), `${(0, importInsertion_1.importAnchorIndent)(sourceCode, anchor)}${importStatement}\n`));
463
463
  scheduledImportFix = true;
464
464
  }
465
465
  // Anchor the decorator on the method — its first token, so ahead of
@@ -507,7 +507,7 @@ exports.enforceMemoizeGetters = (0, createRule_1.createRule)({
507
507
  // with the statement it displaces; widening to the line start
508
508
  // then leaves that statement's indentation intact.
509
509
  const indent = (0, importInsertion_1.importAnchorIndent)(sourceCode, anchor);
510
- fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor), `${indent}import { Memoize } from '${MEMOIZE_PREFERRED_MODULE}';\n`));
510
+ fixes.push((0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), `${indent}import { Memoize } from '${MEMOIZE_PREFERRED_MODULE}';\n`));
511
511
  scheduledImportFix = true;
512
512
  }
513
513
  // Anchor the decorator on the member — its first token, so ahead of
@@ -258,10 +258,13 @@ function ensureDeepCompareImportFixes(context, fixer) {
258
258
  const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
259
259
  const indent = (0, importInsertion_1.importAnchorIndent)(sourceCode, anchor);
260
260
  const importText = `${indent}import { ${DEEP_COMPARE_HOOK} } from '${DEEP_COMPARE_MODULE}';\n`;
261
- // Widened to the anchor's line start because the emitted statement carries
262
- // its own indentation, leaving the displaced anchor sitting on the original.
261
+ // Widened to the anchor's line start where the anchor opens it, because the
262
+ // emitted statement carries its own indentation and so leaves the displaced
263
+ // anchor sitting on the original. Where the anchor shares its line the
264
+ // widening is declined: the offset would otherwise fall ahead of the prologue
265
+ // this anchor was chosen to sit below.
263
266
  return [
264
- (0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStart)(sourceCode, anchor), importText),
267
+ (0, importInsertion_1.insertAtImportAnchor)(sourceCode, fixer, (0, importInsertion_1.importAnchorLineStartIfOwned)(sourceCode, anchor), importText),
265
268
  ];
266
269
  }
267
270
  function isImportedIdentifier(context, name) {
@@ -40,11 +40,25 @@ export declare function importInsertionAnchor(sourceCode: ImportInsertionSource)
40
40
  */
41
41
  export declare function insertAtImportAnchor(sourceCode: ImportInsertionSource, fixer: TSESLint.RuleFixer, anchor: ImportInsertionAnchor, text: string): TSESLint.RuleFix;
42
42
  /**
43
- * Widens `anchor` to the start of its line, for rules that emit
44
- * `${indent}import …\n` so the displaced anchor keeps its own indentation.
43
+ * Widens `anchor` to the start of its line only where whitespace is all that
44
+ * precedes it there, and leaves it alone otherwise.
45
+ *
45
46
  * Raw-offset anchors pass through: they never sit inside an indented line.
47
+ *
48
+ * The widening buys one thing — an emitted `${indent}import …\n` leaves the
49
+ * displaced anchor on the indentation it already had — and that reasoning holds
50
+ * only while the anchor opens its line. When something else opens that line the
51
+ * widened offset sits ahead of it, and the anchor resolves past the prologue
52
+ * precisely because the prologue is what precedes it: a `'use client'` is a
53
+ * directive only while it is the first statement, so an import spliced above it
54
+ * demotes it to an inert expression statement that no bundler reads. Nothing
55
+ * downstream reports that — the output re-lints clean and the demoted directive
56
+ * is still valid TypeScript.
57
+ *
58
+ * Declining to widen costs the displaced anchor its indentation and keeps the
59
+ * file's meaning, which is the safe direction of that trade.
46
60
  */
47
- export declare function importAnchorLineStart(sourceCode: ImportInsertionSource, anchor: ImportInsertionAnchor): ImportInsertionAnchor;
61
+ export declare function importAnchorLineStartIfOwned(sourceCode: ImportInsertionSource, anchor: ImportInsertionAnchor): ImportInsertionAnchor;
48
62
  /**
49
63
  * The whitespace prefix of the anchor's line, for fixers that replicate the
50
64
  * anchor's indentation on the inserted import.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.importAnchorIndent = exports.importAnchorLineStart = exports.insertAtImportAnchor = exports.importInsertionAnchor = void 0;
3
+ exports.importAnchorIndent = exports.importAnchorLineStartIfOwned = exports.insertAtImportAnchor = exports.importInsertionAnchor = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const disableDirectives_1 = require("./disableDirectives");
6
6
  /**
@@ -95,18 +95,35 @@ function insertAtImportAnchor(sourceCode, fixer, anchor, text) {
95
95
  }
96
96
  exports.insertAtImportAnchor = insertAtImportAnchor;
97
97
  /**
98
- * Widens `anchor` to the start of its line, for rules that emit
99
- * `${indent}import …\n` so the displaced anchor keeps its own indentation.
98
+ * Widens `anchor` to the start of its line only where whitespace is all that
99
+ * precedes it there, and leaves it alone otherwise.
100
+ *
100
101
  * Raw-offset anchors pass through: they never sit inside an indented line.
102
+ *
103
+ * The widening buys one thing — an emitted `${indent}import …\n` leaves the
104
+ * displaced anchor on the indentation it already had — and that reasoning holds
105
+ * only while the anchor opens its line. When something else opens that line the
106
+ * widened offset sits ahead of it, and the anchor resolves past the prologue
107
+ * precisely because the prologue is what precedes it: a `'use client'` is a
108
+ * directive only while it is the first statement, so an import spliced above it
109
+ * demotes it to an inert expression statement that no bundler reads. Nothing
110
+ * downstream reports that — the output re-lints clean and the demoted directive
111
+ * is still valid TypeScript.
112
+ *
113
+ * Declining to widen costs the displaced anchor its indentation and keeps the
114
+ * file's meaning, which is the safe direction of that trade.
101
115
  */
102
- function importAnchorLineStart(sourceCode, anchor) {
116
+ function importAnchorLineStartIfOwned(sourceCode, anchor) {
103
117
  if (anchor.kind === 'index') {
104
118
  return anchor;
105
119
  }
106
- const lineStart = sourceCode.text.lastIndexOf('\n', anchor.target.range[0] - 1) + 1;
107
- return { kind: 'index', index: lineStart };
120
+ const start = anchor.target.range[0];
121
+ const lineStart = sourceCode.text.lastIndexOf('\n', start - 1) + 1;
122
+ return /^[\t ]*$/.test(sourceCode.text.slice(lineStart, start))
123
+ ? { kind: 'index', index: lineStart }
124
+ : anchor;
108
125
  }
109
- exports.importAnchorLineStart = importAnchorLineStart;
126
+ exports.importAnchorLineStartIfOwned = importAnchorLineStartIfOwned;
110
127
  /**
111
128
  * The whitespace prefix of the anchor's line, for fixers that replicate the
112
129
  * anchor's indentation on the inserted import.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.140",
3
+ "version": "1.20.141",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,42 @@
1
1
  [
2
+ {
3
+ "version": "1.20.141",
4
+ "date": "2026-08-12T00:57:09.340Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-early-destructuring",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1956
11
+ ],
12
+ "summary": "anchor the hoist to the hook call, not to its line (closes #1956)"
13
+ },
14
+ {
15
+ "name": "enforce-memoize-async",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1957
19
+ ],
20
+ "summary": "keep the injected import below a shared-line directive (closes #1957)"
21
+ },
22
+ {
23
+ "name": "enforce-memoize-getters",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1958
27
+ ],
28
+ "summary": "keep the injected import below a shared-line directive (closes #1958)"
29
+ },
30
+ {
31
+ "name": "prefer-use-deep-compare-memo",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1959
35
+ ],
36
+ "summary": "keep the injected import below a shared-line directive (closes #1959)"
37
+ }
38
+ ]
39
+ },
2
40
  {
3
41
  "version": "1.20.140",
4
42
  "date": "2026-08-11T22:49:18.139Z",