@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,1180 @@
1
+ import { dirname, relative } from 'path';
2
+ import * as ts from 'typescript';
3
+ import { hasReExportsFromOtherFiles, parseBarrelExports } from '../shared/barrel-parsing';
4
+ import { findWorkspaceRoot, isRelativeImport, resolveImportPath } from '../shared/file-system';
5
+ import { extractImportPath, findJestRequireMockCalls, isJestMockCall, isJestRequireActual, resolveNewPathForRequireMock } from '../shared/jest-utils';
6
+ import { findPackageInRegistry } from '../shared/package-registry';
7
+ import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
8
+ import { realFileSystem } from '../shared/types';
9
+
10
+ // Cache per source package name to avoid repeated exports parsing during a single lint run.
11
+ // This is keyed by fs instance to avoid test pollution.
12
+ const sourcePackageExportsMapsByFs = new WeakMap();
13
+ function getSourcePackageExportsMaps(fs) {
14
+ let map = sourcePackageExportsMapsByFs.get(fs);
15
+ if (!map) {
16
+ map = new Map();
17
+ sourcePackageExportsMapsByFs.set(fs, map);
18
+ }
19
+ return map;
20
+ }
21
+
22
+ /**
23
+ * Information about a mock factory's preamble (statements before the return)
24
+ */
25
+
26
+ /**
27
+ * Extract identifiers defined by a statement (e.g., variable declarations)
28
+ * Uses TypeScript AST to find declared identifiers.
29
+ */
30
+ function extractDefinedIdentifiers(statementText) {
31
+ const identifiers = new Set();
32
+ try {
33
+ // Parse the statement as a mini source file
34
+ const sourceFile = ts.createSourceFile('temp.ts', statementText, ts.ScriptTarget.Latest, true);
35
+ const visit = node => {
36
+ if (ts.isVariableStatement(node)) {
37
+ for (const decl of node.declarationList.declarations) {
38
+ if (ts.isIdentifier(decl.name)) {
39
+ identifiers.add(decl.name.text);
40
+ } else if (ts.isObjectBindingPattern(decl.name)) {
41
+ // Handle destructuring: const { a, b } = ...
42
+ for (const element of decl.name.elements) {
43
+ if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
44
+ identifiers.add(element.name.text);
45
+ }
46
+ }
47
+ } else if (ts.isArrayBindingPattern(decl.name)) {
48
+ // Handle array destructuring: const [a, b] = ...
49
+ for (const element of decl.name.elements) {
50
+ if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) {
51
+ identifiers.add(element.name.text);
52
+ }
53
+ }
54
+ }
55
+ }
56
+ } else if (ts.isFunctionDeclaration(node) && node.name) {
57
+ identifiers.add(node.name.text);
58
+ } else if (ts.isClassDeclaration(node) && node.name) {
59
+ identifiers.add(node.name.text);
60
+ }
61
+ ts.forEachChild(node, visit);
62
+ };
63
+ ts.forEachChild(sourceFile, visit);
64
+ } catch {
65
+ // Ignore parsing errors
66
+ }
67
+ return identifiers;
68
+ }
69
+
70
+ /**
71
+ * Find all identifiers used in a given text string.
72
+ * Uses a simple regex approach to find potential identifier references.
73
+ */
74
+ function findUsedIdentifiers(text, potentialIdentifiers) {
75
+ const used = new Set();
76
+ for (const identifier of potentialIdentifiers) {
77
+ // Use word boundary matching to find identifier usage
78
+ // This matches the identifier as a whole word (not part of another word)
79
+ const regex = new RegExp(`\\b${escapeRegExpForIdentifier(identifier)}\\b`);
80
+ if (regex.test(text)) {
81
+ used.add(identifier);
82
+ }
83
+ }
84
+ return used;
85
+ }
86
+
87
+ /**
88
+ * Escape special regex characters for identifier matching
89
+ */
90
+ function escapeRegExpForIdentifier(str) {
91
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
92
+ }
93
+
94
+ /**
95
+ * Filter preamble statements to only include those whose defined identifiers
96
+ * are used in the given property texts.
97
+ */
98
+ function filterPreambleForProperties(preamble, propertyTexts) {
99
+ if (!preamble.hasPreamble || preamble.statements.length === 0) {
100
+ return preamble;
101
+ }
102
+
103
+ // Collect all identifiers defined in the preamble
104
+ const allDefinedIdentifiers = new Set();
105
+ for (const stmt of preamble.statements) {
106
+ for (const id of stmt.definedIdentifiers) {
107
+ allDefinedIdentifiers.add(id);
108
+ }
109
+ }
110
+
111
+ // Find which identifiers are used in the property texts
112
+ const combinedPropertyText = propertyTexts.join('\n');
113
+ const usedIdentifiers = findUsedIdentifiers(combinedPropertyText, allDefinedIdentifiers);
114
+
115
+ // Filter statements to only those that define used identifiers
116
+ const filteredStatements = preamble.statements.filter(stmt => {
117
+ // Include statement if any of its defined identifiers are used
118
+ for (const id of stmt.definedIdentifiers) {
119
+ if (usedIdentifiers.has(id)) {
120
+ return true;
121
+ }
122
+ }
123
+ return false;
124
+ });
125
+ if (filteredStatements.length === 0) {
126
+ return {
127
+ text: '',
128
+ hasPreamble: false,
129
+ statements: []
130
+ };
131
+ }
132
+ return {
133
+ text: filteredStatements.map(s => s.text).join('\n\t'),
134
+ hasPreamble: true,
135
+ statements: filteredStatements
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Convert absolute file path to an import path, handling cross-package resolution.
141
+ * If the export comes from a cross-package source, returns the package path (e.g., '@atlassian/package-b/utils').
142
+ * Otherwise, returns a relative path.
143
+ */
144
+ function getImportPathForSourceFile({
145
+ sourceFilePath,
146
+ basedir,
147
+ originalImportPath,
148
+ exportInfo,
149
+ workspaceRoot,
150
+ fs
151
+ }) {
152
+ var _exportInfo$crossPack;
153
+ const crossPackageName = exportInfo === null || exportInfo === void 0 ? void 0 : (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
154
+ if (crossPackageName) {
155
+ var _findExportForSourceF;
156
+ const sourcePackageExportsMaps = getSourcePackageExportsMaps(fs);
157
+ let exportsMap = sourcePackageExportsMaps.get(crossPackageName);
158
+ if (!exportsMap) {
159
+ const pkgDir = findPackageInRegistry({
160
+ packageName: crossPackageName,
161
+ workspaceRoot,
162
+ fs
163
+ });
164
+ if (pkgDir) {
165
+ exportsMap = parsePackageExports({
166
+ packageDir: pkgDir,
167
+ fs
168
+ });
169
+ sourcePackageExportsMaps.set(crossPackageName, exportsMap);
170
+ }
171
+ }
172
+ const targetExportPath = exportsMap ? (_findExportForSourceF = findExportForSourceFile({
173
+ sourceFilePath,
174
+ exportsMap
175
+ })) === null || _findExportForSourceF === void 0 ? void 0 : _findExportForSourceF.exportPath : null;
176
+ return targetExportPath ? crossPackageName + targetExportPath.slice(1) : crossPackageName;
177
+ }
178
+ return getRelativeImportPath({
179
+ basedir,
180
+ absolutePath: sourceFilePath,
181
+ originalImportPath
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Convert absolute file path back to relative import path
187
+ */
188
+ function getRelativeImportPath({
189
+ basedir,
190
+ absolutePath,
191
+ originalImportPath
192
+ }) {
193
+ let relativePath = relative(basedir, absolutePath);
194
+ // Normalize to use forward slashes
195
+ relativePath = relativePath.replace(/\\/g, '/');
196
+
197
+ // Check for extension in original path
198
+ const extMatch = originalImportPath.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/);
199
+ const originalExt = extMatch ? extMatch[0] : '';
200
+
201
+ // Get extension from the resolved absolute path
202
+ const targetExtMatch = absolutePath.match(/\.(js|jsx|ts|tsx|mjs|cjs)$/);
203
+ const targetExt = targetExtMatch ? targetExtMatch[0] : '';
204
+
205
+ // Remove file extension from the target path
206
+ relativePath = relativePath.replace(/\.(ts|tsx|js|jsx|mjs|cjs)$/, '');
207
+
208
+ // If original had extension, append it
209
+ if (originalExt) {
210
+ // If original was a TypeScript source extension, use the actual target extension
211
+ if (['.ts', '.tsx'].includes(originalExt) && targetExt) {
212
+ relativePath += targetExt;
213
+ } else {
214
+ relativePath += originalExt;
215
+ }
216
+ } else {
217
+ // Remove /index suffix only if no extension was present
218
+ if (relativePath.endsWith('/index')) {
219
+ relativePath = relativePath.slice(0, -6);
220
+ } else if (relativePath === 'index') {
221
+ relativePath = '.';
222
+ }
223
+ }
224
+
225
+ // Ensure it starts with .. or .
226
+ if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
227
+ relativePath = './' + relativePath;
228
+ }
229
+ return relativePath;
230
+ }
231
+
232
+ /**
233
+ * Check if a node is an Object.assign call
234
+ */
235
+ function isObjectAssignCall(node) {
236
+ if (node.type !== 'CallExpression') {
237
+ return false;
238
+ }
239
+ const callee = node.callee;
240
+ if (callee.type === 'MemberExpression') {
241
+ return callee.object.type === 'Identifier' && callee.object.name === 'Object' && callee.property.type === 'Identifier' && callee.property.name === 'assign';
242
+ }
243
+ return false;
244
+ }
245
+
246
+ /**
247
+ * Extract mock object from Object.assign pattern
248
+ * Pattern: Object.assign({}, jest.requireActual(...), { mockProps })
249
+ * Returns the properties object and whether it has requireActual
250
+ */
251
+ function extractObjectAssignMock(node) {
252
+ const args = node.arguments;
253
+
254
+ // Object.assign typically has at least 2 arguments: target and source(s)
255
+ // Pattern: Object.assign({}, jest.requireActual(...), { mockProps })
256
+ // or: Object.assign({}, jest.requireActual(...), { mockProps1 }, { mockProps2 })
257
+ if (args.length < 2) {
258
+ return {
259
+ propertiesObject: null,
260
+ hasRequireActual: false
261
+ };
262
+ }
263
+ let hasRequireActual = false;
264
+ let lastObjectExpression = null;
265
+
266
+ // Scan through arguments to find jest.requireActual and the last object literal
267
+ for (const arg of args) {
268
+ if (isJestRequireActual(arg)) {
269
+ hasRequireActual = true;
270
+ }
271
+ if (arg.type === 'ObjectExpression') {
272
+ // Skip empty objects (the first {} in Object.assign({}, ...))
273
+ if (arg.properties.length > 0) {
274
+ lastObjectExpression = arg;
275
+ }
276
+ }
277
+ }
278
+ return {
279
+ propertiesObject: lastObjectExpression,
280
+ hasRequireActual
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Extract mock object properties from jest.mock call
286
+ * Returns a map of property name -> { node, text } and whether there's a jest.requireActual spread
287
+ */
288
+ function extractMockProperties({
289
+ sourceCode,
290
+ mockObjectNode
291
+ }) {
292
+ const properties = new Map();
293
+ let hasRequireActual = false;
294
+
295
+ // Handle Object.assign pattern: Object.assign({}, jest.requireActual(...), { props })
296
+ if (isObjectAssignCall(mockObjectNode)) {
297
+ const {
298
+ propertiesObject,
299
+ hasRequireActual: objectAssignHasRequireActual
300
+ } = extractObjectAssignMock(mockObjectNode);
301
+ if (propertiesObject) {
302
+ // Recursively extract properties from the properties object
303
+ const result = extractMockProperties({
304
+ sourceCode,
305
+ mockObjectNode: propertiesObject
306
+ });
307
+ return {
308
+ properties: result.properties,
309
+ hasRequireActual: objectAssignHasRequireActual || result.hasRequireActual
310
+ };
311
+ }
312
+ return {
313
+ properties,
314
+ hasRequireActual: objectAssignHasRequireActual
315
+ };
316
+ }
317
+ if (mockObjectNode.type === 'ObjectExpression') {
318
+ for (const prop of mockObjectNode.properties) {
319
+ if (prop.type === 'SpreadElement') {
320
+ // Check if this is ...jest.requireActual(...)
321
+ if (isJestRequireActual(prop.argument)) {
322
+ hasRequireActual = true;
323
+ }
324
+ } else if (prop.type === 'Property') {
325
+ let keyName;
326
+ if (prop.key.type === 'Identifier') {
327
+ keyName = prop.key.name;
328
+ } else if (prop.key.type === 'Literal') {
329
+ keyName = String(prop.key.value);
330
+ } else {
331
+ continue;
332
+ }
333
+ const propText = sourceCode.getText(prop);
334
+ const valueText = sourceCode.getText(prop.value);
335
+ properties.set(keyName, {
336
+ node: prop,
337
+ text: propText,
338
+ valueText
339
+ });
340
+ }
341
+ }
342
+ }
343
+ return {
344
+ properties,
345
+ hasRequireActual
346
+ };
347
+ }
348
+
349
+ /**
350
+ * Validate and resolve a barrel file from an import path
351
+ * Returns null if not a valid relative barrel import
352
+ */
353
+ export function validateAndResolveBarrelFile({
354
+ importPath,
355
+ basedir,
356
+ workspaceRoot,
357
+ fs
358
+ }) {
359
+ if (!isRelativeImport(importPath)) {
360
+ return null;
361
+ }
362
+ const resolvedPath = resolveImportPath({
363
+ basedir,
364
+ importPath,
365
+ fs
366
+ });
367
+ if (!resolvedPath) {
368
+ return null;
369
+ }
370
+ const exportMap = parseBarrelExports({
371
+ barrelFilePath: resolvedPath,
372
+ workspaceRoot,
373
+ fs
374
+ });
375
+ if (exportMap.size === 0) {
376
+ return null;
377
+ }
378
+
379
+ // A file is considered a barrel file if it has re-exports from other files.
380
+ // This is the semantic check - we don't care about the filename.
381
+ if (!hasReExportsFromOtherFiles({
382
+ exportMap,
383
+ sourceFilePath: resolvedPath
384
+ })) {
385
+ return null;
386
+ }
387
+ return {
388
+ resolvedPath,
389
+ exportMap
390
+ };
391
+ }
392
+
393
+ /**
394
+ * Extract the mock implementation object from the jest.mock call
395
+ */
396
+ function extractMockImplementation(mockImpl) {
397
+ if (mockImpl.type === 'ArrowFunctionExpression') {
398
+ if (mockImpl.body.type === 'ObjectExpression') {
399
+ return mockImpl.body;
400
+ }
401
+ // Handle arrow functions that return a call expression directly (e.g., Object.assign)
402
+ if (mockImpl.body.type === 'CallExpression') {
403
+ return mockImpl.body;
404
+ }
405
+ if (mockImpl.body.type === 'BlockStatement') {
406
+ const returnStmt = mockImpl.body.body.find(s => s.type === 'ReturnStatement');
407
+ if (returnStmt !== null && returnStmt !== void 0 && returnStmt.argument) {
408
+ return returnStmt.argument;
409
+ }
410
+ }
411
+ }
412
+ if (mockImpl.type === 'FunctionExpression' && mockImpl.body.type === 'BlockStatement') {
413
+ const returnStmt = mockImpl.body.body.find(s => s.type === 'ReturnStatement');
414
+ if (returnStmt !== null && returnStmt !== void 0 && returnStmt.argument) {
415
+ return returnStmt.argument;
416
+ }
417
+ }
418
+ return mockImpl;
419
+ }
420
+
421
+ /**
422
+ * Extract the preamble (statements before the return) from a mock factory function.
423
+ * This captures variable declarations, assignments, etc. that need to be preserved.
424
+ */
425
+ function extractMockFactoryPreamble({
426
+ mockImpl,
427
+ sourceCode
428
+ }) {
429
+ const emptyPreamble = {
430
+ text: '',
431
+ hasPreamble: false,
432
+ statements: []
433
+ };
434
+
435
+ // Get the block statement body from the mock factory
436
+ let blockBody = null;
437
+ if ((mockImpl.type === 'ArrowFunctionExpression' || mockImpl.type === 'FunctionExpression') && mockImpl.body.type === 'BlockStatement') {
438
+ blockBody = mockImpl.body.body;
439
+ }
440
+ if (!blockBody) {
441
+ return emptyPreamble;
442
+ }
443
+
444
+ // Find the return statement index
445
+ const returnIndex = blockBody.findIndex(s => s.type === 'ReturnStatement');
446
+ if (returnIndex <= 0) {
447
+ // No preamble (return is first statement or not found)
448
+ return emptyPreamble;
449
+ }
450
+
451
+ // Extract all statements before the return
452
+ const preambleStatements = blockBody.slice(0, returnIndex);
453
+ const statementsWithIdentifiers = preambleStatements.map(stmt => {
454
+ const text = sourceCode.getText(stmt);
455
+ const definedIdentifiers = extractDefinedIdentifiers(text);
456
+ return {
457
+ text,
458
+ definedIdentifiers
459
+ };
460
+ });
461
+ const preambleTexts = statementsWithIdentifiers.map(s => s.text);
462
+ return {
463
+ text: preambleTexts.join('\n\t'),
464
+ hasPreamble: true,
465
+ statements: statementsWithIdentifiers
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Rewrite jest.requireActual paths in a text string from the original barrel path to a new path.
471
+ */
472
+ function rewriteRequireActualPaths({
473
+ text,
474
+ originalPath,
475
+ newPath,
476
+ quote
477
+ }) {
478
+ // Match jest.requireActual('originalPath') or jest.requireActual("originalPath")
479
+ // Also handle the 'as Object' or 'as any' type assertions
480
+ const patterns = [
481
+ // With single quotes
482
+ new RegExp(`jest\\.requireActual\\(\\s*'${escapeRegExp(originalPath)}'\\s*\\)(?:\\s+as\\s+\\w+)?`, 'g'),
483
+ // With double quotes
484
+ new RegExp(`jest\\.requireActual\\(\\s*"${escapeRegExp(originalPath)}"\\s*\\)(?:\\s+as\\s+\\w+)?`, 'g')];
485
+ let result = text;
486
+ for (const pattern of patterns) {
487
+ result = result.replace(pattern, `jest.requireActual(${quote}${newPath}${quote})`);
488
+ }
489
+ return result;
490
+ }
491
+
492
+ /**
493
+ * Escape special regex characters in a string
494
+ */
495
+ function escapeRegExp(str) {
496
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
497
+ }
498
+
499
+ /**
500
+ * Group mocked properties by their source file
501
+ */
502
+ function groupPropertiesBySource({
503
+ mockProperties,
504
+ exportMap
505
+ }) {
506
+ const propertiesBySource = new Map();
507
+ for (const [propName] of mockProperties) {
508
+ const exportInfo = exportMap.get(propName);
509
+ if (!exportInfo) {
510
+ continue;
511
+ }
512
+ if (!propertiesBySource.has(exportInfo.path)) {
513
+ propertiesBySource.set(exportInfo.path, []);
514
+ }
515
+ propertiesBySource.get(exportInfo.path).push(propName);
516
+ }
517
+ return propertiesBySource;
518
+ }
519
+
520
+ /**
521
+ * Determine if we should report a barrel mock violation
522
+ */
523
+ function shouldReportBarrelMock({
524
+ propertiesBySource,
525
+ barrelFilePath
526
+ }) {
527
+ // Report if any mocked property is a re-export (comes from a different file than the barrel)
528
+ // This catches both:
529
+ // 1. Properties from multiple source files
530
+ // 2. Properties from a single source file that isn't the barrel itself
531
+ for (const sourcePath of propertiesBySource.keys()) {
532
+ if (sourcePath !== barrelFilePath) {
533
+ return true;
534
+ }
535
+ }
536
+ return false;
537
+ }
538
+
539
+ /**
540
+ * Generate auto-fix for auto-mock case (no mock implementation)
541
+ */
542
+ function generateAutoMockFix({
543
+ exportMap,
544
+ basedir,
545
+ importPath,
546
+ quote,
547
+ workspaceRoot,
548
+ fs
549
+ }) {
550
+ // Group exports by source file, filtering out type-only source files
551
+ // Also track the ExportInfo for cross-package resolution
552
+ const sourceFilesWithInfo = new Map();
553
+ for (const [, info] of exportMap) {
554
+ if (!info.isTypeOnly && !sourceFilesWithInfo.has(info.path)) {
555
+ sourceFilesWithInfo.set(info.path, info);
556
+ }
557
+ }
558
+ const sourceFileArray = Array.from(sourceFilesWithInfo.entries());
559
+ return sourceFileArray.map(([sourceFile, exportInfo]) => {
560
+ const mockPath = getImportPathForSourceFile({
561
+ sourceFilePath: sourceFile,
562
+ basedir,
563
+ originalImportPath: importPath,
564
+ exportInfo,
565
+ workspaceRoot,
566
+ fs
567
+ });
568
+ return `jest.mock(${quote}${mockPath}${quote})`;
569
+ }).join(';\n');
570
+ }
571
+
572
+ /**
573
+ * Normalize a path for comparison (resolve to absolute path)
574
+ */
575
+ function normalizePathForComparison({
576
+ basedir,
577
+ importPath,
578
+ fs
579
+ }) {
580
+ if (!isRelativeImport(importPath)) {
581
+ // For non-relative imports, just return as-is for comparison
582
+ return importPath;
583
+ }
584
+ const resolved = resolveImportPath({
585
+ basedir,
586
+ importPath,
587
+ fs
588
+ });
589
+ return resolved || importPath;
590
+ }
591
+
592
+ /**
593
+ * Scan the entire file for all existing jest.mock calls
594
+ * Returns a map of normalized path -> { node, properties, hasRequireActual }
595
+ */
596
+ function findAllJestMocksInFile({
597
+ context,
598
+ basedir,
599
+ fs
600
+ }) {
601
+ const allMocks = new Map();
602
+ const sourceCode = context.getSourceCode();
603
+ const ast = sourceCode.ast;
604
+
605
+ // Use a visited set to prevent infinite recursion
606
+ const visited = new Set();
607
+
608
+ // Properties to skip to avoid circular references
609
+ const skipKeys = new Set(['parent', 'loc', 'range', 'tokens', 'comments']);
610
+ function visitNode(node) {
611
+ // Prevent revisiting nodes
612
+ if (visited.has(node)) {
613
+ return;
614
+ }
615
+ visited.add(node);
616
+ if (node.type === 'CallExpression' && isJestMockCall(node)) {
617
+ const importPath = extractImportPath(node);
618
+ if (importPath) {
619
+ const normalizedPath = normalizePathForComparison({
620
+ basedir,
621
+ importPath,
622
+ fs
623
+ });
624
+ const mockImpl = node.arguments[1];
625
+ if (mockImpl) {
626
+ const mockObjectNode = extractMockImplementation(mockImpl);
627
+ const {
628
+ properties,
629
+ hasRequireActual
630
+ } = extractMockProperties({
631
+ sourceCode,
632
+ mockObjectNode
633
+ });
634
+ allMocks.set(normalizedPath, {
635
+ node,
636
+ importPath,
637
+ properties,
638
+ hasRequireActual
639
+ });
640
+ }
641
+ }
642
+ }
643
+
644
+ // Recursively visit child nodes
645
+ for (const key in node) {
646
+ if (skipKeys.has(key)) {
647
+ continue;
648
+ }
649
+ const value = node[key];
650
+ if (value && typeof value === 'object') {
651
+ if (Array.isArray(value)) {
652
+ value.forEach(child => {
653
+ if (child && typeof child === 'object' && 'type' in child) {
654
+ visitNode(child);
655
+ }
656
+ });
657
+ } else if ('type' in value) {
658
+ visitNode(value);
659
+ }
660
+ }
661
+ }
662
+ }
663
+ visitNode(ast);
664
+ return allMocks;
665
+ }
666
+
667
+ /**
668
+ * Merge mock properties from multiple sources for the same file
669
+ */
670
+ function mergeMockProperties({
671
+ existingProperties,
672
+ newProperties
673
+ }) {
674
+ const merged = new Map(existingProperties);
675
+ for (const [key, value] of newProperties) {
676
+ merged.set(key, value);
677
+ }
678
+ return merged;
679
+ }
680
+
681
+ /**
682
+ * Generate mock call text for a specific file with given properties
683
+ */
684
+ function generateMockCallText({
685
+ relativePath,
686
+ properties,
687
+ hasRequireActual,
688
+ quote,
689
+ exportMap,
690
+ sourceFile,
691
+ preamble,
692
+ originalImportPath
693
+ }) {
694
+ const propNames = Array.from(properties.keys());
695
+
696
+ // Separate props by whether they're from default exports
697
+ const defaultExportProps = [];
698
+ const namedExportProps = [];
699
+ for (const prop of propNames) {
700
+ const exportInfo = Array.from(exportMap.entries()).find(([exportName, info]) => exportName === prop && info.path === sourceFile);
701
+ if (exportInfo && exportInfo[1].isDefaultExport) {
702
+ defaultExportProps.push(prop);
703
+ } else {
704
+ namedExportProps.push(prop);
705
+ }
706
+ }
707
+
708
+ // Collect all property texts for filtering the preamble
709
+ const allPropertyTexts = [];
710
+ for (const prop of namedExportProps) {
711
+ var _properties$get;
712
+ const propText = (_properties$get = properties.get(prop)) === null || _properties$get === void 0 ? void 0 : _properties$get.text;
713
+ if (propText) {
714
+ allPropertyTexts.push(propText);
715
+ }
716
+ }
717
+ for (const prop of defaultExportProps) {
718
+ var _properties$get2;
719
+ const propText = (_properties$get2 = properties.get(prop)) === null || _properties$get2 === void 0 ? void 0 : _properties$get2.valueText;
720
+ if (propText) {
721
+ allPropertyTexts.push(propText);
722
+ }
723
+ }
724
+
725
+ // Filter preamble to only include statements used by this mock's properties
726
+ const filteredPreamble = preamble ? filterPreambleForProperties(preamble, allPropertyTexts) : undefined;
727
+
728
+ // If we have a preamble, we need to use block body syntax with return statement
729
+ if (filteredPreamble !== null && filteredPreamble !== void 0 && filteredPreamble.hasPreamble) {
730
+ // Rewrite any jest.requireActual paths in the preamble
731
+ let preambleText = filteredPreamble.text;
732
+ if (originalImportPath) {
733
+ preambleText = rewriteRequireActualPaths({
734
+ text: preambleText,
735
+ originalPath: originalImportPath,
736
+ newPath: relativePath,
737
+ quote
738
+ });
739
+ }
740
+
741
+ // Rewrite any jest.requireActual paths in property values
742
+ const rewrittenMockObjectProps = namedExportProps.map(p => {
743
+ var _properties$get3;
744
+ let propText = (_properties$get3 = properties.get(p)) === null || _properties$get3 === void 0 ? void 0 : _properties$get3.text;
745
+ if (propText && originalImportPath) {
746
+ propText = rewriteRequireActualPaths({
747
+ text: propText,
748
+ originalPath: originalImportPath,
749
+ newPath: relativePath,
750
+ quote
751
+ });
752
+ }
753
+ return propText;
754
+ }).filter(p => !!p);
755
+ const mockContentLines = [];
756
+ if (hasRequireActual) {
757
+ mockContentLines.push(`...jest.requireActual(${quote}${relativePath}${quote})`);
758
+ }
759
+ mockContentLines.push(...rewrittenMockObjectProps);
760
+ const formattedContent = mockContentLines.map(line => `\t\t${line},`).join('\n');
761
+ return `jest.mock(${quote}${relativePath}${quote}, () => {\n\t${preambleText}\n\treturn {\n${formattedContent}\n\t};\n})`;
762
+ }
763
+
764
+ // Generate the mock (original logic for simple cases without preamble)
765
+ let mockCall;
766
+ if (defaultExportProps.length > 0 && namedExportProps.length === 0) {
767
+ // All props are from default export
768
+ if (defaultExportProps.length === 1) {
769
+ var _properties$get4;
770
+ // Single default export - use __esModule pattern
771
+ const mockText = ((_properties$get4 = properties.get(defaultExportProps[0])) === null || _properties$get4 === void 0 ? void 0 : _properties$get4.valueText) || '';
772
+ mockCall = `jest.mock(${quote}${relativePath}${quote}, () => ({\n\t__esModule: true,\n\tdefault: ${mockText}\n}))`;
773
+ } else {
774
+ // Multiple props from same default - shouldn't happen, but handle it
775
+ const mockTexts = defaultExportProps.map(p => {
776
+ var _properties$get5;
777
+ return (_properties$get5 = properties.get(p)) === null || _properties$get5 === void 0 ? void 0 : _properties$get5.text;
778
+ }).join(',\n\t');
779
+ mockCall = `jest.mock(${quote}${relativePath}${quote}, () => ({\n\t${mockTexts}\n}))`;
780
+ }
781
+ } else if (defaultExportProps.length === 0 && namedExportProps.length > 0) {
782
+ // All props are named exports
783
+ const mockObjectProps = namedExportProps.map(p => {
784
+ var _properties$get6;
785
+ return (_properties$get6 = properties.get(p)) === null || _properties$get6 === void 0 ? void 0 : _properties$get6.text;
786
+ }).filter(p => !!p);
787
+ const mockContentLines = [];
788
+ if (hasRequireActual) {
789
+ mockContentLines.push(`...jest.requireActual(${quote}${relativePath}${quote})`);
790
+ }
791
+ mockContentLines.push(...mockObjectProps);
792
+ if (mockContentLines.length === 1 && mockContentLines[0].length < 60) {
793
+ mockCall = `jest.mock(${quote}${relativePath}${quote}, () => ({ ${mockContentLines[0]} }))`;
794
+ } else {
795
+ const formattedContent = mockContentLines.map(line => `\t${line},`).join('\n');
796
+ mockCall = `jest.mock(${quote}${relativePath}${quote}, () => ({\n${formattedContent}\n}))`;
797
+ }
798
+ } else {
799
+ // Mixed: has both default and named exports
800
+ const defaultMock = defaultExportProps.map(p => {
801
+ var _properties$get7;
802
+ return (_properties$get7 = properties.get(p)) === null || _properties$get7 === void 0 ? void 0 : _properties$get7.valueText;
803
+ }).join(', ');
804
+ const namedMocks = namedExportProps.map(p => {
805
+ var _properties$get8;
806
+ return (_properties$get8 = properties.get(p)) === null || _properties$get8 === void 0 ? void 0 : _properties$get8.text;
807
+ }).filter(p => !!p);
808
+ const mockContentLines = [`__esModule: true`, `default: ${defaultMock}`, ...namedMocks];
809
+ if (hasRequireActual) {
810
+ mockContentLines.unshift(`...jest.requireActual(${quote}${relativePath}${quote})`);
811
+ }
812
+ const formattedContent = mockContentLines.map(line => `\t${line},`).join('\n');
813
+ mockCall = `jest.mock(${quote}${relativePath}${quote}, () => ({\n${formattedContent}\n}))`;
814
+ }
815
+ return mockCall;
816
+ }
817
+
818
+ /**
819
+ * Generate auto-fix for mock with implementation
820
+ */
821
+ function generateMockImplementationFix({
822
+ propertiesBySource,
823
+ mockProperties,
824
+ hasRequireActual,
825
+ basedir,
826
+ importPath,
827
+ quote,
828
+ exportMap,
829
+ context,
830
+ currentNode,
831
+ preamble,
832
+ workspaceRoot,
833
+ fs
834
+ }) {
835
+ const sourceFilesToMock = Array.from(propertiesBySource.entries());
836
+
837
+ // Find all existing jest.mock calls in the file
838
+ const allExistingMocks = findAllJestMocksInFile({
839
+ context,
840
+ basedir,
841
+ fs
842
+ });
843
+
844
+ // Track which nodes we need to remove and what mock calls to generate
845
+ const nodesToRemove = new Set();
846
+ const mergedMocks = new Map();
847
+
848
+ // Always remove the current barrel mock node
849
+ nodesToRemove.add(currentNode);
850
+
851
+ // Process each source file we're creating mocks for
852
+ for (const [sourceFile, props] of sourceFilesToMock) {
853
+ // Find the ExportInfo for this source file to get cross-package info
854
+ const exportInfoForSource = Array.from(exportMap.values()).find(info => info.path === sourceFile);
855
+ const mockPath = getImportPathForSourceFile({
856
+ sourceFilePath: sourceFile,
857
+ basedir,
858
+ originalImportPath: importPath,
859
+ exportInfo: exportInfoForSource !== null && exportInfoForSource !== void 0 ? exportInfoForSource : null,
860
+ workspaceRoot,
861
+ fs
862
+ });
863
+ const normalizedPath = normalizePathForComparison({
864
+ basedir,
865
+ importPath: mockPath,
866
+ fs
867
+ });
868
+
869
+ // Get properties for this source file from the barrel mock
870
+ const newProperties = new Map();
871
+ for (const prop of props) {
872
+ const propInfo = mockProperties.get(prop);
873
+ if (propInfo) {
874
+ newProperties.set(prop, propInfo);
875
+ }
876
+ }
877
+
878
+ // Check if there's already a mock for this path
879
+ const existingMock = allExistingMocks.get(normalizedPath);
880
+ if (existingMock && existingMock.node !== currentNode) {
881
+ // Merge properties from existing mock with new properties
882
+ const mergedProperties = mergeMockProperties({
883
+ existingProperties: existingMock.properties,
884
+ newProperties
885
+ });
886
+ mergedMocks.set(normalizedPath, {
887
+ mockPath,
888
+ properties: mergedProperties,
889
+ hasRequireActual: existingMock.hasRequireActual || hasRequireActual
890
+ });
891
+ // Mark the existing mock node for removal
892
+ nodesToRemove.add(existingMock.node);
893
+ } else {
894
+ // No existing mock, just use the new properties
895
+ // For newly created mocks from barrel file splits, always include jest.requireActual.
896
+ // This ensures that any properties not explicitly mocked will still be included from the original module.
897
+ mergedMocks.set(normalizedPath, {
898
+ mockPath,
899
+ properties: newProperties,
900
+ hasRequireActual: true
901
+ });
902
+ }
903
+ }
904
+
905
+ // Generate mock calls for all merged mocks
906
+ const replacementParts = [];
907
+ for (const [, mockInfo] of mergedMocks) {
908
+ // Find the source file for this mock path (may be relative or cross-package)
909
+ // For cross-package paths (starting with @), we don't need to resolve
910
+ const isCrossPackagePath = mockInfo.mockPath.startsWith('@');
911
+ const absolutePath = isCrossPackagePath ? null : resolveImportPath({
912
+ basedir,
913
+ importPath: mockInfo.mockPath,
914
+ fs
915
+ });
916
+ if (!isCrossPackagePath && !absolutePath) {
917
+ continue;
918
+ }
919
+ const mockCall = generateMockCallText({
920
+ relativePath: mockInfo.mockPath,
921
+ properties: mockInfo.properties,
922
+ hasRequireActual: mockInfo.hasRequireActual,
923
+ quote,
924
+ exportMap,
925
+ sourceFile: absolutePath !== null && absolutePath !== void 0 ? absolutePath : mockInfo.mockPath,
926
+ preamble,
927
+ originalImportPath: importPath
928
+ });
929
+ replacementParts.push(mockCall);
930
+ }
931
+ const replacementText = replacementParts.join(';\n');
932
+
933
+ // Build a map of symbol name -> new mock path for jest.requireMock() rewriting
934
+ const symbolToNewMockPath = new Map();
935
+ for (const [, mockInfo] of mergedMocks) {
936
+ for (const propName of mockInfo.properties.keys()) {
937
+ symbolToNewMockPath.set(propName, mockInfo.mockPath);
938
+ }
939
+ }
940
+
941
+ // Create fixes: remove all nodes except the first, replace the first with merged mocks
942
+ const fixes = [];
943
+ const sortedNodesToRemove = Array.from(nodesToRemove).sort((a, b) => {
944
+ var _a$range$, _a$range, _b$range$, _b$range;
945
+ 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);
946
+ });
947
+ if (sortedNodesToRemove.length > 0) {
948
+ // Replace the first node with all the merged mocks
949
+ const firstNode = sortedNodesToRemove[0];
950
+ fixes.push({
951
+ range: firstNode.range,
952
+ text: replacementText
953
+ });
954
+
955
+ // Remove all other nodes (subsequent duplicates)
956
+ for (let i = 1; i < sortedNodesToRemove.length; i++) {
957
+ const nodeToRemove = sortedNodesToRemove[i];
958
+ // Find the statement that contains this node to remove the entire line
959
+ const sourceCode = context.getSourceCode();
960
+ const tokenAfter = sourceCode.getTokenAfter(nodeToRemove);
961
+
962
+ // Try to remove the entire statement including semicolon and newline
963
+ let startPos = nodeToRemove.range[0];
964
+ let endPos = nodeToRemove.range[1];
965
+
966
+ // Include trailing semicolon if present
967
+ if (tokenAfter && tokenAfter.type === 'Punctuator' && tokenAfter.value === ';') {
968
+ endPos = tokenAfter.range[1];
969
+ }
970
+
971
+ // Include trailing/leading whitespace and newlines
972
+ const text = sourceCode.getText();
973
+ while (endPos < text.length && /[\s\n]/.test(text[endPos])) {
974
+ endPos++;
975
+ }
976
+ fixes.push({
977
+ range: [startPos, endPos],
978
+ text: ''
979
+ });
980
+ }
981
+ }
982
+
983
+ // Fix jest.requireMock() calls that reference the old barrel path.
984
+ // When we split a jest.mock('./barrel') into jest.mock('./specific-file'),
985
+ // any jest.requireMock('./barrel') calls also need to be updated.
986
+ const ast = context.getSourceCode().ast;
987
+ const normalizedTarget = normalizePathForComparison({
988
+ basedir,
989
+ importPath,
990
+ fs
991
+ });
992
+ const requireMockCalls = findJestRequireMockCalls({
993
+ ast,
994
+ matchPath: candidatePath => normalizePathForComparison({
995
+ basedir,
996
+ importPath: candidatePath,
997
+ fs
998
+ }) === normalizedTarget
999
+ });
1000
+ for (const requireMockNode of requireMockCalls) {
1001
+ const requireMockArg = requireMockNode.arguments[0];
1002
+ if (!requireMockArg) {
1003
+ continue;
1004
+ }
1005
+ const newPath = resolveNewPathForRequireMock({
1006
+ requireMockNode,
1007
+ symbolToNewPath: symbolToNewMockPath
1008
+ });
1009
+ if (newPath) {
1010
+ fixes.push({
1011
+ range: requireMockArg.range,
1012
+ text: `${quote}${newPath}${quote}`
1013
+ });
1014
+ }
1015
+ }
1016
+ return fixes;
1017
+ }
1018
+
1019
+ /**
1020
+ * Metadata for the ESLint rule
1021
+ */
1022
+ const ruleMeta = {
1023
+ type: 'problem',
1024
+ docs: {
1025
+ description: 'Warn when jest.mock is used on a relative import path from a barrel file, and provide an auto-fix to split mocks by source file.',
1026
+ category: 'Best Practices',
1027
+ recommended: false
1028
+ },
1029
+ fixable: 'code',
1030
+ messages: {
1031
+ barrelMock: "jest.mock('{{path}}') is mocking a barrel file. This should be split into separate mocks for each source file to improve performance. Use auto-fix to resolve."
1032
+ }
1033
+ };
1034
+
1035
+ /**
1036
+ * Factory function to create the ESLint rule with a given file system.
1037
+ * This enables testing with mock file systems.
1038
+ */
1039
+ export function createRule(fs) {
1040
+ return {
1041
+ meta: ruleMeta,
1042
+ create(context) {
1043
+ return {
1044
+ CallExpression(rawNode) {
1045
+ const node = rawNode;
1046
+
1047
+ // Step 1: Validate this is a jest.mock call
1048
+ if (!isJestMockCall(node)) {
1049
+ return;
1050
+ }
1051
+
1052
+ // Step 2: Extract the import path
1053
+ const importPath = extractImportPath(node);
1054
+ if (!importPath) {
1055
+ return;
1056
+ }
1057
+
1058
+ // Step 3: Validate and resolve barrel file
1059
+ const basedir = dirname(context.filename);
1060
+ const workspaceRoot = findWorkspaceRoot({
1061
+ startPath: basedir,
1062
+ fs
1063
+ });
1064
+ const barrelInfo = validateAndResolveBarrelFile({
1065
+ importPath,
1066
+ basedir,
1067
+ workspaceRoot,
1068
+ fs
1069
+ });
1070
+ if (!barrelInfo) {
1071
+ return;
1072
+ }
1073
+ const {
1074
+ exportMap,
1075
+ resolvedPath: barrelFilePath
1076
+ } = barrelInfo;
1077
+ const sourceCode = context.getSourceCode();
1078
+ const firstArg = node.arguments[0];
1079
+
1080
+ // Step 4: Handle auto-mock case (no mock implementation)
1081
+ const mockImpl = node.arguments[1];
1082
+ if (!mockImpl) {
1083
+ // Group exports by source file, filtering out type-only source files
1084
+ const sourceFilesWithNonTypeExports = new Set();
1085
+ for (const [, info] of exportMap) {
1086
+ if (!info.isTypeOnly) {
1087
+ sourceFilesWithNonTypeExports.add(info.path);
1088
+ }
1089
+ }
1090
+ if (sourceFilesWithNonTypeExports.size === 0) {
1091
+ return;
1092
+ }
1093
+ context.report({
1094
+ node: node,
1095
+ messageId: 'barrelMock',
1096
+ data: {
1097
+ path: importPath
1098
+ },
1099
+ fix(fixer) {
1100
+ const quote = sourceCode.getText(firstArg)[0];
1101
+ const replacement = generateAutoMockFix({
1102
+ exportMap,
1103
+ basedir,
1104
+ importPath,
1105
+ quote,
1106
+ workspaceRoot,
1107
+ fs
1108
+ });
1109
+ return fixer.replaceText(node, replacement);
1110
+ }
1111
+ });
1112
+ return;
1113
+ }
1114
+
1115
+ // Step 5: Extract mock implementation and properties
1116
+ const mockObjectNode = extractMockImplementation(mockImpl);
1117
+ const {
1118
+ properties: mockProperties,
1119
+ hasRequireActual
1120
+ } = extractMockProperties({
1121
+ sourceCode,
1122
+ mockObjectNode
1123
+ });
1124
+
1125
+ // Extract preamble (variable declarations, etc.) from the mock factory
1126
+ const preamble = extractMockFactoryPreamble({
1127
+ mockImpl: mockImpl,
1128
+ sourceCode
1129
+ });
1130
+ if (mockProperties.size === 0) {
1131
+ return;
1132
+ }
1133
+
1134
+ // Step 6: Group properties by their source files
1135
+ const propertiesBySource = groupPropertiesBySource({
1136
+ mockProperties,
1137
+ exportMap
1138
+ });
1139
+
1140
+ // Step 7: Determine if we should report
1141
+ if (!shouldReportBarrelMock({
1142
+ propertiesBySource,
1143
+ barrelFilePath
1144
+ })) {
1145
+ return;
1146
+ }
1147
+
1148
+ // Step 8: Report with auto-fix
1149
+ context.report({
1150
+ node: node,
1151
+ messageId: 'barrelMock',
1152
+ data: {
1153
+ path: importPath
1154
+ },
1155
+ fix(_fixer) {
1156
+ const quote = sourceCode.getText(firstArg)[0];
1157
+ const fixes = generateMockImplementationFix({
1158
+ propertiesBySource,
1159
+ mockProperties,
1160
+ hasRequireActual,
1161
+ basedir,
1162
+ importPath,
1163
+ quote,
1164
+ exportMap,
1165
+ context,
1166
+ currentNode: node,
1167
+ preamble,
1168
+ workspaceRoot,
1169
+ fs
1170
+ });
1171
+ return fixes;
1172
+ }
1173
+ });
1174
+ }
1175
+ };
1176
+ }
1177
+ };
1178
+ }
1179
+ const rule = createRule(realFileSystem);
1180
+ export default rule;