@atlaskit/eslint-plugin-platform 2.7.2 → 2.9.0

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.
Files changed (85) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/cjs/index.js +19 -3
  3. package/dist/cjs/rules/ensure-use-sync-external-store-server-snapshot/index.js +41 -0
  4. package/dist/cjs/rules/feature-gating/valid-gate-name/index.js +60 -0
  5. package/dist/cjs/rules/import/no-barrel-entry-imports/index.js +1279 -0
  6. package/dist/cjs/rules/import/no-barrel-entry-jest-mock/index.js +1659 -0
  7. package/dist/cjs/rules/import/no-conversation-assistant-barrel-imports/index.js +43 -0
  8. package/dist/cjs/rules/import/no-jest-mock-barrel-files/index.js +1402 -0
  9. package/dist/cjs/rules/import/no-relative-barrel-file-imports/index.js +781 -0
  10. package/dist/cjs/rules/import/shared/barrel-parsing.js +511 -0
  11. package/dist/cjs/rules/import/shared/file-system.js +186 -0
  12. package/dist/cjs/rules/import/shared/jest-utils.js +244 -0
  13. package/dist/cjs/rules/import/shared/package-registry.js +263 -0
  14. package/dist/cjs/rules/import/shared/package-resolution.js +318 -0
  15. package/dist/cjs/rules/import/shared/perf.js +89 -0
  16. package/dist/cjs/rules/import/shared/types.js +67 -0
  17. package/dist/cjs/rules/visit-example-type-import-required/index.js +409 -0
  18. package/dist/es2019/index.js +19 -3
  19. package/dist/es2019/rules/ensure-use-sync-external-store-server-snapshot/index.js +43 -0
  20. package/dist/es2019/rules/feature-gating/valid-gate-name/index.js +52 -0
  21. package/dist/es2019/rules/import/no-barrel-entry-imports/index.js +1158 -0
  22. package/dist/es2019/rules/import/no-barrel-entry-jest-mock/index.js +1341 -0
  23. package/dist/es2019/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
  24. package/dist/es2019/rules/import/no-jest-mock-barrel-files/index.js +1180 -0
  25. package/dist/es2019/rules/import/no-relative-barrel-file-imports/index.js +742 -0
  26. package/dist/es2019/rules/import/shared/barrel-parsing.js +433 -0
  27. package/dist/es2019/rules/import/shared/file-system.js +174 -0
  28. package/dist/es2019/rules/import/shared/jest-utils.js +203 -0
  29. package/dist/es2019/rules/import/shared/package-registry.js +240 -0
  30. package/dist/es2019/rules/import/shared/package-resolution.js +253 -0
  31. package/dist/es2019/rules/import/shared/perf.js +83 -0
  32. package/dist/es2019/rules/import/shared/types.js +57 -0
  33. package/dist/es2019/rules/visit-example-type-import-required/index.js +375 -0
  34. package/dist/esm/index.js +19 -3
  35. package/dist/esm/rules/ensure-use-sync-external-store-server-snapshot/index.js +35 -0
  36. package/dist/esm/rules/feature-gating/valid-gate-name/index.js +53 -0
  37. package/dist/esm/rules/import/no-barrel-entry-imports/index.js +1272 -0
  38. package/dist/esm/rules/import/no-barrel-entry-jest-mock/index.js +1650 -0
  39. package/dist/esm/rules/import/no-conversation-assistant-barrel-imports/index.js +37 -0
  40. package/dist/esm/rules/import/no-jest-mock-barrel-files/index.js +1392 -0
  41. package/dist/esm/rules/import/no-relative-barrel-file-imports/index.js +774 -0
  42. package/dist/esm/rules/import/shared/barrel-parsing.js +500 -0
  43. package/dist/esm/rules/import/shared/file-system.js +176 -0
  44. package/dist/esm/rules/import/shared/jest-utils.js +231 -0
  45. package/dist/esm/rules/import/shared/package-registry.js +256 -0
  46. package/dist/esm/rules/import/shared/package-resolution.js +306 -0
  47. package/dist/esm/rules/import/shared/perf.js +80 -0
  48. package/dist/esm/rules/import/shared/types.js +61 -0
  49. package/dist/esm/rules/visit-example-type-import-required/index.js +402 -0
  50. package/dist/types/index.d.ts +28 -2
  51. package/dist/types/rules/ensure-use-sync-external-store-server-snapshot/index.d.ts +3 -0
  52. package/dist/types/rules/feature-gating/valid-gate-name/index.d.ts +3 -0
  53. package/dist/types/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
  54. package/dist/types/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
  55. package/dist/types/rules/import/no-conversation-assistant-barrel-imports/index.d.ts +3 -0
  56. package/dist/types/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
  57. package/dist/types/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
  58. package/dist/types/rules/import/shared/barrel-parsing.d.ts +30 -0
  59. package/dist/types/rules/import/shared/file-system.d.ts +38 -0
  60. package/dist/types/rules/import/shared/jest-utils.d.ts +55 -0
  61. package/dist/types/rules/import/shared/package-registry.d.ts +26 -0
  62. package/dist/types/rules/import/shared/package-resolution.d.ts +58 -0
  63. package/dist/types/rules/import/shared/perf.d.ts +13 -0
  64. package/dist/types/rules/import/shared/types.d.ts +131 -0
  65. package/dist/types/rules/visit-example-type-import-required/index.d.ts +4 -0
  66. package/dist/types-ts4.5/index.d.ts +28 -2
  67. package/dist/types-ts4.5/rules/import/no-barrel-entry-imports/index.d.ts +9 -0
  68. package/dist/types-ts4.5/rules/import/no-barrel-entry-jest-mock/index.d.ts +9 -0
  69. package/dist/types-ts4.5/rules/import/no-conversation-assistant-barrel-imports/index.d.ts +3 -0
  70. package/dist/types-ts4.5/rules/import/no-jest-mock-barrel-files/index.d.ts +22 -0
  71. package/dist/types-ts4.5/rules/import/no-relative-barrel-file-imports/index.d.ts +5 -0
  72. package/dist/types-ts4.5/rules/import/shared/barrel-parsing.d.ts +30 -0
  73. package/dist/types-ts4.5/rules/import/shared/file-system.d.ts +38 -0
  74. package/dist/types-ts4.5/rules/import/shared/jest-utils.d.ts +55 -0
  75. package/dist/types-ts4.5/rules/import/shared/package-registry.d.ts +26 -0
  76. package/dist/types-ts4.5/rules/import/shared/package-resolution.d.ts +58 -0
  77. package/dist/types-ts4.5/rules/import/shared/perf.d.ts +13 -0
  78. package/dist/types-ts4.5/rules/import/shared/types.d.ts +131 -0
  79. package/dist/types-ts4.5/rules/visit-example-type-import-required/index.d.ts +4 -0
  80. package/package.json +6 -2
  81. package/dist/cjs/rules/ensure-native-and-af-exports-synced/index.js +0 -158
  82. package/dist/es2019/rules/ensure-native-and-af-exports-synced/index.js +0 -146
  83. package/dist/esm/rules/ensure-native-and-af-exports-synced/index.js +0 -151
  84. /package/dist/types-ts4.5/rules/{ensure-native-and-af-exports-synced → ensure-use-sync-external-store-server-snapshot}/index.d.ts +0 -0
  85. /package/dist/{types/rules/ensure-native-and-af-exports-synced → types-ts4.5/rules/feature-gating/valid-gate-name}/index.d.ts +0 -0
