@blumintinc/eslint-plugin-blumint 1.20.98 → 1.20.100
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
|
@@ -43,6 +43,46 @@ function bindsMemoize(variable) {
|
|
|
43
43
|
ALLOWED_MEMOIZE_MODULES.has(String(declaration.source.value)));
|
|
44
44
|
}));
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* `jest.mock` hoists its module factory above the file's imports;
|
|
48
|
+
* `doMock`/`setMock` register a factory of the same shape at call time. The
|
|
49
|
+
* hoist rejects a factory that reads any out-of-scope binding whose name does
|
|
50
|
+
* not begin with `mock`, which is what puts a module-scope `Memoize` binding —
|
|
51
|
+
* injected or already present — out of reach inside one.
|
|
52
|
+
*/
|
|
53
|
+
const MOCK_REGISTRARS = new Set(['mock', 'doMock', 'setMock']);
|
|
54
|
+
/** Whether the call registers a module factory with `jest`. */
|
|
55
|
+
function isMockRegistrarCall(node) {
|
|
56
|
+
const { callee } = node;
|
|
57
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression || callee.computed) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const { object, property } = callee;
|
|
61
|
+
return (object.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
62
|
+
object.name === 'jest' &&
|
|
63
|
+
property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
64
|
+
MOCK_REGISTRARS.has(property.name));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Whether the node sits inside the factory a jest registrar hoists — the second
|
|
68
|
+
* argument of the call. The module specifier that precedes it is evaluated in
|
|
69
|
+
* place and keeps its access to the file's imports, so only the factory subtree
|
|
70
|
+
* is out of reach.
|
|
71
|
+
*/
|
|
72
|
+
function isInsideMockFactory(node) {
|
|
73
|
+
let child = node;
|
|
74
|
+
let parent = node.parent;
|
|
75
|
+
while (parent) {
|
|
76
|
+
if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
77
|
+
parent.arguments[1] === child &&
|
|
78
|
+
isMockRegistrarCall(parent)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
child = parent;
|
|
82
|
+
parent = parent.parent;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
46
86
|
/**
|
|
47
87
|
* Whether a declared return type annotation promises no value: `void` or
|
|
48
88
|
* `Promise<void>`.
|
|
@@ -297,6 +337,20 @@ exports.enforceMemoizeAsync = (0, createRule_1.createRule)({
|
|
|
297
337
|
if (isReportSuppressed(node)) {
|
|
298
338
|
return null;
|
|
299
339
|
}
|
|
340
|
+
// A jest registrar's factory is hoisted above every import in the
|
|
341
|
+
// file, so the decorator emitted inside one names a binding that
|
|
342
|
+
// does not exist yet: the hoist admits only globals and
|
|
343
|
+
// `mock`-prefixed bindings, and rejects the module at transform
|
|
344
|
+
// time otherwise, taking the whole suite down with it. That holds
|
|
345
|
+
// for an alias or namespace decorator too — those read a
|
|
346
|
+
// module-scope import the factory cannot reach either. Declining
|
|
347
|
+
// here, ahead of the import carrier claim below, leaves the import
|
|
348
|
+
// to a violation that does fix, and leaves the report standing so
|
|
349
|
+
// the author reaches for a remedy the factory can hold, such as
|
|
350
|
+
// decorating the real class the mock stands in for.
|
|
351
|
+
if (isInsideMockFactory(node)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
300
354
|
const fixes = [];
|
|
301
355
|
const sourceCode = context.sourceCode;
|
|
302
356
|
// Determine which identifier to use for the decorator
|
|
@@ -421,14 +421,17 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
421
421
|
if (!declaredVariable) {
|
|
422
422
|
return null;
|
|
423
423
|
}
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
427
|
-
//
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
424
|
+
// An exported binding's name is a cross-file contract: every
|
|
425
|
+
// importer spells it out in a file this single-file fixer
|
|
426
|
+
// cannot reach, so renaming the declaration breaks them all
|
|
427
|
+
// (TS2724/TS2305, an unresolved JSX element, a `jest.mock`
|
|
428
|
+
// factory key). The hazard lives entirely in those other files,
|
|
429
|
+
// so it does not depend on whether the declaring file also uses
|
|
430
|
+
// the name — a constants module with no local use sites is the
|
|
431
|
+
// most exposed shape, not the safest. Report-only; the sibling
|
|
432
|
+
// `as const` fix still applies because it never touches the
|
|
433
|
+
// export name.
|
|
434
|
+
if (isExported) {
|
|
432
435
|
return null;
|
|
433
436
|
}
|
|
434
437
|
// Suppress the fix when `newName` already binds something in a
|
|
@@ -20,69 +20,38 @@ function isKnownHookCallee(callee, knownHooks, assumeAllUseAreMemoized) {
|
|
|
20
20
|
}
|
|
21
21
|
return false;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
const EVENT_SUPPRESSION_METHODS = new Set([
|
|
24
|
+
'preventDefault',
|
|
25
|
+
'stopPropagation',
|
|
26
|
+
'stopImmediatePropagation',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* A wrapper that suppresses the event carries behaviour the rule's own remedy
|
|
30
|
+
* cannot preserve: passing the memoized callback directly both drops the
|
|
31
|
+
* suppression call and hands React's event to a callback that took no
|
|
32
|
+
* arguments. Such a wrapper is not redundant, so the receiver is deliberately
|
|
33
|
+
* unconstrained — deleting `x.preventDefault()` changes behaviour whether `x`
|
|
34
|
+
* is a parameter, a captured value or a nested member.
|
|
35
|
+
*/
|
|
36
|
+
function isEventSuppressionCall(stmt) {
|
|
24
37
|
if (stmt.type !== utils_1.AST_NODE_TYPES.ExpressionStatement)
|
|
25
38
|
return false;
|
|
26
|
-
const expr = stmt.expression;
|
|
27
|
-
if (expr.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
39
|
+
const expr = unwrapChainExpression(stmt.expression);
|
|
40
|
+
if (!expr || expr.type !== utils_1.AST_NODE_TYPES.CallExpression)
|
|
28
41
|
return false;
|
|
29
|
-
|
|
42
|
+
const callee = unwrapChainExpression(expr.callee);
|
|
43
|
+
if (!callee)
|
|
30
44
|
return false;
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
member.property.type === utils_1.AST_NODE_TYPES.Identifier &&
|
|
35
|
-
(member.property.name === 'preventDefault' ||
|
|
36
|
-
member.property.name === 'stopPropagation' ||
|
|
37
|
-
member.property.name === 'stopImmediatePropagation')) {
|
|
38
|
-
return true;
|
|
45
|
+
// A destructured `({ preventDefault })` reaches the method without a receiver.
|
|
46
|
+
if (callee.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
47
|
+
return EVENT_SUPPRESSION_METHODS.has(callee.name);
|
|
39
48
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
for (const p of node.params) {
|
|
45
|
-
if (p.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
46
|
-
names.add(p.name);
|
|
47
|
-
}
|
|
48
|
-
else if (p.type === utils_1.AST_NODE_TYPES.ObjectPattern) {
|
|
49
|
-
for (const prop of p.properties) {
|
|
50
|
-
if (prop.type === utils_1.AST_NODE_TYPES.Property) {
|
|
51
|
-
// Collect bound identifier names (aliases/defaults)
|
|
52
|
-
if (prop.value.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
53
|
-
names.add(prop.value.name);
|
|
54
|
-
}
|
|
55
|
-
else if (prop.value.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
56
|
-
prop.value.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
57
|
-
names.add(prop.value.left.name);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
else if (prop.type === utils_1.AST_NODE_TYPES.RestElement) {
|
|
61
|
-
if (prop.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
62
|
-
names.add(prop.argument.name);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
else if (p.type === utils_1.AST_NODE_TYPES.ArrayPattern) {
|
|
68
|
-
for (const element of p.elements) {
|
|
69
|
-
if (!element)
|
|
70
|
-
continue;
|
|
71
|
-
if (element.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
72
|
-
names.add(element.name);
|
|
73
|
-
}
|
|
74
|
-
else if (element.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
|
|
75
|
-
element.left.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
76
|
-
names.add(element.left.name);
|
|
77
|
-
}
|
|
78
|
-
else if (element.type === utils_1.AST_NODE_TYPES.RestElement &&
|
|
79
|
-
element.argument.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
80
|
-
names.add(element.argument.name);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
49
|
+
if (callee.type === utils_1.AST_NODE_TYPES.MemberExpression &&
|
|
50
|
+
!callee.computed &&
|
|
51
|
+
callee.property.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
52
|
+
return EVENT_SUPPRESSION_METHODS.has(callee.property.name);
|
|
84
53
|
}
|
|
85
|
-
return
|
|
54
|
+
return false;
|
|
86
55
|
}
|
|
87
56
|
function isIdentifierOrMemberOn(obj, nameSet) {
|
|
88
57
|
if (obj.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
@@ -224,7 +193,6 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
224
193
|
(unwrappedArg.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
225
194
|
unwrappedArg.type === utils_1.AST_NODE_TYPES.FunctionExpression)) {
|
|
226
195
|
const fn = unwrappedArg;
|
|
227
|
-
const params = getParams(fn);
|
|
228
196
|
// Handle implicit return: () => memoizedFn()
|
|
229
197
|
if (fn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
230
198
|
fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
@@ -263,21 +231,25 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
|
|
|
263
231
|
}
|
|
264
232
|
return;
|
|
265
233
|
}
|
|
266
|
-
// Handle block body: () => {
|
|
234
|
+
// Handle block body: () => { return memoizedFn(); }
|
|
267
235
|
if (fn.body && fn.body.type === utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
268
236
|
const stmts = fn.body.body.filter(Boolean);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
237
|
+
// An event-suppression call disqualifies the wrapper outright: no
|
|
238
|
+
// spelling of "pass the callback directly" keeps it, so reporting
|
|
239
|
+
// here would prescribe a remedy that does not exist.
|
|
240
|
+
if (stmts.some(isEventSuppressionCall))
|
|
241
|
+
return;
|
|
242
|
+
// Exactly one statement. A wrapper that sequences a second call is
|
|
243
|
+
// doing work the delegate alone does not, so collapsing it would
|
|
244
|
+
// drop that call — only the branch's own statement is ever read,
|
|
245
|
+
// so a wider count silently discards whatever it did not look at.
|
|
246
|
+
if (stmts.length === 1) {
|
|
247
|
+
const first = stmts[0];
|
|
248
|
+
if (first.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
|
|
249
|
+
first.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
|
|
250
|
+
const expr = first.type === utils_1.AST_NODE_TYPES.ReturnStatement
|
|
251
|
+
? first.argument
|
|
252
|
+
: first.expression;
|
|
281
253
|
if (expr && expr.type === utils_1.AST_NODE_TYPES.CallExpression) {
|
|
282
254
|
const callee = unwrapChainExpression(expr.callee);
|
|
283
255
|
const isHookProp = callee &&
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.100",
|
|
4
|
+
"date": "2026-08-04T13:28:51.191Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "global-const-style",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1700
|
|
11
|
+
],
|
|
12
|
+
"summary": "withhold the rename for every exported declaration (closes #1700)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.99",
|
|
18
|
+
"date": "2026-08-04T12:38:33.934Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-memoize-async",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1697
|
|
25
|
+
],
|
|
26
|
+
"summary": "decline the decorator inside a jest.mock factory (closes #1697)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "no-redundant-usecallback-wrapper",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1696,
|
|
33
|
+
1699
|
|
34
|
+
],
|
|
35
|
+
"summary": "require a single-statement body before collapsing (closes #1699); treat event suppression as disqualifying, not skippable (closes #1696)"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
},
|
|
2
39
|
{
|
|
3
40
|
"version": "1.20.98",
|
|
4
41
|
"date": "2026-08-04T10:58:56.821Z",
|