@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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSESLint } from '@typescript-eslint/utils';
|
|
1
2
|
type MessageIds = 'upperSnakeCase' | 'asConst';
|
|
2
|
-
declare const _default:
|
|
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
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
package/release-manifest.json
CHANGED
|
@@ -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",
|