@@ -0,0 +1,1341 @@
1
+ import { dirname } from 'path';
2
+ import * as ts from 'typescript';
3
+ import { hasReExportsFromOtherFiles, parseBarrelExports } from '../shared/barrel-parsing';
4
+ import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport, readFileContent, resolveImportPath } from '../shared/file-system';
5
+ import { extractImportPath, findJestRequireActualCalls, findJestRequireMockCalls, isJestMockCall, isJestRequireActual, resolveNewPathForRequireMock } from '../shared/jest-utils';
6
+ import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
7
+ import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
8
+ import { realFileSystem } from '../shared/types';
9
+
10
+ /**
11
+ * Options for the no-barrel-entry-jest-mock rule.
12
+ */
13
+
14
+ /**
15
+ * Result of tracing a symbol through barrel files to its source.
16
+ */
17
+
18
+ /**
19
+ * Grouped mock properties by their target export path.
20
+ */
21
+
22
+ /**
23
+ * Trace the re-export chain for a symbol from a barrel file.
24
+ * Returns an array of file paths representing the chain from the barrel to the source.
25
+ * For example: [barrel.ts, intermediate.ts, source.ts]
26
+ */
27
+ function traceReExportChain({
28
+ barrelFilePath,
29
+ symbolName,
30
+ fs,
31
+ visited = new Set()
32
+ }) {
33
+ if (visited.has(barrelFilePath)) {
34
+ return [];
35
+ }
36
+ visited.add(barrelFilePath);
37
+ const content = readFileContent({
38
+ filePath: barrelFilePath,
39
+ fs
40
+ });
41
+ if (!content) {
42
+ return [barrelFilePath];
43
+ }
44
+ let sourceFile;
45
+ try {
46
+ sourceFile = ts.createSourceFile(barrelFilePath, content, ts.ScriptTarget.Latest, true);
47
+ } catch {
48
+ return [barrelFilePath];
49
+ }
50
+ const barrelDir = dirname(barrelFilePath);
51
+ for (const statement of sourceFile.statements) {
52
+ if (!ts.isExportDeclaration(statement) || !statement.moduleSpecifier) {
53
+ continue;
54
+ }
55
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) {
56
+ continue;
57
+ }
58
+ const modulePath = statement.moduleSpecifier.text;
59
+ if (!isRelativeImport(modulePath)) {
60
+ continue;
61
+ }
62
+
63
+ // Check if this export statement includes our symbol
64
+ let includesSymbol = false;
65
+ if (statement.exportClause) {
66
+ if (ts.isNamedExports(statement.exportClause)) {
67
+ for (const element of statement.exportClause.elements) {
68
+ const exportedName = element.name.text;
69
+ if (exportedName === symbolName) {
70
+ includesSymbol = true;
71
+ break;
72
+ }
73
+ }
74
+ }
75
+ } else {
76
+ // Star export - might include the symbol
77
+ includesSymbol = true;
78
+ }
79
+ if (includesSymbol) {
80
+ const resolvedSource = resolveImportPath({
81
+ basedir: barrelDir,
82
+ importPath: modulePath,
83
+ fs
84
+ });
85
+ if (resolvedSource) {
86
+ // Recursively trace from the resolved source
87
+ const restOfChain = traceReExportChain({
88
+ barrelFilePath: resolvedSource,
89
+ symbolName,
90
+ fs,
91
+ visited
92
+ });
93
+ return [barrelFilePath, ...restOfChain];
94
+ }
95
+ }
96
+ }
97
+
98
+ // Symbol is defined in this file (not re-exported)
99
+ return [barrelFilePath];
100
+ }
101
+
102
+ /**
103
+ * Find a package.json export that can provide a given symbol.
104
+ *
105
+ * This function traces the re-export chain from the current barrel to the symbol's source,
106
+ * and returns an export if its entry file is on that chain (i.e., it's an intermediate barrel
107
+ * that the main barrel imports from for this symbol).
108
+ *
109
+ * This prevents suggesting unrelated barrel files that happen to re-export the same symbol
110
+ * through a different path.
111
+ */
112
+ function findExportForSymbol({
113
+ symbolName,
114
+ symbolSourcePath,
115
+ exportsMap,
116
+ currentExportPath,
117
+ fs
118
+ }) {
119
+ const currentEntryFilePath = exportsMap.get(currentExportPath);
120
+ if (!currentEntryFilePath) {
121
+ return null;
122
+ }
123
+
124
+ // Trace the re-export chain from the current barrel to the source
125
+ const reExportChain = traceReExportChain({
126
+ barrelFilePath: currentEntryFilePath,
127
+ symbolName,
128
+ fs
129
+ });
130
+
131
+ // Convert chain to a Set for O(1) lookup
132
+ const chainSet = new Set(reExportChain);
133
+
134
+ // Check each package.json export entry (except the current one)
135
+ for (const [exportPath, entryFilePath] of exportsMap) {
136
+ if (exportPath === currentExportPath) {
137
+ continue;
138
+ }
139
+
140
+ // Skip exports that resolve to the same file as the current export path
141
+ if (entryFilePath === currentEntryFilePath) {
142
+ continue;
143
+ }
144
+
145
+ // Only return this export if its entry file is on the re-export chain
146
+ // (meaning it's an intermediate barrel the main barrel imports from for this symbol)
147
+ // or if it directly points to the source file
148
+ if (chainSet.has(entryFilePath) || entryFilePath === symbolSourcePath) {
149
+ // Verify the symbol is actually exported from this entry file
150
+ const entryExports = parseBarrelExports({
151
+ barrelFilePath: entryFilePath,
152
+ depth: 0,
153
+ fs
154
+ });
155
+ if (entryExports.has(symbolName)) {
156
+ return exportPath;
157
+ }
158
+ }
159
+ }
160
+ return null;
161
+ }
162
+
163
+ /**
164
+ * Preamble statement extracted from a mock factory function.
165
+ */
166
+
167
+ /**
168
+ * Collect all identifier names used in a node (recursively).
169
+ * Avoids circular references by skipping parent-related properties.
170
+ */
171
+ function collectUsedIdentifiers({
172
+ node
173
+ }) {
174
+ const identifiers = new Set();
175
+ const visited = new WeakSet();
176
+
177
+ // Properties to skip to avoid circular references
178
+ const skipProperties = new Set(['parent', 'tokens', 'comments', 'loc', 'range']);
179
+ function traverse(n) {
180
+ if (visited.has(n)) {
181
+ return;
182
+ }
183
+ visited.add(n);
184
+ if (n.type === 'Identifier') {
185
+ identifiers.add(n.name);
186
+ }
187
+ for (const key of Object.keys(n)) {
188
+ if (skipProperties.has(key)) {
189
+ continue;
190
+ }
191
+ const child = n[key];
192
+ if (child && typeof child === 'object') {
193
+ if (Array.isArray(child)) {
194
+ for (const item of child) {
195
+ if (item && typeof item === 'object' && 'type' in item) {
196
+ traverse(item);
197
+ }
198
+ }
199
+ } else if ('type' in child) {
200
+ traverse(child);
201
+ }
202
+ }
203
+ }
204
+ }
205
+ traverse(node);
206
+ return identifiers;
207
+ }
208
+
209
+ /**
210
+ * Extract preamble statements (variable declarations) from a block body.
211
+ */
212
+ function extractPreambleStatements({
213
+ mockImpl,
214
+ sourceCode
215
+ }) {
216
+ const preamble = [];
217
+ let body = null;
218
+ if (mockImpl.type === 'ArrowFunctionExpression' && mockImpl.body.type === 'BlockStatement') {
219
+ body = mockImpl.body.body;
220
+ } else if (mockImpl.type === 'FunctionExpression' && mockImpl.body.type === 'BlockStatement') {
221
+ body = mockImpl.body.body;
222
+ }
223
+ if (!body) {
224
+ return preamble;
225
+ }
226
+ for (const stmt of body) {
227
+ if (stmt.type === 'ReturnStatement') {
228
+ break; // Stop at return
229
+ }
230
+ if (stmt.type === 'VariableDeclaration') {
231
+ const declaredNames = [];
232
+ const usedIdentifiers = new Set();
233
+ for (const decl of stmt.declarations) {
234
+ if (decl.id.type === 'Identifier') {
235
+ declaredNames.push(decl.id.name);
236
+ }
237
+ if (decl.init) {
238
+ const used = collectUsedIdentifiers({
239
+ node: decl.init
240
+ });
241
+ for (const id of used) {
242
+ usedIdentifiers.add(id);
243
+ }
244
+ }
245
+ }
246
+
247
+ // Remove declared names from used identifiers
248
+ for (const name of declaredNames) {
249
+ usedIdentifiers.delete(name);
250
+ }
251
+ preamble.push({
252
+ declaredNames,
253
+ text: sourceCode.getText(stmt),
254
+ usedIdentifiers
255
+ });
256
+ }
257
+ }
258
+ return preamble;
259
+ }
260
+
261
+ /**
262
+ * Determine which preamble statements are needed for a set of property texts.
263
+ * Returns the preamble statements in order, including any transitively needed ones.
264
+ */
265
+ function getNeededPreamble({
266
+ propertyTexts,
267
+ allPreamble
268
+ }) {
269
+ // Collect all identifiers used in the property texts
270
+ const neededIdentifiers = new Set();
271
+ for (const text of propertyTexts) {
272
+ // Simple regex to find identifiers in the text
273
+ // This is a basic approach; handles most common cases
274
+ const identifierPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
275
+ let match;
276
+ while ((match = identifierPattern.exec(text)) !== null) {
277
+ neededIdentifiers.add(match[1]);
278
+ }
279
+ }
280
+
281
+ // Build dependency graph and find needed preamble
282
+ const neededPreamble = [];
283
+ const includedNames = new Set();
284
+ let changed = true;
285
+ while (changed) {
286
+ changed = false;
287
+ for (const stmt of allPreamble) {
288
+ // Check if any declared name is needed
289
+ const isNeeded = stmt.declaredNames.some(name => neededIdentifiers.has(name));
290
+ const alreadyIncluded = stmt.declaredNames.some(name => includedNames.has(name));
291
+ if (isNeeded && !alreadyIncluded) {
292
+ neededPreamble.push(stmt);
293
+ for (const name of stmt.declaredNames) {
294
+ includedNames.add(name);
295
+ }
296
+ // Add any identifiers this statement uses to needed set
297
+ for (const id of stmt.usedIdentifiers) {
298
+ if (!neededIdentifiers.has(id)) {
299
+ neededIdentifiers.add(id);
300
+ changed = true;
301
+ }
302
+ }
303
+ }
304
+ }
305
+ }
306
+
307
+ // Return in original order
308
+ return allPreamble.filter(stmt => neededPreamble.includes(stmt));
309
+ }
310
+
311
+ /**
312
+ * Extract mock object properties from jest.mock call
313
+ * Returns a map of property name -> { node, text } and whether there's a jest.requireActual spread
314
+ */
315
+ function extractMockProperties({
316
+ sourceCode,
317
+ mockObjectNode
318
+ }) {
319
+ const properties = new Map();
320
+ let hasRequireActual = false;
321
+ if (mockObjectNode.type === 'ObjectExpression') {
322
+ for (const prop of mockObjectNode.properties) {
323
+ if (prop.type === 'SpreadElement') {
324
+ if (isJestRequireActual(prop.argument)) {
325
+ hasRequireActual = true;
326
+ }
327
+ } else if (prop.type === 'Property') {
328
+ let keyName;
329
+ if (prop.key.type === 'Identifier') {
330
+ keyName = prop.key.name;
331
+ } else if (prop.key.type === 'Literal') {
332
+ keyName = String(prop.key.value);
333
+ } else {
334
+ continue;
335
+ }
336
+ const propText = sourceCode.getText(prop);
337
+ properties.set(keyName, {
338
+ node: prop,
339
+ text: propText
340
+ });
341
+ }
342
+ }
343
+ }
344
+ return {
345
+ properties,
346
+ hasRequireActual
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Information about an existing jest.mock call in the file
352
+ */
353
+
354
+ /**
355
+ * Find all jest.mock calls in the current file
356
+ */
357
+ function findAllJestMocksInFile({
358
+ context
359
+ }) {
360
+ const allMocks = new Map();
361
+ const sourceCode = context.getSourceCode();
362
+ const ast = sourceCode.ast;
363
+
364
+ // Use a visited set to prevent infinite recursion
365
+ const visited = new WeakSet();
366
+
367
+ // Properties to skip to avoid circular references
368
+ const skipKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
369
+ function visitNode(node) {
370
+ // Prevent revisiting nodes
371
+ if (visited.has(node)) {
372
+ return;
373
+ }
374
+ visited.add(node);
375
+ if (node.type === 'CallExpression' && isJestMockCall(node)) {
376
+ const importPath = extractImportPath(node);
377
+ if (importPath) {
378
+ const mockImpl = node.arguments[1];
379
+ if (mockImpl) {
380
+ const mockObjectNode = extractMockImplementation({
381
+ mockImpl: mockImpl
382
+ });
383
+ const {
384
+ properties,
385
+ hasRequireActual
386
+ } = extractMockProperties({
387
+ sourceCode,
388
+ mockObjectNode
389
+ });
390
+ allMocks.set(importPath, {
391
+ node,
392
+ importPath,
393
+ properties,
394
+ hasRequireActual
395
+ });
396
+ }
397
+ }
398
+ }
399
+
400
+ // Recursively visit child nodes
401
+ for (const key in node) {
402
+ if (skipKeys.has(key)) {
403
+ continue;
404
+ }
405
+ const value = node[key];
406
+ if (value && typeof value === 'object') {
407
+ if (Array.isArray(value)) {
408
+ for (const child of value) {
409
+ if (child && typeof child === 'object' && 'type' in child) {
410
+ visitNode(child);
411
+ }
412
+ }
413
+ } else if ('type' in value) {
414
+ visitNode(value);
415
+ }
416
+ }
417
+ }
418
+ }
419
+ visitNode(ast);
420
+ return allMocks;
421
+ }
422
+
423
+ /**
424
+ * Merge mock properties from multiple sources for the same file
425
+ */
426
+ function mergeMockProperties({
427
+ existingProperties,
428
+ newProperties
429
+ }) {
430
+ const merged = new Map(existingProperties);
431
+ for (const [key, value] of newProperties) {
432
+ merged.set(key, value);
433
+ }
434
+ return merged;
435
+ }
436
+
437
+ /**
438
+ * Check if a node is an Object.assign call
439
+ */
440
+ function isObjectAssignCall({
441
+ node
442
+ }) {
443
+ if (node.type !== 'CallExpression') {
444
+ return false;
445
+ }
446
+ const callee = node.callee;
447
+ if (callee.type === 'MemberExpression') {
448
+ return callee.object.type === 'Identifier' && callee.object.name === 'Object' && callee.property.type === 'Identifier' && callee.property.name === 'assign';
449
+ }
450
+ return false;
451
+ }
452
+
453
+ /**
454
+ * Extract the object expression containing mock properties from an Object.assign call.
455
+ * Pattern: Object.assign({}, jest.requireActual(...), { props... })
456
+ * Returns the last ObjectExpression argument, or null if not found.
457
+ */
458
+ function extractObjectFromAssign({
459
+ callExpr
460
+ }) {
461
+ // Look for ObjectExpression arguments that are not the target (first arg)
462
+ // The pattern is typically: Object.assign({}, jest.requireActual(...), { actual props })
463
+ // We want the last ObjectExpression that contains the actual mock properties
464
+ for (let i = callExpr.arguments.length - 1; i >= 0; i--) {
465
+ const arg = callExpr.arguments[i];
466
+ if (arg.type === 'ObjectExpression' && arg.properties.length > 0) {
467
+ return arg;
468
+ }
469
+ }
470
+ return null;
471
+ }
472
+
473
+ /**
474
+ * Extract the mock implementation object from the jest.mock call.
475
+ * Handles arrow functions, function expressions, and Object.assign patterns.
476
+ */
477
+ function extractMockImplementation({
478
+ mockImpl
479
+ }) {
480
+ // Helper to unwrap Object.assign if present
481
+ const unwrapObjectAssign = node => {
482
+ if (node.type === 'CallExpression' && isObjectAssignCall({
483
+ node
484
+ })) {
485
+ const extracted = extractObjectFromAssign({
486
+ callExpr: node
487
+ });
488
+ if (extracted) {
489
+ return extracted;
490
+ }
491
+ }
492
+ return node;
493
+ };
494
+ if (mockImpl.type === 'ArrowFunctionExpression') {
495
+ if (mockImpl.body.type === 'ObjectExpression') {
496
+ return mockImpl.body;
497
+ }
498
+ // Handle arrow function returning Object.assign
499
+ if (mockImpl.body.type === 'CallExpression' && isObjectAssignCall({
500
+ node: mockImpl.body
501
+ })) {
502
+ return unwrapObjectAssign(mockImpl.body);
503
+ }
504
+ if (mockImpl.body.type === 'BlockStatement') {
505
+ const returnStmt = mockImpl.body.body.find(s => s.type === 'ReturnStatement');
506
+ if (returnStmt !== null && returnStmt !== void 0 && returnStmt.argument) {
507
+ return unwrapObjectAssign(returnStmt.argument);
508
+ }
509
+ }
510
+ }
511
+ if (mockImpl.type === 'FunctionExpression' && mockImpl.body.type === 'BlockStatement') {
512
+ const returnStmt = mockImpl.body.body.find(s => s.type === 'ReturnStatement');
513
+ if (returnStmt !== null && returnStmt !== void 0 && returnStmt.argument) {
514
+ return unwrapObjectAssign(returnStmt.argument);
515
+ }
516
+ }
517
+ return mockImpl;
518
+ }
519
+
520
+ /**
521
+ * Trace mocked symbols to their source files and group by package.json exports.
522
+ */
523
+ function traceSymbolsToExports({
524
+ symbolNames,
525
+ exportMap,
526
+ exportsMap,
527
+ currentExportPath,
528
+ fs
529
+ }) {
530
+ const groupedByExport = new Map();
531
+ const crossPackageGroups = new Map();
532
+ const unmappedSymbols = [];
533
+ for (const symbolName of symbolNames) {
534
+ var _findExportForSourceF, _findExportForSourceF2;
535
+ const exportInfo = exportMap.get(symbolName);
536
+ if (!exportInfo) {
537
+ unmappedSymbols.push(symbolName);
538
+ continue;
539
+ }
540
+
541
+ // Check for cross-package source first
542
+ if (exportInfo.crossPackageSource) {
543
+ const key = `${exportInfo.crossPackageSource.packageName}${exportInfo.crossPackageSource.exportPath === '.' ? '' : exportInfo.crossPackageSource.exportPath.slice(1)}`;
544
+ if (!crossPackageGroups.has(key)) {
545
+ crossPackageGroups.set(key, []);
546
+ }
547
+ crossPackageGroups.get(key).push({
548
+ symbolName,
549
+ originalName: exportInfo.originalName,
550
+ sourceFilePath: exportInfo.path,
551
+ isTypeOnly: exportInfo.isTypeOnly,
552
+ crossPackageSource: exportInfo.crossPackageSource
553
+ });
554
+ continue;
555
+ }
556
+
557
+ // First try to find an export that directly exposes the source file
558
+ let targetExportPath = (_findExportForSourceF = (_findExportForSourceF2 = findExportForSourceFile({
559
+ sourceFilePath: exportInfo.path,
560
+ exportsMap
561
+ })) === null || _findExportForSourceF2 === void 0 ? void 0 : _findExportForSourceF2.exportPath) !== null && _findExportForSourceF !== void 0 ? _findExportForSourceF : null;
562
+
563
+ // If no direct match, check which export can provide this symbol
564
+ // (handles nested barrels where the symbol is re-exported through intermediate files)
565
+ if (!targetExportPath) {
566
+ targetExportPath = findExportForSymbol({
567
+ symbolName,
568
+ symbolSourcePath: exportInfo.path,
569
+ exportsMap,
570
+ currentExportPath,
571
+ fs
572
+ });
573
+ }
574
+ if (targetExportPath && targetExportPath !== currentExportPath) {
575
+ if (!groupedByExport.has(targetExportPath)) {
576
+ groupedByExport.set(targetExportPath, []);
577
+ }
578
+ groupedByExport.get(targetExportPath).push({
579
+ symbolName,
580
+ originalName: exportInfo.originalName,
581
+ sourceFilePath: exportInfo.path,
582
+ isTypeOnly: exportInfo.isTypeOnly
583
+ });
584
+ } else {
585
+ unmappedSymbols.push(symbolName);
586
+ }
587
+ }
588
+ return {
589
+ groupedByExport,
590
+ crossPackageGroups,
591
+ unmappedSymbols
592
+ };
593
+ }
594
+
595
+ /**
596
+ * Replace the property key in property text when the export is aliased.
597
+ * For example, if the original text is "renamedFunction: jest.fn()" and
598
+ * the original name is "originalFunction", returns "originalFunction: jest.fn()".
599
+ */
600
+ function replacePropertyKey({
601
+ propText,
602
+ mockName,
603
+ originalName
604
+ }) {
605
+ // Match the property key at the start (handles both quoted and unquoted keys)
606
+ const keyPattern = new RegExp(`^(['"]?)${escapeRegExp(mockName)}\\1\\s*:`);
607
+ return propText.replace(keyPattern, `${originalName}:`);
608
+ }
609
+
610
+ /**
611
+ * Escape special regex characters in a string.
612
+ */
613
+ function escapeRegExp(str) {
614
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
615
+ }
616
+
617
+ /** Mock object keys that are module interop metadata, not package exports. */
618
+ const ESM_INTEROP_MOCK_KEYS = new Set(['__esModule']);
619
+
620
+ /**
621
+ * If a preamble line is `const|let actual = jest.requireActual('<barrel>')`, rewrite the specifier to
622
+ * `targetImportPathForThisMock` when every `binding.<prop>` read in this split mock's implementation
623
+ * resolves to that same path via `symbolToNewImportPath`. Otherwise leave unchanged (e.g. mixed paths
624
+ * still need the barrel module).
625
+ */
626
+ function rewritePreambleLineBarrelRequireActual({
627
+ lineText,
628
+ oldBarrelPath,
629
+ targetImportPathForThisMock,
630
+ mockImplementationTextForGroup,
631
+ symbolToNewImportPath
632
+ }) {
633
+ const requireActualRe = /jest\.requireActual(?:<[^>]*>)?\((['"])([^'"]+)\1\)/;
634
+ const requireMatch = requireActualRe.exec(lineText);
635
+ if (!requireMatch || requireMatch[2] !== oldBarrelPath) {
636
+ return lineText;
637
+ }
638
+ const quote = requireMatch[1];
639
+ const bindingMatch = /^\s*(?:const|let)\s+(\w+)\s*=/m.exec(lineText);
640
+ if (!bindingMatch) {
641
+ return lineText;
642
+ }
643
+ const binding = bindingMatch[1];
644
+ const propAccessRe = new RegExp(`\\b${escapeRegExp(binding)}\\.(\\w+)`, 'g');
645
+ const accessedProps = new Set();
646
+ let propMatch;
647
+ while ((propMatch = propAccessRe.exec(mockImplementationTextForGroup)) !== null) {
648
+ accessedProps.add(propMatch[1]);
649
+ }
650
+ if (accessedProps.size === 0) {
651
+ return lineText;
652
+ }
653
+ const resolvedPaths = [];
654
+ for (const prop of accessedProps) {
655
+ const mapped = symbolToNewImportPath.get(prop);
656
+ if (mapped) {
657
+ resolvedPaths.push(mapped);
658
+ }
659
+ }
660
+ if (resolvedPaths.length === 0) {
661
+ return lineText;
662
+ }
663
+ const uniquePaths = new Set(resolvedPaths);
664
+ if (uniquePaths.size !== 1 || !uniquePaths.has(targetImportPathForThisMock)) {
665
+ return lineText;
666
+ }
667
+ return lineText.replace(`jest.requireActual(${quote}${oldBarrelPath}${quote})`, `jest.requireActual(${quote}${targetImportPathForThisMock}${quote})`);
668
+ }
669
+
670
+ /**
671
+ * Generate fix text for multiple jest.mock calls
672
+ */
673
+ function generateMockFixes({
674
+ groups,
675
+ crossPackageGroups,
676
+ packageName,
677
+ mockProperties,
678
+ quote,
679
+ preambleStatements,
680
+ propagateEsModuleFromOriginalMock,
681
+ oldBarrelImportPath,
682
+ symbolToNewImportPath
683
+ }) {
684
+ const mockCalls = [];
685
+
686
+ // Helper to generate a single mock call
687
+ const generateMockCall = (group, fullImportPath) => {
688
+ const propTexts = [];
689
+ propTexts.push(`...jest.requireActual(${quote}${fullImportPath}${quote})`);
690
+
691
+ // Add __esModule: true when mocking default exports, or when the original mock already used __esModule (apply to every split).
692
+ if (group.hasDefaultExport || propagateEsModuleFromOriginalMock) {
693
+ propTexts.push('__esModule: true');
694
+ }
695
+ for (const propName of group.propertyNames) {
696
+ // First try to get from group's propertyTexts (used for merged mocks)
697
+ const groupPropText = group.propertyTexts.get(propName);
698
+ if (groupPropText) {
699
+ // Check if this property needs to be renamed (aliased export)
700
+ const originalName = group.nameMapping.get(propName);
701
+ if (originalName && originalName !== propName) {
702
+ const renamedText = replacePropertyKey({
703
+ propText: groupPropText,
704
+ mockName: propName,
705
+ originalName
706
+ });
707
+ propTexts.push(renamedText);
708
+ } else {
709
+ propTexts.push(groupPropText);
710
+ }
711
+ } else {
712
+ // Fallback to mockProperties (shouldn't happen with properly constructed groups)
713
+ const propInfo = mockProperties.get(propName);
714
+ if (propInfo) {
715
+ const originalName = group.nameMapping.get(propName);
716
+ if (originalName && originalName !== propName) {
717
+ const renamedText = replacePropertyKey({
718
+ propText: propInfo.text,
719
+ mockName: propName,
720
+ originalName
721
+ });
722
+ propTexts.push(renamedText);
723
+ } else {
724
+ propTexts.push(propInfo.text);
725
+ }
726
+ }
727
+ }
728
+ }
729
+ const combinedGroupImplText = group.propertyNames.map(name => {
730
+ var _ref, _group$propertyTexts$, _mockProperties$get;
731
+ return (_ref = (_group$propertyTexts$ = group.propertyTexts.get(name)) !== null && _group$propertyTexts$ !== void 0 ? _group$propertyTexts$ : (_mockProperties$get = mockProperties.get(name)) === null || _mockProperties$get === void 0 ? void 0 : _mockProperties$get.text) !== null && _ref !== void 0 ? _ref : '';
732
+ }).join('\n');
733
+
734
+ // Determine if we need preamble for this group
735
+ const neededPreamble = getNeededPreamble({
736
+ propertyTexts: propTexts,
737
+ allPreamble: preambleStatements
738
+ });
739
+ const rewrittenPreamble = neededPreamble.map(p => rewritePreambleLineBarrelRequireActual({
740
+ lineText: p.text,
741
+ oldBarrelPath: oldBarrelImportPath,
742
+ targetImportPathForThisMock: fullImportPath,
743
+ mockImplementationTextForGroup: combinedGroupImplText,
744
+ symbolToNewImportPath
745
+ }));
746
+ if (neededPreamble.length > 0) {
747
+ // Generate block body arrow function with preamble
748
+ const preambleLines = rewrittenPreamble.map(text => `\t${text}`).join('\n');
749
+ const formattedProps = propTexts.map(p => `\t\t${p},`).join('\n');
750
+ return `jest.mock(${quote}${fullImportPath}${quote}, () => {\n${preambleLines}\n\treturn {\n${formattedProps}\n\t};\n})`;
751
+ } else {
752
+ // Always use multi-line format for consistency
753
+ const formattedProps = propTexts.map(p => `\t${p},`).join('\n');
754
+ return `jest.mock(${quote}${fullImportPath}${quote}, () => ({\n${formattedProps}\n}))`;
755
+ }
756
+ };
757
+
758
+ // Generate mocks for cross-package groups first
759
+ for (const group of crossPackageGroups) {
760
+ mockCalls.push(generateMockCall(group, group.importPath));
761
+ }
762
+
763
+ // Generate mocks for same-package groups
764
+ for (const group of groups) {
765
+ const fullImportPath = `${packageName}${group.exportPath.slice(1)}`;
766
+ mockCalls.push(generateMockCall(group, fullImportPath));
767
+ }
768
+
769
+ // Join with semicolons but don't add trailing semicolon
770
+ return mockCalls.join(';\n');
771
+ }
772
+
773
+ /**
774
+ * Context resolved for a jest.mock that may be mocking a barrel file.
775
+ */
776
+
777
+ /**
778
+ * Resolves jest.mock context for barrel file analysis.
779
+ * Returns null if the mock should not be processed.
780
+ */
781
+ function resolveJestMockContext({
782
+ importPath,
783
+ workspaceRoot,
784
+ fs,
785
+ applyToImportsFrom
786
+ }) {
787
+ if (isRelativeImport(importPath)) {
788
+ return null;
789
+ }
790
+ const packageNameMatch = importPath.match(/^(@[^/]+\/[^/]+)/);
791
+ if (!packageNameMatch) {
792
+ return null;
793
+ }
794
+ const packageName = packageNameMatch[1];
795
+ const subPath = importPath.slice(packageName.length);
796
+
797
+ // Find the package (resolution is not constrained by applyToImportsFrom)
798
+ const packageDir = findPackageInRegistry({
799
+ packageName,
800
+ workspaceRoot,
801
+ fs
802
+ });
803
+ if (!packageDir) {
804
+ return null;
805
+ }
806
+
807
+ // Only check mocks from packages in our applyToImportsFrom folders
808
+ if (!isPackageInApplyToImportsFrom({
809
+ packageDir,
810
+ workspaceRoot,
811
+ applyToImportsFrom
812
+ })) {
813
+ return null;
814
+ }
815
+ const exportsMap = parsePackageExports({
816
+ packageDir,
817
+ fs
818
+ });
819
+ if (exportsMap.size === 0) {
820
+ return null;
821
+ }
822
+ const currentExportPath = subPath ? '.' + subPath : '.';
823
+ const entryFilePath = exportsMap.get(currentExportPath);
824
+ if (!entryFilePath) {
825
+ return null;
826
+ }
827
+ const exportMap = parseBarrelExports({
828
+ barrelFilePath: entryFilePath,
829
+ fs,
830
+ workspaceRoot
831
+ });
832
+ if (exportMap.size === 0) {
833
+ return null;
834
+ }
835
+ return {
836
+ importPath,
837
+ packageName,
838
+ packageDir,
839
+ currentExportPath,
840
+ exportsMap,
841
+ exportMap,
842
+ entryFilePath
843
+ };
844
+ }
845
+
846
+ /**
847
+ * Check if the entry file is a barrel file (re-exports from other files)
848
+ */
849
+ function isBarrelFile({
850
+ exportMap,
851
+ entryFilePath
852
+ }) {
853
+ return hasReExportsFromOtherFiles({
854
+ exportMap,
855
+ sourceFilePath: entryFilePath
856
+ });
857
+ }
858
+
859
+ /**
860
+ * Metadata for the ESLint rule
861
+ */
862
+ const ruleMeta = {
863
+ type: 'problem',
864
+ docs: {
865
+ description: 'Disallow jest.mock calls on barrel file entry points. Mock source files directly using package.json exports.',
866
+ category: 'Best Practices',
867
+ recommended: false
868
+ },
869
+ fixable: 'code',
870
+ schema: [{
871
+ type: 'object',
872
+ properties: {
873
+ applyToImportsFrom: {
874
+ type: 'array',
875
+ items: {
876
+ type: 'string'
877
+ },
878
+ description: 'The folder paths (relative to workspace root) containing packages whose imports will be checked and autofixed.'
879
+ }
880
+ },
881
+ additionalProperties: false
882
+ }],
883
+ messages: {
884
+ barrelEntryMock: "jest.mock('{{path}}') is mocking a barrel file entry point. Split into separate mocks for each source file using package.json exports.",
885
+ barrelEntryRequireActual: "jest.requireActual('{{path}}') references a barrel file entry point. Use a specific package.json export path instead."
886
+ }
887
+ };
888
+
889
+ /**
890
+ * Factory function to create the ESLint rule with a given file system.
891
+ * This enables testing with mock file systems.
892
+ */
893
+ export function createRule(fs) {
894
+ return {
895
+ meta: ruleMeta,
896
+ create(context) {
897
+ var _options$applyToImpor;
898
+ const options = context.options[0] || {};
899
+ const applyToImportsFrom = (_options$applyToImpor = options.applyToImportsFrom) !== null && _options$applyToImpor !== void 0 ? _options$applyToImpor : DEFAULT_TARGET_FOLDERS;
900
+ const workspaceRoot = findWorkspaceRoot({
901
+ startPath: dirname(context.filename),
902
+ fs,
903
+ applyToImportsFrom
904
+ });
905
+ return {
906
+ CallExpression(rawNode) {
907
+ const node = rawNode;
908
+
909
+ // Handle standalone jest.requireActual() calls that reference barrel entries.
910
+ // e.g. jest.requireActual('@atlaskit/pkg').Foo or const { Foo } = jest.requireActual('@atlaskit/pkg')
911
+ if (isJestRequireActual(node)) {
912
+ const raImportPath = extractImportPath(node);
913
+ if (!raImportPath) {
914
+ return;
915
+ }
916
+ const raContext = resolveJestMockContext({
917
+ importPath: raImportPath,
918
+ workspaceRoot,
919
+ fs,
920
+ applyToImportsFrom
921
+ });
922
+ if (!raContext) {
923
+ return;
924
+ }
925
+ if (!isBarrelFile({
926
+ exportMap: raContext.exportMap,
927
+ entryFilePath: raContext.entryFilePath
928
+ })) {
929
+ return;
930
+ }
931
+
932
+ // `jest.requireActual('<barrel>')` inside `jest.mock('<barrel>')` is handled by the mock
933
+ // rule's fix (preamble retargeting + `jest.requireActual('barrel').x` rewriting), including
934
+ // `const actual = jest.requireActual('<barrel>')` with no member access on the call.
935
+ // Skip standalone handling here to avoid duplicate diagnostics.
936
+ let ancestor = node.parent;
937
+ while (ancestor) {
938
+ if (ancestor.type === 'CallExpression' && isJestMockCall(ancestor)) {
939
+ const ancestorPath = extractImportPath(ancestor);
940
+ if (ancestorPath) {
941
+ const ancestorCtx = resolveJestMockContext({
942
+ importPath: ancestorPath,
943
+ workspaceRoot,
944
+ fs,
945
+ applyToImportsFrom
946
+ });
947
+ if (ancestorCtx && isBarrelFile({
948
+ exportMap: ancestorCtx.exportMap,
949
+ entryFilePath: ancestorCtx.entryFilePath
950
+ })) {
951
+ return;
952
+ }
953
+ }
954
+ }
955
+ ancestor = ancestor.parent;
956
+ }
957
+
958
+ // Determine which symbols are accessed from the barrel
959
+ const parent = node.parent;
960
+ const accessedSymbols = [];
961
+ if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'MemberExpression' && parent.property.type === 'Identifier') {
962
+ accessedSymbols.push(parent.property.name);
963
+ } else if ((parent === null || parent === void 0 ? void 0 : parent.type) === 'VariableDeclarator' && parent.id.type === 'ObjectPattern') {
964
+ for (const prop of parent.id.properties) {
965
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
966
+ accessedSymbols.push(prop.key.name);
967
+ }
968
+ }
969
+ }
970
+ if (accessedSymbols.length === 0) {
971
+ context.report({
972
+ node: node,
973
+ messageId: 'barrelEntryRequireActual',
974
+ data: {
975
+ path: raImportPath
976
+ }
977
+ });
978
+ return;
979
+ }
980
+ const {
981
+ groupedByExport,
982
+ crossPackageGroups
983
+ } = traceSymbolsToExports({
984
+ symbolNames: accessedSymbols,
985
+ exportMap: raContext.exportMap,
986
+ exportsMap: raContext.exportsMap,
987
+ currentExportPath: raContext.currentExportPath,
988
+ fs
989
+ });
990
+ let newPath = null;
991
+ if (groupedByExport.size === 1 && crossPackageGroups.size === 0) {
992
+ const [exportPath] = groupedByExport.keys();
993
+ newPath = `${raContext.packageName}${exportPath.slice(1)}`;
994
+ } else if (crossPackageGroups.size === 1 && groupedByExport.size === 0) {
995
+ const [cpImportPath] = crossPackageGroups.keys();
996
+ newPath = cpImportPath;
997
+ }
998
+ const sourceCode = context.getSourceCode();
999
+ if (newPath) {
1000
+ const resolvedNewPath = newPath;
1001
+ context.report({
1002
+ node: node,
1003
+ messageId: 'barrelEntryRequireActual',
1004
+ data: {
1005
+ path: raImportPath
1006
+ },
1007
+ fix(fixer) {
1008
+ const firstArg = node.arguments[0];
1009
+ const quote = sourceCode.getText(firstArg)[0];
1010
+ return fixer.replaceText(firstArg, `${quote}${resolvedNewPath}${quote}`);
1011
+ }
1012
+ });
1013
+ } else {
1014
+ context.report({
1015
+ node: node,
1016
+ messageId: 'barrelEntryRequireActual',
1017
+ data: {
1018
+ path: raImportPath
1019
+ }
1020
+ });
1021
+ }
1022
+ return;
1023
+ }
1024
+ if (!isJestMockCall(node)) {
1025
+ return;
1026
+ }
1027
+ const importPath = extractImportPath(node);
1028
+ if (!importPath) {
1029
+ return;
1030
+ }
1031
+ const mockContext = resolveJestMockContext({
1032
+ importPath,
1033
+ workspaceRoot,
1034
+ fs,
1035
+ applyToImportsFrom
1036
+ });
1037
+ if (!mockContext) {
1038
+ return;
1039
+ }
1040
+ if (!isBarrelFile({
1041
+ exportMap: mockContext.exportMap,
1042
+ entryFilePath: mockContext.entryFilePath
1043
+ })) {
1044
+ return;
1045
+ }
1046
+ const mockImpl = node.arguments[1];
1047
+ // Ignore auto-mocks (jest.mock with only the import string and no second argument)
1048
+ // These are intentionally excluded from barrel file checks as they auto-mock all exports
1049
+ if (!mockImpl) {
1050
+ return;
1051
+ }
1052
+ const mockObjectNode = extractMockImplementation({
1053
+ mockImpl: mockImpl
1054
+ });
1055
+ const sourceCode = context.getSourceCode();
1056
+ const {
1057
+ properties: mockProperties
1058
+ } = extractMockProperties({
1059
+ sourceCode,
1060
+ mockObjectNode
1061
+ });
1062
+
1063
+ // Extract preamble statements (variable declarations before return)
1064
+ const preambleStatements = extractPreambleStatements({
1065
+ mockImpl: mockImpl,
1066
+ sourceCode
1067
+ });
1068
+ if (mockProperties.size === 0) {
1069
+ return;
1070
+ }
1071
+ const originalMockHadEsModule = mockProperties.has('__esModule');
1072
+ const symbolNames = Array.from(mockProperties.keys()).filter(name => !ESM_INTEROP_MOCK_KEYS.has(name));
1073
+ const {
1074
+ groupedByExport,
1075
+ crossPackageGroups,
1076
+ unmappedSymbols
1077
+ } = traceSymbolsToExports({
1078
+ symbolNames,
1079
+ exportMap: mockContext.exportMap,
1080
+ exportsMap: mockContext.exportsMap,
1081
+ currentExportPath: mockContext.currentExportPath,
1082
+ fs
1083
+ });
1084
+
1085
+ // If no symbols can be mapped to specific exports or cross-package sources,
1086
+ // there's nothing to fix so don't report an error
1087
+ if (groupedByExport.size === 0 && crossPackageGroups.size === 0) {
1088
+ return;
1089
+ }
1090
+ const groups = [];
1091
+ for (const [exportPath, symbols] of groupedByExport) {
1092
+ // Build name mapping for aliased exports
1093
+ const nameMapping = new Map();
1094
+ for (const s of symbols) {
1095
+ if (s.originalName) {
1096
+ nameMapping.set(s.symbolName, s.originalName);
1097
+ }
1098
+ }
1099
+
1100
+ // Check if any symbol in this group is a default export
1101
+ const hasDefaultExport = symbols.some(s => s.originalName === 'default');
1102
+ groups.push({
1103
+ exportPath,
1104
+ importPath: `${mockContext.packageName}${exportPath.slice(1)}`,
1105
+ propertyNames: symbols.map(s => s.symbolName),
1106
+ propertyTexts: new Map(symbols.map(s => [s.symbolName, mockProperties.get(s.symbolName).text])),
1107
+ nameMapping,
1108
+ hasDefaultExport
1109
+ });
1110
+ }
1111
+
1112
+ // Build cross-package groups
1113
+ const crossPackageMockGroups = [];
1114
+ for (const [importPath, symbols] of crossPackageGroups) {
1115
+ // Build name mapping for aliased exports
1116
+ const nameMapping = new Map();
1117
+ for (const s of symbols) {
1118
+ if (s.originalName) {
1119
+ nameMapping.set(s.symbolName, s.originalName);
1120
+ }
1121
+ }
1122
+
1123
+ // Check if any symbol in this group is a default export
1124
+ const hasDefaultExport = symbols.some(s => s.originalName === 'default');
1125
+
1126
+ // Get cross-package source info from the first symbol (all symbols in same group have same source)
1127
+ const crossPackageSource = symbols[0].crossPackageSource;
1128
+ crossPackageMockGroups.push({
1129
+ exportPath: crossPackageSource.exportPath,
1130
+ importPath,
1131
+ propertyNames: symbols.map(s => s.symbolName),
1132
+ propertyTexts: new Map(symbols.map(s => [s.symbolName, mockProperties.get(s.symbolName).text])),
1133
+ nameMapping,
1134
+ hasDefaultExport
1135
+ });
1136
+ }
1137
+ if (unmappedSymbols.length > 0) {
1138
+ groups.push({
1139
+ exportPath: mockContext.currentExportPath,
1140
+ importPath: mockContext.importPath,
1141
+ propertyNames: unmappedSymbols,
1142
+ propertyTexts: new Map(unmappedSymbols.map(s => [s, mockProperties.get(s).text])),
1143
+ nameMapping: new Map(),
1144
+ hasDefaultExport: false
1145
+ });
1146
+ }
1147
+ context.report({
1148
+ node: node,
1149
+ messageId: 'barrelEntryMock',
1150
+ data: {
1151
+ path: importPath
1152
+ },
1153
+ fix(fixer) {
1154
+ const firstArg = node.arguments[0];
1155
+ const quote = sourceCode.getText(firstArg)[0];
1156
+
1157
+ // Build a mapping from old import path to new import paths (with their symbols)
1158
+ // so we can update jest.requireMock() calls later
1159
+ const oldImportPath = importPath;
1160
+
1161
+ // Find all existing jest.mock calls in the file
1162
+ const allExistingMocks = findAllJestMocksInFile({
1163
+ context
1164
+ });
1165
+
1166
+ // Track nodes to remove and merged mock info
1167
+ const nodesToRemove = [node];
1168
+ const mergedGroups = [];
1169
+ for (const group of groups) {
1170
+ const existingMock = allExistingMocks.get(group.importPath);
1171
+ if (existingMock && existingMock.node !== node) {
1172
+ // Merge properties from existing mock with new properties
1173
+ const newPropertiesMap = new Map();
1174
+ for (const propName of group.propertyNames) {
1175
+ const propInfo = mockProperties.get(propName);
1176
+ if (propInfo) {
1177
+ // Check if this property needs to be renamed (aliased export)
1178
+ const originalName = group.nameMapping.get(propName);
1179
+ if (originalName && originalName !== propName) {
1180
+ const renamedText = replacePropertyKey({
1181
+ propText: propInfo.text,
1182
+ mockName: propName,
1183
+ originalName
1184
+ });
1185
+ newPropertiesMap.set(originalName, {
1186
+ node: propInfo.node,
1187
+ text: renamedText
1188
+ });
1189
+ } else {
1190
+ newPropertiesMap.set(propName, propInfo);
1191
+ }
1192
+ }
1193
+ }
1194
+ const mergedProperties = mergeMockProperties({
1195
+ existingProperties: existingMock.properties,
1196
+ newProperties: newPropertiesMap
1197
+ });
1198
+
1199
+ // Create merged group with all properties
1200
+ mergedGroups.push({
1201
+ exportPath: group.exportPath,
1202
+ importPath: group.importPath,
1203
+ propertyNames: Array.from(mergedProperties.keys()),
1204
+ propertyTexts: new Map(Array.from(mergedProperties.entries()).map(([k, v]) => [k, v.text])),
1205
+ nameMapping: new Map(),
1206
+ // Already applied above
1207
+ hasDefaultExport: group.hasDefaultExport
1208
+ });
1209
+
1210
+ // Mark existing mock for removal
1211
+ nodesToRemove.push(existingMock.node);
1212
+ } else {
1213
+ // No existing mock, use the group as-is
1214
+ mergedGroups.push(group);
1215
+ }
1216
+ }
1217
+ const symbolToNewImportPath = new Map();
1218
+ for (const group of [...mergedGroups, ...crossPackageMockGroups]) {
1219
+ for (const propName of group.propertyNames) {
1220
+ symbolToNewImportPath.set(propName, group.importPath);
1221
+ }
1222
+ }
1223
+ let fixText = generateMockFixes({
1224
+ groups: mergedGroups,
1225
+ crossPackageGroups: crossPackageMockGroups,
1226
+ packageName: mockContext.packageName,
1227
+ mockProperties,
1228
+ quote,
1229
+ preambleStatements,
1230
+ propagateEsModuleFromOriginalMock: originalMockHadEsModule,
1231
+ oldBarrelImportPath: oldImportPath,
1232
+ symbolToNewImportPath
1233
+ });
1234
+
1235
+ // Post-process fixText to update jest.requireActual('barrel').Symbol
1236
+ // references embedded in property texts (e.g. inside jest.fn callbacks)
1237
+ fixText = fixText.replace(/jest\.requireActual(?:<[^>]*>)?\((['"])([^'"]+)\1\)\.(\w+)/g, (match, _q, path, symbol) => {
1238
+ if (path !== oldImportPath) {
1239
+ return match;
1240
+ }
1241
+ const newPath = symbolToNewImportPath.get(symbol);
1242
+ if (newPath && newPath !== path) {
1243
+ return match.replace(path, newPath);
1244
+ }
1245
+ return match;
1246
+ });
1247
+
1248
+ // Sort nodes by position
1249
+ const sortedNodesToRemove = nodesToRemove.sort((a, b) => {
1250
+ var _a$range$, _a$range, _b$range$, _b$range;
1251
+ return ((_a$range$ = (_a$range = a.range) === null || _a$range === void 0 ? void 0 : _a$range[0]) !== null && _a$range$ !== void 0 ? _a$range$ : 0) - ((_b$range$ = (_b$range = b.range) === null || _b$range === void 0 ? void 0 : _b$range[0]) !== null && _b$range$ !== void 0 ? _b$range$ : 0);
1252
+ });
1253
+ const fixes = [];
1254
+ if (sortedNodesToRemove.length === 1) {
1255
+ // Simple case: just replace the current node
1256
+ fixes.push(fixer.replaceText(node, fixText));
1257
+ } else {
1258
+ // Complex case: replace first node, remove others
1259
+ // Replace the first node with the merged mocks
1260
+ fixes.push(fixer.replaceText(sortedNodesToRemove[0], fixText));
1261
+
1262
+ // Remove remaining nodes
1263
+ for (let i = 1; i < sortedNodesToRemove.length; i++) {
1264
+ const nodeToRemove = sortedNodesToRemove[i];
1265
+ const tokenAfter = sourceCode.getTokenAfter(nodeToRemove);
1266
+ let startPos = nodeToRemove.range[0];
1267
+ let endPos = nodeToRemove.range[1];
1268
+
1269
+ // Include trailing semicolon if present
1270
+ if (tokenAfter && tokenAfter.type === 'Punctuator' && tokenAfter.value === ';') {
1271
+ endPos = tokenAfter.range[1];
1272
+ }
1273
+
1274
+ // Include trailing whitespace and newlines
1275
+ const text = sourceCode.getText();
1276
+ while (endPos < text.length && /[\s\n]/.test(text[endPos])) {
1277
+ endPos++;
1278
+ }
1279
+ fixes.push(fixer.removeRange([startPos, endPos]));
1280
+ }
1281
+ }
1282
+
1283
+ // Fix jest.requireMock() calls that reference the old barrel path.
1284
+ // When we split a jest.mock('pkg/barrel') into jest.mock('pkg/subpath'),
1285
+ // any jest.requireMock('pkg/barrel') calls also need to be updated.
1286
+ const ast = sourceCode.ast;
1287
+ const requireMockCalls = findJestRequireMockCalls({
1288
+ ast,
1289
+ matchPath: candidatePath => candidatePath === oldImportPath
1290
+ });
1291
+ for (const requireMockNode of requireMockCalls) {
1292
+ const requireMockArg = requireMockNode.arguments[0];
1293
+ if (!requireMockArg) {
1294
+ continue;
1295
+ }
1296
+ const newPath = resolveNewPathForRequireMock({
1297
+ requireMockNode,
1298
+ symbolToNewPath: symbolToNewImportPath
1299
+ });
1300
+ if (newPath) {
1301
+ fixes.push(fixer.replaceText(requireMockArg, `${quote}${newPath}${quote}`));
1302
+ }
1303
+ }
1304
+
1305
+ // Fix jest.requireActual() calls that reference the old barrel path.
1306
+ // Only fix calls OUTSIDE the replaced jest.mock node range
1307
+ // (calls inside it are handled via fixText string replacement below).
1308
+ const replacedRanges = sortedNodesToRemove.map(n => n.range);
1309
+ const requireActualCalls = findJestRequireActualCalls({
1310
+ ast,
1311
+ matchPath: candidatePath => candidatePath === oldImportPath
1312
+ });
1313
+ for (const raNode of requireActualCalls) {
1314
+ const raArg = raNode.arguments[0];
1315
+ if (!raArg || !raNode.range) {
1316
+ continue;
1317
+ }
1318
+
1319
+ // Skip calls inside any node being replaced (ranges overlap)
1320
+ const insideReplacedNode = replacedRanges.some(([start, end]) => raNode.range[0] >= start && raNode.range[1] <= end);
1321
+ if (insideReplacedNode) {
1322
+ continue;
1323
+ }
1324
+ const newPath = resolveNewPathForRequireMock({
1325
+ requireMockNode: raNode,
1326
+ symbolToNewPath: symbolToNewImportPath
1327
+ });
1328
+ if (newPath) {
1329
+ fixes.push(fixer.replaceText(raArg, `${quote}${newPath}${quote}`));
1330
+ }
1331
+ }
1332
+ return fixes;
1333
+ }
1334
+ });
1335
+ }
1336
+ };
1337
+ }
1338
+ };
1339
+ }
1340
+ const rule = createRule(realFileSystem);
1341
+ export default rule;