@blumintinc/eslint-plugin-blumint 1.19.8 → 1.19.10
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,50 @@ 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
|
+
// The declaration write reference is the id node itself and
|
|
344
|
+
// is already handled above. Skipping it also avoids emitting
|
|
345
|
+
// overlapping fix ranges, which ESLint rejects.
|
|
346
|
+
if (ref.identifier === idNode) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
fixes.push(fixer.replaceText(ref.identifier, newName));
|
|
229
350
|
}
|
|
351
|
+
return fixes;
|
|
230
352
|
},
|
|
231
353
|
});
|
|
232
354
|
}
|
|
@@ -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);
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,32 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.19.10",
|
|
4
|
+
"date": "2026-07-17T10:33:03.696Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1313
|
|
11
|
+
],
|
|
12
|
+
"summary": "scope-aware upperSnakeCase autofix + exempt jest mock handles (closes #1313)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.19.9",
|
|
18
|
+
"date": "2026-07-17T01:27:12.035Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "no-harness-coupled-disables",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1312
|
|
25
|
+
],
|
|
26
|
+
"summary": "only merge preceding comment when directive defers to it (closes #1312)"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
2
30
|
{
|
|
3
31
|
"version": "1.19.8",
|
|
4
32
|
"date": "2026-07-17T00:25:07.461Z",
|