@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,801 @@
1
+ import { dirname } from 'path';
2
+ import { parseBarrelExports } from '../shared/barrel-parsing';
3
+ import { DEFAULT_TARGET_FOLDERS, findWorkspaceRoot, isRelativeImport } from '../shared/file-system';
4
+ import { findPackageInRegistry, isPackageInApplyToImportsFrom } from '../shared/package-registry';
5
+ import { findExportForSourceFile, parsePackageExports } from '../shared/package-resolution';
6
+ import { realFileSystem } from '../shared/types';
7
+
8
+ /**
9
+ * Options for the no-barrel-entry-imports rule.
10
+ */
11
+
12
+ /**
13
+ * Represents a Jest automock call: jest.mock('path') with no additional arguments
14
+ */
15
+
16
+ /**
17
+ * Metadata for the ESLint rule
18
+ */
19
+ const ruleMeta = {
20
+ type: 'problem',
21
+ docs: {
22
+ description: 'Disallow importing from barrel files in entry points.',
23
+ category: 'Best Practices',
24
+ recommended: false
25
+ },
26
+ fixable: 'code',
27
+ schema: [{
28
+ type: 'object',
29
+ properties: {
30
+ applyToImportsFrom: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'string'
34
+ },
35
+ description: 'The folder paths (relative to workspace root) containing packages whose imports will be checked and autofixed.'
36
+ }
37
+ },
38
+ additionalProperties: false
39
+ }],
40
+ messages: {
41
+ barrelEntryImport: "Importing from barrel file '{{path}}' is not allowed. Import directly from the source file using a more specific package.json export instead."
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Get the imported name from an ImportSpecifier, handling both Identifier and Literal
47
+ */
48
+ function getImportedName(spec) {
49
+ const imported = spec.imported;
50
+ return imported.type === 'Identifier' ? imported.name : String(imported.value);
51
+ }
52
+
53
+ /**
54
+ * Build an import statement for a set of specifiers
55
+ */
56
+ function buildImportStatement({
57
+ specs,
58
+ path,
59
+ quoteChar,
60
+ isTypeImport = false
61
+ }) {
62
+ const importNames = specs.map(spec => {
63
+ if (spec.type === 'ImportDefaultSpecifier') {
64
+ return spec.local.name;
65
+ } else if (spec.type === 'ImportSpecifier') {
66
+ const imported = getImportedName(spec);
67
+ const local = spec.local.name;
68
+ const isInlineType = spec.importKind === 'type' && !isTypeImport;
69
+ const prefix = isInlineType ? 'type ' : '';
70
+ return imported === local ? `${prefix}${imported}` : `${prefix}${imported} as ${local}`;
71
+ }
72
+ return '';
73
+ }).filter(name => name.length > 0);
74
+ if (importNames.length === 0) {
75
+ return '';
76
+ }
77
+ const typeKeyword = isTypeImport ? 'type ' : '';
78
+ const hasDefault = specs.some(spec => spec.type === 'ImportDefaultSpecifier');
79
+ const hasNamed = specs.some(spec => spec.type === 'ImportSpecifier');
80
+ if (hasDefault && hasNamed) {
81
+ var _specs$find;
82
+ const defaultName = (_specs$find = specs.find(spec => spec.type === 'ImportDefaultSpecifier')) === null || _specs$find === void 0 ? void 0 : _specs$find.local.name;
83
+ const namedImports = specs.filter(spec => spec.type === 'ImportSpecifier').map(spec => {
84
+ const imported = getImportedName(spec);
85
+ const local = spec.local.name;
86
+ const isInlineType = spec.importKind === 'type' && !isTypeImport;
87
+ const prefix = isInlineType ? 'type ' : '';
88
+ return imported === local ? `${prefix}${imported}` : `${prefix}${imported} as ${local}`;
89
+ }).join(', ');
90
+ return `import ${typeKeyword}${defaultName}, { ${namedImports} } from ${quoteChar}${path}${quoteChar};`;
91
+ } else if (hasDefault) {
92
+ var _specs$find2;
93
+ const defaultName = (_specs$find2 = specs.find(spec => spec.type === 'ImportDefaultSpecifier')) === null || _specs$find2 === void 0 ? void 0 : _specs$find2.local.name;
94
+ return `import ${typeKeyword}${defaultName} from ${quoteChar}${path}${quoteChar};`;
95
+ } else {
96
+ return `import ${typeKeyword}{ ${importNames.join(', ')} } from ${quoteChar}${path}${quoteChar};`;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Represents a specifier with its resolved target export path information.
102
+ */
103
+
104
+ /**
105
+ * Context resolved for an import that may be a barrel import.
106
+ */
107
+
108
+ /**
109
+ * Result of classifying specifiers by their target export paths.
110
+ */
111
+
112
+ /**
113
+ * Resolves import context for barrel file analysis.
114
+ * Returns null if the import should not be processed (relative import, not in target folder, etc.)
115
+ */
116
+ function resolveImportContext({
117
+ node,
118
+ workspaceRoot,
119
+ fs,
120
+ applyToImportsFrom
121
+ }) {
122
+ if (!node.source || typeof node.source.value !== 'string') {
123
+ return null;
124
+ }
125
+ const importPath = node.source.value;
126
+
127
+ // Skip relative imports - this rule is for cross-package imports
128
+ if (isRelativeImport(importPath)) {
129
+ return null;
130
+ }
131
+
132
+ // Extract the base package name (without subpath)
133
+ // e.g., "@atlassian/conversation-assistant-instrumentation" from
134
+ // "@atlassian/conversation-assistant-instrumentation" or
135
+ // "@atlassian/conversation-assistant-instrumentation/controllers/analytics"
136
+ const packageNameMatch = importPath.match(/^(@[^/]+\/[^/]+)/);
137
+ if (!packageNameMatch) {
138
+ return null;
139
+ }
140
+ const packageName = packageNameMatch[1];
141
+ const subPath = importPath.slice(packageName.length); // e.g., "" or "/controllers/analytics"
142
+
143
+ // Find the package (resolution is not constrained by applyToImportsFrom)
144
+ const packageDir = findPackageInRegistry({
145
+ packageName,
146
+ workspaceRoot,
147
+ fs
148
+ });
149
+ if (!packageDir) {
150
+ return null;
151
+ }
152
+
153
+ // Only check imports from packages in our applyToImportsFrom folders
154
+ if (!isPackageInApplyToImportsFrom({
155
+ packageDir,
156
+ workspaceRoot,
157
+ applyToImportsFrom
158
+ })) {
159
+ return null;
160
+ }
161
+
162
+ // Get the exports map for this package
163
+ const exportsMap = parsePackageExports({
164
+ packageDir,
165
+ fs
166
+ });
167
+ if (exportsMap.size === 0) {
168
+ return null;
169
+ }
170
+
171
+ // Determine which export path we're importing from
172
+ // For bare package imports, it's ".", for subpath imports it's "./" + subPath
173
+ const currentExportPath = subPath ? '.' + subPath : '.';
174
+
175
+ // Get the resolved path for the current export (the entry point file for this import)
176
+ const entryFilePath = exportsMap.get(currentExportPath);
177
+ if (!entryFilePath) {
178
+ return null;
179
+ }
180
+
181
+ // Parse the entry file to find where each export originates
182
+ // Pass workspaceRoot to enable cross-package re-export resolution
183
+ const exportMap = parseBarrelExports({
184
+ barrelFilePath: entryFilePath,
185
+ fs,
186
+ workspaceRoot
187
+ });
188
+ if (exportMap.size === 0) {
189
+ return null;
190
+ }
191
+ return {
192
+ importPath,
193
+ packageName,
194
+ currentExportPath,
195
+ exportsMap,
196
+ exportMap
197
+ };
198
+ }
199
+
200
+ /**
201
+ * Classifies import specifiers by their target export paths.
202
+ * Groups specifiers that can be remapped to more specific exports.
203
+ * For cross-package re-exports, suggests importing from the source package's most specific subpath.
204
+ */
205
+ function classifySpecifiers({
206
+ node,
207
+ importContext,
208
+ workspaceRoot,
209
+ fs
210
+ }) {
211
+ const {
212
+ currentExportPath,
213
+ exportsMap,
214
+ exportMap
215
+ } = importContext;
216
+ const specifiers = node.specifiers;
217
+ const specifiersByTarget = new Map();
218
+ const unmappedSpecifiers = [];
219
+ let hasNamespaceImport = false;
220
+
221
+ // Cache for source package exports maps to avoid redundant parsing
222
+ const sourcePackageExportsMaps = new Map();
223
+ for (const spec of specifiers) {
224
+ if (spec.type === 'ImportNamespaceSpecifier') {
225
+ hasNamespaceImport = true;
226
+ continue;
227
+ }
228
+ let nameInSource;
229
+ let kind = 'value';
230
+ if (spec.type === 'ImportDefaultSpecifier') {
231
+ nameInSource = 'default';
232
+ } else if (spec.type === 'ImportSpecifier') {
233
+ nameInSource = getImportedName(spec);
234
+ const parentImportKind = node.importKind;
235
+ kind = parentImportKind === 'type' || spec.importKind === 'type' ? 'type' : 'value';
236
+ } else {
237
+ continue;
238
+ }
239
+ const exportInfo = exportMap.get(nameInSource);
240
+ if (exportInfo) {
241
+ var _exportInfo$crossPack;
242
+ const effectiveKind = kind === 'type' || exportInfo.isTypeOnly ? 'type' : 'value';
243
+
244
+ // Check if this is a cross-package re-export
245
+ const sourcePackageName = (_exportInfo$crossPack = exportInfo.crossPackageSource) === null || _exportInfo$crossPack === void 0 ? void 0 : _exportInfo$crossPack.packageName;
246
+ if (sourcePackageName) {
247
+ // For cross-package re-exports, find the most specific subpath in the source package
248
+ // Note: Package resolution is not constrained by applyToImportsFrom - any package can be resolved
249
+ let sourcePackageExportsMap = sourcePackageExportsMaps.get(sourcePackageName);
250
+ if (!sourcePackageExportsMap) {
251
+ const sourcePackageDir = findPackageInRegistry({
252
+ packageName: sourcePackageName,
253
+ workspaceRoot,
254
+ fs
255
+ });
256
+ if (sourcePackageDir) {
257
+ sourcePackageExportsMap = parsePackageExports({
258
+ packageDir: sourcePackageDir,
259
+ fs
260
+ });
261
+ sourcePackageExportsMaps.set(sourcePackageName, sourcePackageExportsMap);
262
+ }
263
+ }
264
+
265
+ // Find the best export path in the source package
266
+ let targetExportPath = null;
267
+ if (sourcePackageExportsMap) {
268
+ targetExportPath = findExportForSourceFile({
269
+ sourceFilePath: exportInfo.path,
270
+ exportsMap: sourcePackageExportsMap
271
+ });
272
+ }
273
+
274
+ // Build the full import path: @package/subpath or just @package if no subpath found
275
+ const targetKey = targetExportPath ? sourcePackageName + targetExportPath.slice(1) // Remove leading '.' from subpath
276
+ : sourcePackageName;
277
+ if (!specifiersByTarget.has(targetKey)) {
278
+ specifiersByTarget.set(targetKey, []);
279
+ }
280
+ specifiersByTarget.get(targetKey).push({
281
+ spec: {
282
+ ...spec,
283
+ importKind: effectiveKind
284
+ },
285
+ originalName: exportInfo.originalName,
286
+ targetExportPath: targetKey,
287
+ kind: effectiveKind,
288
+ sourcePackageName
289
+ });
290
+ continue;
291
+ }
292
+
293
+ // Find if there's a package.json export that points to this source file
294
+ const targetExportPath = findExportForSourceFile({
295
+ sourceFilePath: exportInfo.path,
296
+ exportsMap
297
+ });
298
+
299
+ // Get the file that the current export path resolves to
300
+ const currentExportResolvedFile = exportsMap.get(currentExportPath);
301
+
302
+ // Skip if:
303
+ // 1. No target export path found
304
+ // 2. Target is same as current (no change needed)
305
+ // 3. Current export path already resolves to the same file as the source
306
+ // (handles multiple exports pointing to same file - avoid no-op changes)
307
+ const currentExportAlreadyPointsToSourceFile = currentExportResolvedFile !== undefined && currentExportResolvedFile === exportInfo.path;
308
+ if (targetExportPath && targetExportPath !== currentExportPath && !currentExportAlreadyPointsToSourceFile) {
309
+ if (!specifiersByTarget.has(targetExportPath)) {
310
+ specifiersByTarget.set(targetExportPath, []);
311
+ }
312
+ specifiersByTarget.get(targetExportPath).push({
313
+ spec: {
314
+ ...spec,
315
+ importKind: effectiveKind
316
+ },
317
+ originalName: exportInfo.originalName,
318
+ targetExportPath,
319
+ kind: effectiveKind
320
+ });
321
+ } else {
322
+ // No more specific export available
323
+ unmappedSpecifiers.push({
324
+ spec: spec,
325
+ targetExportPath: null,
326
+ kind
327
+ });
328
+ }
329
+ } else {
330
+ unmappedSpecifiers.push({
331
+ spec: spec,
332
+ targetExportPath: null,
333
+ kind
334
+ });
335
+ }
336
+ }
337
+ return {
338
+ specifiersByTarget,
339
+ unmappedSpecifiers,
340
+ hasNamespaceImport
341
+ };
342
+ }
343
+
344
+ /**
345
+ * Transforms a specifier to use the original export name (handling aliasing).
346
+ * Converts named imports of default exports to ImportDefaultSpecifier.
347
+ */
348
+ function transformSpecifierForExport({
349
+ spec,
350
+ originalName,
351
+ kind
352
+ }) {
353
+ if (!originalName) {
354
+ return spec;
355
+ }
356
+ if (originalName === 'default') {
357
+ // Should be ImportDefaultSpecifier
358
+ if (spec.type === 'ImportDefaultSpecifier') {
359
+ return spec;
360
+ }
361
+ // Convert ImportSpecifier to ImportDefaultSpecifier
362
+ return {
363
+ type: 'ImportDefaultSpecifier',
364
+ local: spec.local,
365
+ range: spec.range,
366
+ loc: spec.loc,
367
+ parent: spec.parent
368
+ };
369
+ } else {
370
+ // Create synthetic ImportSpecifier with correct importKind
371
+ return {
372
+ type: 'ImportSpecifier',
373
+ local: spec.local,
374
+ imported: {
375
+ type: 'Identifier',
376
+ name: originalName,
377
+ range: [0, 0],
378
+ loc: {
379
+ start: {
380
+ line: 0,
381
+ column: 0
382
+ },
383
+ end: {
384
+ line: 0,
385
+ column: 0
386
+ }
387
+ }
388
+ },
389
+ importKind: kind,
390
+ range: spec.range,
391
+ loc: spec.loc,
392
+ parent: spec.parent
393
+ };
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Merges new specifiers with an existing import declaration.
399
+ * Returns the new import statement string.
400
+ */
401
+ function buildMergedImportStatement({
402
+ existingImport,
403
+ newSpecs,
404
+ newImportPath,
405
+ nodeImportKind,
406
+ quoteChar
407
+ }) {
408
+ const existingSpecs = existingImport.specifiers.map(s => {
409
+ if (existingImport.importKind === 'type') {
410
+ return {
411
+ ...s,
412
+ importKind: 'type'
413
+ };
414
+ }
415
+ return s;
416
+ });
417
+ const augmentedNewSpecs = newSpecs.map(s => {
418
+ if (nodeImportKind === 'type') {
419
+ return {
420
+ ...s,
421
+ importKind: 'type'
422
+ };
423
+ }
424
+ return s;
425
+ });
426
+ const mergedSpecs = [...existingSpecs, ...augmentedNewSpecs];
427
+
428
+ // Determine if we should use 'import type'
429
+ const allType = mergedSpecs.every(s => s.importKind === 'type');
430
+ return buildImportStatement({
431
+ specs: mergedSpecs,
432
+ path: newImportPath,
433
+ quoteChar,
434
+ isTypeImport: allType
435
+ });
436
+ }
437
+
438
+ /**
439
+ * Check if an ExpressionStatement is a Jest automock: jest.mock('path') with exactly one string argument.
440
+ * Returns the JestAutomock info if it is, null otherwise.
441
+ */
442
+ function getJestAutomock(node) {
443
+ if (node.type !== 'ExpressionStatement') {
444
+ return null;
445
+ }
446
+ const statement = node;
447
+ const expr = statement.expression;
448
+ if (expr.type !== 'CallExpression') {
449
+ return null;
450
+ }
451
+
452
+ // Check for jest.mock(...)
453
+ const callee = expr.callee;
454
+ if (callee.type !== 'MemberExpression' || callee.object.type !== 'Identifier' || callee.object.name !== 'jest' || callee.property.type !== 'Identifier' || callee.property.name !== 'mock') {
455
+ return null;
456
+ }
457
+
458
+ // Must have exactly one argument (automock = no factory function)
459
+ if (expr.arguments.length !== 1) {
460
+ return null;
461
+ }
462
+ const arg = expr.arguments[0];
463
+ if (arg.type !== 'Literal' || typeof arg.value !== 'string') {
464
+ return null;
465
+ }
466
+
467
+ // Get the quote character from the raw value
468
+ const raw = arg.raw || `'${arg.value}'`;
469
+ const quoteChar = raw[0];
470
+ return {
471
+ statementNode: statement,
472
+ path: arg.value,
473
+ quoteChar
474
+ };
475
+ }
476
+
477
+ /**
478
+ * Find all Jest automocks in the AST that match the given import path.
479
+ */
480
+ function findMatchingAutomocks({
481
+ sourceCode,
482
+ importPath
483
+ }) {
484
+ const automocks = [];
485
+ const ast = sourceCode.ast;
486
+ for (const statement of ast.body) {
487
+ const automock = getJestAutomock(statement);
488
+ if (automock && automock.path === importPath) {
489
+ automocks.push(automock);
490
+ }
491
+ }
492
+ return automocks;
493
+ }
494
+
495
+ /**
496
+ * Build a jest.mock() statement string
497
+ */
498
+ function buildAutomockStatement({
499
+ path,
500
+ quoteChar
501
+ }) {
502
+ return `jest.mock(${quoteChar}${path}${quoteChar});`;
503
+ }
504
+
505
+ /**
506
+ * Creates a fix to remove a node with proper whitespace handling.
507
+ * Removes surrounding newlines to avoid leaving blank lines.
508
+ */
509
+ function createNodeRemovalFix({
510
+ fixer,
511
+ node,
512
+ sourceCode
513
+ }) {
514
+ const nodeStart = node.range[0];
515
+ const nodeEnd = node.range[1];
516
+
517
+ // Check for leading newline (prefer removing the line separator before the node)
518
+ const textBeforeNode = sourceCode.text.slice(0, nodeStart);
519
+ const leadingNewlineMatch = textBeforeNode.match(/(\r?\n)$/);
520
+ if (leadingNewlineMatch) {
521
+ // Remove the leading newline plus the node
522
+ return fixer.removeRange([nodeStart - leadingNewlineMatch[1].length, nodeEnd]);
523
+ }
524
+
525
+ // No leading newline - check for trailing newline
526
+ const textAfterNode = sourceCode.text.slice(nodeEnd);
527
+ const trailingNewlineMatch = textAfterNode.match(/^(\r?\n)/);
528
+ if (trailingNewlineMatch) {
529
+ return fixer.removeRange([nodeStart, nodeEnd + trailingNewlineMatch[1].length]);
530
+ }
531
+ return fixer.remove(node);
532
+ }
533
+
534
+ /**
535
+ * Creates the auto-fix for barrel import violations.
536
+ * Generates new import statements and handles merging with existing imports.
537
+ * Also updates Jest automocks (jest.mock calls with only a path) when present.
538
+ */
539
+ function createBarrelImportFix({
540
+ fixer,
541
+ node,
542
+ context,
543
+ importContext,
544
+ specifiersByTarget,
545
+ unmappedSpecifiers
546
+ }) {
547
+ const {
548
+ importPath,
549
+ packageName
550
+ } = importContext;
551
+ const sourceCode = context.sourceCode;
552
+ const quote = sourceCode.getText(node.source)[0]; // Get quote character
553
+
554
+ const fixes = [];
555
+ const newStatements = [];
556
+
557
+ // Find any Jest automocks that match this import path
558
+ const automocks = findMatchingAutomocks({
559
+ sourceCode,
560
+ importPath
561
+ });
562
+
563
+ // Track which new import paths need automocks (only value imports, not type-only)
564
+ const automockPaths = [];
565
+
566
+ // Track if we have any value imports at all (to determine if automocks should be updated)
567
+ let hasAnyValueImports = false;
568
+
569
+ // Get all existing imports to check for merging
570
+ const allImports = sourceCode.ast.body.filter(n => n.type === 'ImportDeclaration' && n !== node);
571
+
572
+ // Generate new import statements for each target export path
573
+ for (const [targetExportPath, specsWithTarget] of specifiersByTarget) {
574
+ // Check if this is a cross-package re-export (sourcePackageName is set)
575
+ const isCrossPackage = specsWithTarget.some(s => s.sourcePackageName);
576
+ const newImportPath = isCrossPackage ? targetExportPath // For cross-package, targetExportPath is already the full import path (e.g., @package/subpath)
577
+ : packageName + targetExportPath.slice(1); // Remove leading '.' for same-package imports
578
+
579
+ // Transform specifiers if needed (handle aliasing)
580
+ const specs = specsWithTarget.map(({
581
+ spec,
582
+ originalName,
583
+ kind
584
+ }) => transformSpecifierForExport({
585
+ spec,
586
+ originalName,
587
+ kind
588
+ }));
589
+
590
+ // Check if any specifier in this group is a value import (not type-only)
591
+ // Only add automock paths for value imports (types don't need mocking at runtime)
592
+ if (automocks.length > 0) {
593
+ const hasValueImport = specsWithTarget.some(({
594
+ kind,
595
+ spec
596
+ }) => kind === 'value' && (spec.type !== 'ImportSpecifier' || spec.importKind !== 'type'));
597
+ if (hasValueImport) {
598
+ hasAnyValueImports = true;
599
+ automockPaths.push(newImportPath);
600
+ }
601
+ }
602
+
603
+ // Check for existing import from the same path
604
+ const existingImport = allImports.find(n => n.source.value === newImportPath);
605
+
606
+ // Skip merging if existing is namespace import
607
+ const isNamespace = existingImport === null || existingImport === void 0 ? void 0 : existingImport.specifiers.some(s => s.type === 'ImportNamespaceSpecifier');
608
+ if (existingImport && !isNamespace) {
609
+ // Merge with existing import
610
+ const newImportStatement = buildMergedImportStatement({
611
+ existingImport,
612
+ newSpecs: specs,
613
+ newImportPath,
614
+ nodeImportKind: node.importKind,
615
+ quoteChar: quote
616
+ });
617
+ if (newImportStatement.length > 0) {
618
+ fixes.push(fixer.replaceText(existingImport, newImportStatement));
619
+ }
620
+ } else {
621
+ // Create new import
622
+ const isTypeImport = node.importKind === 'type';
623
+ const importStatement = buildImportStatement({
624
+ specs,
625
+ path: newImportPath,
626
+ quoteChar: quote,
627
+ isTypeImport
628
+ });
629
+ if (importStatement.length > 0) {
630
+ newStatements.push(importStatement);
631
+ }
632
+ }
633
+ }
634
+
635
+ // Handle unmapped specifiers - they stay in the original import
636
+ if (unmappedSpecifiers.length > 0) {
637
+ const unmappedSpecs = unmappedSpecifiers.map(u => u.spec);
638
+ const isTypeImport = node.importKind === 'type';
639
+ const remainingImport = buildImportStatement({
640
+ specs: unmappedSpecs,
641
+ path: importPath,
642
+ quoteChar: quote,
643
+ isTypeImport
644
+ });
645
+ if (remainingImport.length > 0) {
646
+ newStatements.push(remainingImport);
647
+ }
648
+
649
+ // If there are unmapped value specifiers and automocks, keep the original automock path too
650
+ if (automocks.length > 0) {
651
+ const hasUnmappedValueImport = unmappedSpecifiers.some(({
652
+ kind,
653
+ spec
654
+ }) => kind === 'value' && (spec.type !== 'ImportSpecifier' || spec.importKind !== 'type'));
655
+ if (hasUnmappedValueImport) {
656
+ hasAnyValueImports = true;
657
+ automockPaths.push(importPath);
658
+ }
659
+ }
660
+ }
661
+ if (newStatements.length > 0) {
662
+ fixes.push(fixer.replaceText(node, newStatements.join('\n')));
663
+ } else {
664
+ // If all were merged, remove the node including surrounding whitespace/newlines
665
+ fixes.push(createNodeRemovalFix({
666
+ fixer,
667
+ node,
668
+ sourceCode
669
+ }));
670
+ }
671
+
672
+ // Handle automock updates
673
+ // Only modify automocks if there are value imports being fixed
674
+ // Type-only imports don't need runtime mocking, so we preserve existing automocks
675
+ if (automocks.length > 0 && hasAnyValueImports && automockPaths.length > 0) {
676
+ for (const automock of automocks) {
677
+ // Build new automock statements for all new paths
678
+ const newAutomockStatements = automockPaths.map(path => buildAutomockStatement({
679
+ path,
680
+ quoteChar: automock.quoteChar
681
+ }));
682
+
683
+ // Replace the original automock statement with the new automock(s)
684
+ fixes.push(fixer.replaceTextRange(automock.statementNode.range, newAutomockStatements.join('\n')));
685
+ }
686
+ }
687
+ return fixes;
688
+ }
689
+
690
+ /**
691
+ * Handles an ImportDeclaration node to check for barrel file imports.
692
+ * Reports and auto-fixes imports that could use more specific export paths.
693
+ */
694
+ function handleImportDeclaration({
695
+ node,
696
+ context,
697
+ workspaceRoot,
698
+ fs,
699
+ applyToImportsFrom
700
+ }) {
701
+ // Resolve import context (validates and extracts package/export info)
702
+ // applyToImportsFrom is used here to filter which packages the rule applies to
703
+ const importContext = resolveImportContext({
704
+ node,
705
+ workspaceRoot,
706
+ fs,
707
+ applyToImportsFrom
708
+ });
709
+ if (!importContext) {
710
+ return;
711
+ }
712
+
713
+ // Check each imported specifier to see if we can find a more specific export
714
+ if (node.specifiers.length === 0) {
715
+ return;
716
+ }
717
+
718
+ // Classify specifiers by their target export paths
719
+ const {
720
+ specifiersByTarget,
721
+ unmappedSpecifiers,
722
+ hasNamespaceImport
723
+ } = classifySpecifiers({
724
+ node,
725
+ importContext,
726
+ workspaceRoot,
727
+ fs
728
+ });
729
+
730
+ // If namespace import, report without auto-fix if there are specific exports available
731
+ if (hasNamespaceImport) {
732
+ if (specifiersByTarget.size > 0) {
733
+ context.report({
734
+ node,
735
+ messageId: 'barrelEntryImport',
736
+ data: {
737
+ path: importContext.importPath
738
+ }
739
+ });
740
+ }
741
+ return;
742
+ }
743
+
744
+ // If no specifiers can be remapped to more specific imports, don't report
745
+ if (specifiersByTarget.size === 0) {
746
+ return;
747
+ }
748
+
749
+ // Report with auto-fix
750
+ context.report({
751
+ node,
752
+ messageId: 'barrelEntryImport',
753
+ data: {
754
+ path: importContext.importPath
755
+ },
756
+ fix(fixer) {
757
+ return createBarrelImportFix({
758
+ fixer,
759
+ node,
760
+ context,
761
+ importContext,
762
+ specifiersByTarget,
763
+ unmappedSpecifiers
764
+ });
765
+ }
766
+ });
767
+ }
768
+
769
+ /**
770
+ * Factory function to create the ESLint rule with a given file system.
771
+ * This enables testing with mock file systems.
772
+ */
773
+ export function createRule(fs) {
774
+ return {
775
+ meta: ruleMeta,
776
+ create(context) {
777
+ var _options$applyToImpor;
778
+ const options = context.options[0] || {};
779
+ const applyToImportsFrom = (_options$applyToImpor = options.applyToImportsFrom) !== null && _options$applyToImpor !== void 0 ? _options$applyToImpor : DEFAULT_TARGET_FOLDERS;
780
+ const workspaceRoot = findWorkspaceRoot({
781
+ startPath: dirname(context.filename),
782
+ fs,
783
+ applyToImportsFrom
784
+ });
785
+ return {
786
+ ImportDeclaration(rawNode) {
787
+ const node = rawNode;
788
+ handleImportDeclaration({
789
+ node,
790
+ context,
791
+ workspaceRoot,
792
+ fs,
793
+ applyToImportsFrom
794
+ });
795
+ }
796
+ };
797
+ }
798
+ };
799
+ }
800
+ const rule = createRule(realFileSystem);
801
+ export default rule;