@blumintinc/eslint-plugin-blumint 1.19.9 → 1.19.11

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.9',
225
+ version: '1.19.11',
226
226
  },
227
227
  parseOptions: {
228
228
  ecmaVersion: 2020,
@@ -1,3 +1,4 @@
1
+ import { TSESLint } from '@typescript-eslint/utils';
1
2
  type MessageIds = 'upperSnakeCase' | 'asConst';
2
- declare const _default: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<MessageIds, [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
3
+ declare const _default: TSESLint.RuleModule<MessageIds, [], TSESLint.RuleListener>;
3
4
  export default _default;
@@ -3,6 +3,86 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const utils_1 = require("@typescript-eslint/utils");
4
4
  const createRule_1 = require("../utils/createRule");
5
5
  const isUpperSnakeCase = (str) => /^[A-Z][A-Z0-9_]*$/.test(str);
6
+ // Jest mock handles produced by an `as` cast to a `jest.Mock*` type are
7
+ // stateful test doubles that are reassigned/mutated through
8
+ // `.mockImplementation()`, `.mockReturnValue()`, etc. They are not immutable
9
+ // module configuration, and the `mockedX` camelCase spelling is the established
10
+ // idiom, so they are exempt from the UPPER_SNAKE_CASE rename requirement.
11
+ const JEST_MOCK_TYPE_NAMES = new Set([
12
+ 'Mock',
13
+ 'MockedFunction',
14
+ 'Mocked',
15
+ 'MockedClass',
16
+ ]);
17
+ // Match `expr as jest.Mock<...>` / `jest.MockedFunction<...>` /
18
+ // `jest.Mocked<...>` / `jest.MockedClass<...>`. The match is kept deliberately
19
+ // narrow — a qualified `jest.<MockType>` type reference — so unrelated `as`
20
+ // casts keep triggering the rename check.
21
+ const isJestMockCast = (node) => {
22
+ if (node.type !== utils_1.AST_NODE_TYPES.TSAsExpression) {
23
+ return false;
24
+ }
25
+ const typeAnnotation = node.typeAnnotation;
26
+ if (typeAnnotation.type !== utils_1.AST_NODE_TYPES.TSTypeReference) {
27
+ return false;
28
+ }
29
+ const { typeName } = typeAnnotation;
30
+ return (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName &&
31
+ typeName.left.type === utils_1.AST_NODE_TYPES.Identifier &&
32
+ typeName.left.name === 'jest' &&
33
+ typeName.right.type === utils_1.AST_NODE_TYPES.Identifier &&
34
+ JEST_MOCK_TYPE_NAMES.has(typeName.right.name));
35
+ };
36
+ /**
37
+ * Walks the scope chain upward from `scope` (inclusive) and reports whether
38
+ * `targetName` is bound anywhere between `scope` and `stopScope` (inclusive).
39
+ * Mirrors how the engine resolves an identifier at a use site: the first scope
40
+ * on the chain that declares the name wins. Used to detect whether a rewritten
41
+ * reference would be captured by a binding sitting between it and the
42
+ * declaration it currently resolves to.
43
+ */
44
+ const isNameBoundInChain = (scope, stopScope, targetName) => {
45
+ let current = scope;
46
+ while (current) {
47
+ if (current.set.has(targetName)) {
48
+ return true;
49
+ }
50
+ if (current === stopScope) {
51
+ break;
52
+ }
53
+ current = current.upper;
54
+ }
55
+ return false;
56
+ };
57
+ /**
58
+ * Returns true when renaming `variable` to `newName` would collide with an
59
+ * existing binding in any scope the rename touches, making the autofix
60
+ * semantics-changing (and thus unsafe). The rename fixer rewrites the
61
+ * declaration plus every in-file reference to `newName`; if `newName` already
62
+ * resolves to a different binding the rewrite would either redeclare a name
63
+ * already bound in the declaration scope or capture a reference onto an
64
+ * intervening binding. In every such case the fix is suppressed (report-only).
65
+ */
66
+ const renameWouldCollide = (variable, newName) => {
67
+ const declarationScope = variable.scope;
68
+ // (1) Declaration site: `newName` already bound in the scope that holds the
69
+ // declaration would make the rename a redeclaration/shadow. The declared
70
+ // variable itself carries the old name, so any entry for `newName` is a
71
+ // distinct, colliding binding.
72
+ if (declarationScope.set.has(newName)) {
73
+ return true;
74
+ }
75
+ // (2) Reference sites: a binding of `newName` sitting between a reference and
76
+ // the declaration scope would swallow the rewritten identifier — the
77
+ // reference would resolve to that binding instead of the constant.
78
+ for (const ref of variable.references) {
79
+ const referenceScope = ref.from ?? declarationScope;
80
+ if (isNameBoundInChain(referenceScope, declarationScope, newName)) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ };
6
86
  // Next.js recognizes these export names by their literal identifier, so
7
87
  // renaming them to UPPER_SNAKE_CASE silently breaks the framework contract
8
88
  // (e.g. `export const config` controls the API-route body parser / runtime).
@@ -207,12 +287,16 @@ exports.default = (0, createRule_1.createRule)({
207
287
  if (isExported && NEXTJS_RESERVED_EXPORTS.has(name)) {
208
288
  return;
209
289
  }
210
- // Check for UPPER_SNAKE_CASE
211
- if (!isUpperSnakeCase(name)) {
290
+ // Check for UPPER_SNAKE_CASE. Jest mock handles (`x as jest.Mock<…>`)
291
+ // are exempt: they are mutable test doubles, not immutable config, so
292
+ // the `mockedX` idiom is intentional. The exemption gates only this
293
+ // rename check — the `as const` logic above is untouched.
294
+ if (!isUpperSnakeCase(name) && !isJestMockCast(init)) {
212
295
  const newName = name
213
296
  .replace(/([A-Z])/g, '_$1')
214
297
  .toUpperCase()
215
298
  .replace(/^_/, '');
299
+ const idNode = declaration.id;
216
300
  context.report({
217
301
  node: declaration,
218
302
  messageId: 'upperSnakeCase',
@@ -221,12 +305,71 @@ exports.default = (0, createRule_1.createRule)({
221
305
  suggestedName: newName,
222
306
  },
223
307
  fix(fixer) {
224
- if (typeAnnotation) {
225
- return fixer.replaceText(declaration, `${newName}${typeText} = ${initText}`);
308
+ // Resolve the declared variable so the rename can rewrite the
309
+ // declaration AND every reference together. Renaming only the
310
+ // declaration id (the previous behavior) left every use site
311
+ // bound to a now-undefined name — `--fix` exited 0 while
312
+ // silently corrupting working code (Issue #1313, same defect
313
+ // class as #1256).
314
+ const declaredVariable = context
315
+ .getDeclaredVariables(declaration)
316
+ .find((variable) => variable.name === name) ?? null;
317
+ // Cannot resolve the variable — never emit a partial rename.
318
+ if (!declaredVariable) {
319
+ return null;
320
+ }
321
+ // Exported symbols with in-file use sites are cross-file
322
+ // contracts whose importers a single-file fixer cannot reach;
323
+ // rewriting the local sites alone would still leave the export
324
+ // renamed and importers broken. Report-only. (A bare exported
325
+ // declaration with no extra references keeps the historical
326
+ // rename behavior — nothing to orphan in-file.)
327
+ const hasExtraReferences = declaredVariable.references.some((ref) => ref.identifier !== idNode);
328
+ if (isExported && hasExtraReferences) {
329
+ return null;
330
+ }
331
+ // Suppress the fix when `newName` already binds something in a
332
+ // scope the rename would touch — a rename fixer must never
333
+ // change program semantics or shadow an existing binding.
334
+ if (renameWouldCollide(declaredVariable, newName)) {
335
+ return null;
226
336
  }
227
- else {
228
- return fixer.replaceText(declaration.id, newName);
337
+ // Rewrite the declaration id (preserving any type annotation,
338
+ // whose range is part of the id node) plus every reference.
339
+ const fixes = [
340
+ fixer.replaceText(idNode, typeAnnotation ? `${newName}${typeText}` : newName),
341
+ ];
342
+ for (const ref of declaredVariable.references) {
343
+ const refId = ref.identifier;
344
+ // The declaration write reference is the id node itself and
345
+ // is already handled above. Skipping it also avoids emitting
346
+ // overlapping fix ranges, which ESLint rejects.
347
+ if (refId === idNode) {
348
+ continue;
349
+ }
350
+ const refParent = refId.parent;
351
+ // An object-literal shorthand `{ fooBar }` desugars to
352
+ // `{ fooBar: fooBar }`: the one token is both the property key
353
+ // and its value. Rewriting it to `{ FOO_BAR }` would rename
354
+ // the KEY too, silently changing the object's shape. Expand to
355
+ // `oldKey: NEW_NAME` so only the value is renamed.
356
+ if (refParent?.type === utils_1.AST_NODE_TYPES.Property &&
357
+ refParent.shorthand &&
358
+ refParent.parent?.type === utils_1.AST_NODE_TYPES.ObjectExpression) {
359
+ fixes.push(fixer.replaceText(refId, `${name}: ${newName}`));
360
+ continue;
361
+ }
362
+ // A re-export specifier `export { fooBar }` binds the public
363
+ // export name to this identifier. Renaming it would change the
364
+ // exported name — a cross-file contract a single-file fixer
365
+ // cannot safely rewrite (the declaration-level export guard
366
+ // above only catches inline `export const`). Decline the fix.
367
+ if (refParent?.type === utils_1.AST_NODE_TYPES.ExportSpecifier) {
368
+ return null;
369
+ }
370
+ fixes.push(fixer.replaceText(refId, newName));
229
371
  }
372
+ return fixes;
230
373
  },
231
374
  });
232
375
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.19.9",
3
+ "version": "1.19.11",
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.19.11",
4
+ "date": "2026-07-17T10:46:20.593Z",
5
+ "rules": [
6
+ {
7
+ "name": "global-const-style",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1313
11
+ ],
12
+ "summary": "keep rename autofix reference-safe for shorthand props and re-exports (refs #1313)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.19.10",
18
+ "date": "2026-07-17T10:33:03.696Z",
19
+ "rules": [
20
+ {
21
+ "name": "global-const-style",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1313
25
+ ],
26
+ "summary": "scope-aware upperSnakeCase autofix + exempt jest mock handles (closes #1313)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.19.9",
4
32
  "date": "2026-07-17T01:27:12.035Z",