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