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