@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
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.98',
226
+ version: '1.20.99',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -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
- function isPreventDefaultCall(stmt, params) {
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
- if (expr.callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
42
+ const callee = unwrapChainExpression(expr.callee);
43
+ if (!callee)
30
44
  return false;
31
- const member = expr.callee;
32
- if (member.object.type === utils_1.AST_NODE_TYPES.Identifier &&
33
- params.includes(member.object.name) &&
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
- return false;
41
- }
42
- function getParams(node) {
43
- const names = new Set();
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 Array.from(names);
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: () => { [maybe e.preventDefault()]; return memoizedFn(); }
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
- if (stmts.length >= 1 && stmts.length <= 2) {
270
- let idx = 0;
271
- if (stmts.length === 2 &&
272
- isPreventDefaultCall(stmts[0], params)) {
273
- idx = 1;
274
- }
275
- const last = stmts[idx];
276
- if (last.type === utils_1.AST_NODE_TYPES.ReturnStatement ||
277
- last.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
278
- const expr = last.type === utils_1.AST_NODE_TYPES.ReturnStatement
279
- ? last.argument
280
- : last.expression;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.98",
3
+ "version": "1.20.99",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",