@blumintinc/eslint-plugin-blumint 1.20.89 → 1.20.90

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.89',
226
+ version: '1.20.90',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -78,6 +78,46 @@ function nearestManifestDeclaresModule(startDir) {
78
78
  }
79
79
  }
80
80
  }
81
+ /**
82
+ * `jest.mock` hoists its module factory above the file's imports;
83
+ * `doMock`/`setMock` register a factory of the same shape at call time. The
84
+ * hoist rejects a factory that reads any out-of-scope binding whose name does
85
+ * not begin with `mock`, which is what puts the injected `assertSafe` import
86
+ * out of reach inside one.
87
+ */
88
+ const MOCK_REGISTRARS = new Set(['mock', 'doMock', 'setMock']);
89
+ /** Whether the call registers a module factory with `jest`. */
90
+ function isMockRegistrarCall(node) {
91
+ const { callee } = node;
92
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression || callee.computed) {
93
+ return false;
94
+ }
95
+ const { object, property } = callee;
96
+ return (object.type === utils_1.AST_NODE_TYPES.Identifier &&
97
+ object.name === 'jest' &&
98
+ property.type === utils_1.AST_NODE_TYPES.Identifier &&
99
+ MOCK_REGISTRARS.has(property.name));
100
+ }
101
+ /**
102
+ * Whether the node sits inside the factory a jest registrar hoists — the second
103
+ * argument of the call. The module specifier that precedes it is evaluated in
104
+ * place and keeps its access to the file's imports, so only the factory subtree
105
+ * is out of reach.
106
+ */
107
+ function isInsideMockFactory(node) {
108
+ let child = node;
109
+ let parent = node.parent;
110
+ while (parent) {
111
+ if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
112
+ parent.arguments[1] === child &&
113
+ isMockRegistrarCall(parent)) {
114
+ return true;
115
+ }
116
+ child = parent;
117
+ parent = parent.parent;
118
+ }
119
+ return false;
120
+ }
81
121
  /** Names that read as a positional sequence rather than a keyed record. */
82
122
  const ARRAY_LIKE_NAME = /^(array|arr|items|elements|list|collection|data)s?$/i;
