@blumintinc/eslint-plugin-blumint 1.20.98 → 1.20.99
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
|
|
@@ -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,27 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.99",
|
|
4
|
+
"date": "2026-08-04T12:38:33.934Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-memoize-async",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1697
|
|
11
|
+
],
|
|
12
|
+
"summary": "decline the decorator inside a jest.mock factory (closes #1697)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "no-redundant-usecallback-wrapper",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1696,
|
|
19
|
+
1699
|
|
20
|
+
],
|
|
21
|
+
"summary": "require a single-statement body before collapsing (closes #1699); treat event suppression as disqualifying, not skippable (closes #1696)"
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
},
|
|
2
25
|
{
|
|
3
26
|
"version": "1.20.98",
|
|
4
27
|
"date": "2026-08-04T10:58:56.821Z",
|