@blumintinc/eslint-plugin-blumint 1.20.121 → 1.20.123

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.121',
226
+ version: '1.20.123',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -8,6 +8,36 @@ const FIRESTORE_PATHS = [
8
8
  'firebase-admin',
9
9
  'firebase-admin/firestore',
10
10
  ];
11
+ /**
12
+ * A `jest.mock` factory produces the same module shape whether it is written as
13
+ * a concise arrow, a block-bodied arrow, or a `function` expression. Matching
14
+ * the factory argument itself would only ever see the concise arrow, letting an
15
+ * identical mock evade the rule on a body-form choice alone. Resolving to the
16
+ * produced object keeps every spelling on one matching path.
17
+ *
18
+ * A body with more than a lone `return` is deliberately unresolved: the object
19
+ * reaching the caller can no longer be read off a single expression.
20
+ */
21
+ const resolveFactoryReturn = (factory) => {
22
+ if (!factory ||
23
+ (factory.type !== utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
24
+ factory.type !== utils_1.AST_NODE_TYPES.FunctionExpression)) {
25
+ return undefined;
26
+ }
27
+ // A concise arrow body is the produced expression itself. Parentheses around
28
+ // an object body are not part of the AST, so the node is matched directly.
29
+ if (factory.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
30
+ return factory.body;
31
+ }
32
+ const statements = factory.body.body;
33
+ if (statements.length !== 1) {
34
+ return undefined;
35
+ }
36
+ const [statement] = statements;
37
+ return statement.type === utils_1.AST_NODE_TYPES.ReturnStatement
38
+ ? statement.argument ?? undefined
39
+ : undefined;
40
+ };
11
41
  exports.enforceFirestoreMock = (0, createRule_1.createRule)({
12
42
  name: 'enforce-mock-firestore',
13
43
  meta: {
@@ -39,21 +69,18 @@ exports.enforceFirestoreMock = (0, createRule_1.createRule)({
39
69
  typeof node.arguments[0].value === 'string' &&
40
70
  node.arguments[0].value?.includes(path))) {
41
71
  // Check if the mock includes Firestore-related properties
42
- const mockFn = node.arguments[1];
43
- if (mockFn &&
44
- mockFn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression) {
45
- const returnStmt = mockFn.body;
46
- if (returnStmt.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
47
- returnStmt.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
48
- prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
49
- (prop.key.name === 'db' ||
50
- prop.key.name === 'firestore' ||
51
- prop.key.name === 'getFirestore'))) {
52
- context.report({
53
- node,
54
- messageId: 'noManualFirestoreMock',
55
- });
56
- }
72
+ const mockedModule = resolveFactoryReturn(node.arguments[1]);
73
+ if (mockedModule &&
74
+ mockedModule.type === utils_1.AST_NODE_TYPES.ObjectExpression &&
75
+ mockedModule.properties.some((prop) => prop.type === utils_1.AST_NODE_TYPES.Property &&
76
+ prop.key.type === utils_1.AST_NODE_TYPES.Identifier &&
77
+ (prop.key.name === 'db' ||
78
+ prop.key.name === 'firestore' ||
79
+ prop.key.name === 'getFirestore'))) {
80
+ context.report({
81
+ node,
82
+ messageId: 'noManualFirestoreMock',
83
+ });
57
84
  }
58
85
  }
59
86
  },
@@ -103,11 +103,17 @@ const EVENT_SUPPRESSION_METHODS = new Set([
103
103
  * arguments. Such a wrapper is not redundant, so the receiver is deliberately
104
104
  * unconstrained — deleting `x.preventDefault()` changes behaviour whether `x`
105
105
  * is a parameter, a captured value or a nested member.
106
+ *
107
+ * The question is asked of the call itself rather than of a statement, because
108
+ * the behaviour that makes the wrapper load-bearing is identical whether the
109
+ * author spelled it as a concise arrow body, a `return` or an expression
110
+ * statement — and a predicate that recognizes only one of those spellings
111
+ * reports the other two while prescribing a remedy that does not exist.
106
112
  */
107
- function isEventSuppressionCall(stmt) {
108
- if (stmt.type !== utils_1.AST_NODE_TYPES.ExpressionStatement)
113
+ function isEventSuppressionCall(expression) {
114
+ if (!expression)
109
115
  return false;
110
- const expr = unwrapChainExpression(stmt.expression);
116
+ const expr = unwrapChainExpression(expression);
111
117
  if (!expr || expr.type !== utils_1.AST_NODE_TYPES.CallExpression)
112
118
  return false;
113
119
  const callee = unwrapChainExpression(expr.callee);
@@ -124,6 +130,19 @@ function isEventSuppressionCall(stmt) {
124
130
  }
125
131
  return false;
126
132
  }
133
+ /**
134
+ * The statement carrying a wrapper's effective call, so the suppression test
135
+ * reads the same expression from either statement kind.
136
+ */
137
+ function isEventSuppressionStatement(stmt) {
138
+ if (stmt.type === utils_1.AST_NODE_TYPES.ExpressionStatement) {
139
+ return isEventSuppressionCall(stmt.expression);
140
+ }
141
+ if (stmt.type === utils_1.AST_NODE_TYPES.ReturnStatement) {
142
+ return isEventSuppressionCall(stmt.argument);
143
+ }
144
+ return false;
145
+ }
127
146
  function isIdentifierOrMemberOn(obj, nameSet) {
128
147
  if (obj.type === utils_1.AST_NODE_TYPES.Identifier) {
129
148
  return nameSet.has(obj.name);
@@ -379,6 +398,11 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
379
398
  // Handle implicit return: () => memoizedFn()
380
399
  if (fn.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression &&
381
400
  fn.body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
401
+ // An event-suppression call disqualifies the wrapper outright: no
402
+ // spelling of "pass the callback directly" keeps it, so reporting
403
+ // here would prescribe a remedy that does not exist.
404
+ if (isEventSuppressionCall(fn.body))
405
+ return;
382
406
  const bodyExpr = unwrapChainExpression(fn.body);
383
407
  if (bodyExpr && bodyExpr.type === utils_1.AST_NODE_TYPES.CallExpression) {
384
408
  const callee = unwrapChainExpression(bodyExpr.callee);
@@ -426,8 +450,10 @@ exports.noRedundantUseCallbackWrapper = (0, createRule_1.createRule)({
426
450
  const stmts = fn.body.body.filter(Boolean);
427
451
  // An event-suppression call disqualifies the wrapper outright: no
428
452
  // spelling of "pass the callback directly" keeps it, so reporting
429
- // here would prescribe a remedy that does not exist.
430
- if (stmts.some(isEventSuppressionCall))
453
+ // here would prescribe a remedy that does not exist. Any statement
454
+ // carrying one is enough — the call is load-bearing wherever in
455
+ // the body it sits.
456
+ if (stmts.some(isEventSuppressionStatement))
431
457
  return;
432
458
  // Exactly one statement. A wrapper that sequences a second call is
433
459
  // doing work the delegate alone does not, so collapsing it would
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.121",
3
+ "version": "1.20.123",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,32 @@
1
1
  [
2
+ {
3
+ "version": "1.20.123",
4
+ "date": "2026-08-06T11:53:46.588Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-mock-firestore",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1798
11
+ ],
12
+ "summary": "resolve the jest.mock factory's returned object in every body form (closes #1798)"
13
+ }
14
+ ]
15
+ },
16
+ {
17
+ "version": "1.20.122",
18
+ "date": "2026-08-06T11:06:53.595Z",
19
+ "rules": [
20
+ {
21
+ "name": "no-redundant-usecallback-wrapper",
22
+ "changeType": "fix",
23
+ "issues": [
24
+ 1796
25
+ ],
26
+ "summary": "exempt a suppression wrapper in every body spelling (closes #1796)"
27
+ }
28
+ ]
29
+ },
2
30
  {
3
31
  "version": "1.20.121",
4
32
  "date": "2026-08-06T10:37:08.150Z",