83
123
  /**
@@ -365,6 +405,18 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
365
405
  if (isReportSuppressed(node)) {
366
406
  return null;
367
407
  }
408
+ // A jest registrar's factory is hoisted above every import in the
409
+ // file, so the injected `import { assertSafe }` binds too late for
410
+ // the emitted call: the hoist allows a factory to read only globals
411
+ // and `mock`-prefixed bindings, and rejects the module at transform
412
+ // time otherwise, taking the whole suite down with it. Declining —
413
+ // and leaving the import unclaimed for a violation that does fix —
414
+ // keeps the report standing so the author reaches for a remedy the
415
+ // factory can hold, such as `jest.requireActual` inside it or a
416
+ // `mock`-prefixed import alias.
417
+ if (isInsideMockFactory(node)) {
418
+ return null;
419
+ }
368
420
  // Resolve `assertSafe` through the scope chain at the fixed node. A
369
421
  // binding that is not the helper import breaks the edit two ways: the
370
422
  // injected import collides with a module-scope declaration (TS2440,
@@ -59,6 +59,52 @@ function bindsReactFragment(variable) {
59
59
  declaration.source.value === REACT_MODULE);
60
60
  }));
61
61
  }
62
+ /**
63
+ * `jest.mock` hoists its module factory above the file's imports;
64
+ * `doMock`/`setMock` register a factory of the same shape at call time. The
65
+ * hoist rejects a factory that reads any out-of-scope binding whose name does
66
+ * not begin with `mock`, which is what puts the injected `Fragment` import out
67
+ * of reach inside one: the module fails at transform time
68
+ * (`Invalid variable access: Fragment`) and takes the whole suite down with it.
69
+ * The shorthand `<>` the rule rewrites away is the spelling that works there.
70
+ *
71
+ * The report still fires, because remedies the factory can hold exist —
72
+ * `const { Fragment } = jest.requireActual('react')` inside it, or a
73
+ * `mock`-prefixed import alias — and only the fix is withheld.
74
+ */
75
+ const MOCK_REGISTRARS = new Set(['mock', 'doMock', 'setMock']);
76
+ /** Whether the call registers a module factory with `jest`. */
77
+ function isMockRegistrarCall(node) {
78
+ const { callee } = node;
79
+ if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression || callee.computed) {
80
+ return false;
81
+ }
82
+ const { object, property } = callee;
83
+ return (object.type === utils_1.AST_NODE_TYPES.Identifier &&
84
+ object.name === 'jest' &&
85
+ property.type === utils_1.AST_NODE_TYPES.Identifier &&
86
+ MOCK_REGISTRARS.has(property.name));
87
+ }
88
+ /**
89
+ * Whether the node sits inside the factory a jest registrar hoists — the second
90
+ * argument of the call. The module specifier that precedes it is evaluated in
91
+ * place and keeps its access to the file's imports, so only the factory subtree
92
+ * is out of reach.
93
+ */
94
+ function isInsideMockFactory(node) {
95
+ let child = node;
96
+ let parent = node.parent;
97
+ while (parent) {
98
+ if (parent.type === utils_1.AST_NODE_TYPES.CallExpression &&
99
+ parent.arguments[1] === child &&
100
+ isMockRegistrarCall(parent)) {
101
+ return true;
102
+ }
103
+ child = parent;
104
+ parent = parent.parent;
105
+ }
106
+ return false;
107
+ }
62
108
  exports.preferFragmentComponent = (0, createRule_1.createRule)({
63
109
  name: 'prefer-fragment-component',
64
110
  meta: {
@@ -227,6 +273,13 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
227
273
  if (isReportSuppressed(node)) {
228
274
  return null;
229
275
  }
276
+ // A hoisted jest factory cannot reach the injected import, so
277
+ // the rewrite is withheld inside one. Checking the inner
278
+ // fragment covers the outer React.Fragment as well, since a
279
+ // parent of a node in the factory is in the factory too.
280
+ if (isInsideMockFactory(node)) {
281
+ return null;
282
+ }
230
283
  // Both tags are rewritten to the same bare name, so resolving at
231
284
  // the inner fragment — the deeper of the two scopes — covers the
232
285
  // outer one too. Every bail precedes the latch so a withheld edit
@@ -294,6 +347,11 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
294
347
  if (isReportSuppressed(node)) {
295
348
  return null;
296
349
  }
350
+ // A hoisted jest factory cannot reach the injected import, so the
351
+ // rewrite is withheld inside one.
352
+ if (isInsideMockFactory(node)) {
353
+ return null;
354
+ }
297
355
  // Every bail precedes the latch so a withheld edit cannot make a
298
356
  // later fix believe the import is already handled.
299
357
  if (!resolvesToReactFragment(node)) {
@@ -350,6 +408,11 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
350
408
  if (isReportSuppressed(node.name)) {
351
409
  return null;
352
410
  }
411
+ // A hoisted jest factory cannot reach the injected import, so
412
+ // the rewrite is withheld inside one.
413
+ if (isInsideMockFactory(node)) {
414
+ return null;
415
+ }
353
416
  // Every bail precedes the latch so a withheld edit cannot make a
354
417
  // later fix believe the import is already handled.
355
418
  if (!resolvesToReactFragment(node)) {
@@ -391,6 +454,11 @@ exports.preferFragmentComponent = (0, createRule_1.createRule)({
391
454
  if (isReportSuppressed(node.name)) {
392
455
  return null;
393
456
  }
457
+ // A hoisted jest factory cannot reach the injected import, so the
458
+ // rewrite is withheld inside one.
459
+ if (isInsideMockFactory(node)) {
460
+ return null;
461
+ }
394
462
  // Every bail precedes the latch so a withheld edit cannot make a
395
463
  // later fix believe the import is already handled.
396
464
  if (!resolvesToReactFragment(node)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.89",
3
+ "version": "1.20.90",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.90",
4
+ "date": "2026-08-03T13:37:18.271Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-assert-safe-object-key",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1659
11
+ ],
12
+ "summary": "decline the fix inside a jest mock factory (closes #1659)"
13
+ },
14
+ {
15
+ "name": "prefer-fragment-component",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1660
19
+ ],
20
+ "summary": "decline the fix inside a jest mock factory (closes #1660)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.89",
4
26
  "date": "2026-08-03T12:31:01.823Z",