@blumintinc/eslint-plugin-blumint 1.20.187 → 1.20.189

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.187',
226
+ version: '1.20.189',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'avoidEntireObject' | 'removeUnusedDependency';
2
- export declare const noEntireObjectHookDeps: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ export declare const noEntireObjectHookDeps: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
3
4
  export {};
@@ -1167,6 +1167,48 @@ function getObjectUsagesInHook(hookBody, objectName, typeInfo, bodyIsDeferred =
1167
1167
  notUsed,
1168
1168
  };
1169
1169
  }
1170
+ /** The run of spaces/tabs opening the line that `offset` sits on. */
1171
+ function indentAt(text, offset) {
1172
+ const lineStart = text.lastIndexOf('\n', offset - 1) + 1;
1173
+ return /^[ \t]*/.exec(text.slice(lineStart, offset))?.[0] ?? '';
1174
+ }
1175
+ /**
1176
+ * Drop `span`, but re-emit any comment standing inside it.
1177
+ *
1178
+ * Removing a dependency means removing its separator too, so the span reaches
1179
+ * to a NEIGHBOURING element and therefore covers the margin between the two.
1180
+ * That margin is not the fixer's to delete: it can hold an
1181
+ * `eslint-disable-next-line` protecting the dependency that survives, and
1182
+ * dropping that directive silently re-enables another rule (#2208). Declining
1183
+ * the fix instead would only trade the lost comment for a transform that a
1184
+ * comment decides (#1877), so the comment is carried rather than obeyed.
1185
+ *
1186
+ * Each carried comment is re-emitted on a line of its own: a `//` comment
1187
+ * swallows whatever follows it on the same line, so folding one inline would
1188
+ * comment out the dependency that was meant to survive.
1189
+ */
1190
+ function removeCarryingComments(fixer, sourceCode, span, anchor) {
1191
+ const carried = sourceCode
1192
+ .getAllComments()
1193
+ .filter((comment) => comment.range[0] >= span[0] && comment.range[1] <= span[1]);
1194
+ // With nothing to preserve the span is pure separator and whitespace, so the
1195
+ // plain removal keeps the comment-free output exactly as it has always been.
1196
+ if (carried.length === 0) {
1197
+ return fixer.removeRange(span);
1198
+ }
1199
+ const text = sourceCode.getText();
1200
+ // Anchor the indentation on the end the surviving code sits against.
1201
+ const indent = indentAt(text, anchor === 'toNextElement' ? span[1] : carried[0].range[0]);
1202
+ const body = carried
1203
+ .map((comment) => text.slice(comment.range[0], comment.range[1]))
1204
+ .join(`\n${indent}`);
1205
+ // Both spellings close on a fresh line: whatever the span abutted — the next
1206
+ // dependency, or the separator trailing the last one — would otherwise land
1207
+ // on the final carried comment's line and be commented out.
1208
+ return fixer.replaceTextRange(span, anchor === 'toNextElement'
1209
+ ? `${body}\n${indent}`
1210
+ : `\n${indent}${body}\n${indent}`);
1211
+ }
1170
1212
  exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
1171
1213
  name: 'no-entire-object-hook-deps',
1172
1214
  meta: {
@@ -1339,21 +1381,19 @@ exports.noEntireObjectHookDeps = (0, createRule_1.createRule)({
1339
1381
  if (elementIndex === depsArg.elements.length - 1) {
1340
1382
  const prevElement = depsArg.elements[elementIndex - 1];
1341
1383
  if (prevElement) {
1342
- const range = [
1384
+ return removeCarryingComments(fixer, sourceCode, [
1343
1385
  prevElement.range[1],
1344
1386
  element.range[1],
1345
- ];
1346
- return fixer.removeRange(range);
1387
+ ], 'fromPrevElement');
1347
1388
  }
1348
1389
  }
1349
1390
  // Otherwise, remove the element and the following comma
1350
1391
  const nextElement = depsArg.elements[elementIndex + 1];
1351
1392
  if (nextElement) {
1352
- const range = [
1393
+ return removeCarryingComments(fixer, sourceCode, [
1353
1394
  element.range[0],
1354
1395
  nextElement.range[0],
1355
- ];
1356
- return fixer.removeRange(range);
1396
+ ], 'toNextElement');
1357
1397
  }
1358
1398
  // Fallback to just removing the element
1359
1399
  return fixer.remove(element);
@@ -838,7 +838,18 @@ function calculateImportPath(currentFilePath) {
838
838
  return 'src/util/memo';
839
839
  // Split the current file path into parts and normalize
840
840
  const parts = currentFilePath.split(/[\\/]/); // Handle both Unix and Windows paths
841
- const srcIndex = parts.indexOf('src');
841
+ /**
842
+ * The source root is the `src` NEAREST the file, not the first one in the
843
+ * path. `context.getFilename()` is always absolute under every real ESLint
844
+ * entry point, so the absolute prefix is part of the path being searched: a
845
+ * checkout under a directory named `src` (`~/src/proj/src/components/Foo.tsx`)
846
+ * would otherwise anchor on that outer segment and emit a specifier that walks
847
+ * out of the project entirely.
848
+ *
849
+ * Directory segments only — a file that is itself named `src` names no
850
+ * directory the import could be relative to.
851
+ */
852
+ const srcIndex = parts.slice(0, -1).lastIndexOf('src');
842
853
  if (srcIndex === -1) {
843
854
  // If we're not in a src directory, use absolute path
844
855
  return 'src/util/memo';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.187",
3
+ "version": "1.20.189",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.20.189",
4
+ "date": "2026-08-29T11:57:05.586Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-entire-object-hook-deps",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2208
11
+ ],
12
+ "summary": "carry comments in the removed dep's margin (closes #2208)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.188",
18
+ "date": "2026-08-29T07:11:55.294Z",
19
+ "rules": [
20
+ {
21
+ "name": "require-memo",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 2207
25
+ ],
26
+ "summary": "anchor the memo import on the nearest src, not the first (closes #2207)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.187",
4
32
  "date": "2026-08-28T10:18:26.589Z",