eslint-plugin-no-mistakes 0.31.7 → 0.32.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.31.7",
3
+ "version": "0.32.0",
4
4
  "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -21,10 +21,10 @@
21
21
  "test": "vitest run --coverage"
22
22
  },
23
23
  "devDependencies": {
24
- "@typescript-eslint/parser": "^8.62.0",
25
- "@vitest/coverage-v8": "^4.1.9",
24
+ "@typescript-eslint/parser": "^8.62.1",
25
+ "@vitest/coverage-v8": "^4.1.10",
26
26
  "eslint": "^10.6.0",
27
- "oxlint": "^1.71.0",
27
+ "oxlint": "^1.72.0",
28
28
  "vitest": "^4.1.6"
29
29
  },
30
30
  "peerDependencies": {
@@ -5,17 +5,14 @@ const { baselineKey, baselineMap } = require("./module-mock-baseline");
5
5
  const { matchDirectMockCallApply } = require("./module-mock-call-apply");
6
6
  const { integrationAllows } = require("./module-mock-integration");
7
7
  const {
8
- collectPatternNames,
9
8
  importSpecifierName,
10
- isFrameworkBinding,
11
9
  isInternalSpecifier,
12
10
  isModuleMockMemberCall,
13
- memberPropertyName,
14
11
  moduleMockSpecifierArgument,
15
12
  pathAllowed,
16
- propertyName,
17
13
  repoRelativeFilename,
18
14
  } = require("./module-mock-helpers");
15
+ const { createMockAliases } = require("./module-mock-preserve-aliases");
19
16
 
20
17
  const MODULE_MOCK_METHODS = new Set([
21
18
  "doMock",
@@ -54,16 +51,6 @@ function reportMessage(dynamic, specifier) {
54
51
  return `Module mock boundary does not allow mocking internal module "${specifier}".`;
55
52
  }
56
53
 
57
- function resolveVariable(node, context) {
58
- let scope = context.sourceCode.getScope(node);
59
- while (scope) {
60
- const variable = scope.variables.find((candidate) => candidate.name === node.name);
61
- if (variable) return variable;
62
- scope = scope.upper;
63
- }
64
- return null;
65
- }
66
-
67
54
  module.exports = rule(
68
55
  {
69
56
  type: "problem",
@@ -81,42 +68,12 @@ module.exports = rule(
81
68
  const options = context.options?.[0] ?? {};
82
69
  const filename = context.filename;
83
70
  const tracker = createBaselineTracker(filename, options.baseline);
84
- const mockAliases = new Map();
71
+ const mockAliases = createMockAliases(context, MODULE_MOCK_METHODS);
85
72
  if (!pathAllowed(filename, options)) return {};
86
73
 
87
- function declareAlias(id, init) {
88
- if (!id || !init) return;
89
- if (id.type === "ObjectPattern" && isFrameworkBinding(init, context)) {
90
- for (const property of id.properties) {
91
- if (property.type !== "Property") continue;
92
- const method = propertyName(property.key);
93
- if (MODULE_MOCK_METHODS.has(method)) {
94
- for (const name of collectPatternNames(property.value)) {
95
- mockAliases.set(name, resolveVariable(property.value, context));
96
- }
97
- }
98
- }
99
- return;
100
- }
101
- if (
102
- init.type === "MemberExpression" &&
103
- memberPropertyName(init) &&
104
- MODULE_MOCK_METHODS.has(memberPropertyName(init)) &&
105
- isFrameworkBinding(init.object, context)
106
- ) {
107
- for (const name of collectPatternNames(id))
108
- mockAliases.set(name, resolveVariable(id, context));
109
- }
110
- }
111
-
112
- function isUnshadowedMockAlias(node) {
113
- return (
114
- node.type === "Identifier" && mockAliases.get(node.name) === resolveVariable(node, context)
115
- );
116
- }
117
-
118
74
  function reportIfDisallowed(
119
75
  node,
76
+ mock,
120
77
  specifierNode = node.arguments[0],
121
78
  factory = node.arguments[1],
122
79
  ) {
@@ -127,7 +84,7 @@ module.exports = rule(
127
84
  return;
128
85
  }
129
86
  if (!specifier || !isInternalSpecifier(specifier, options)) return;
130
- if (integrationAllows(specifier, factory, options)) return;
87
+ if (integrationAllows(specifier, factory, mock, context, options)) return;
131
88
  if (tracker.allowed(specifier)) return;
132
89
  context.report({
133
90
  node,
@@ -141,54 +98,32 @@ module.exports = rule(
141
98
  if (node.source.value !== "vitest" && node.source.value !== "@jest/globals") return;
142
99
  for (const specifier of node.specifiers) {
143
100
  if (specifier.type !== "ImportSpecifier") continue;
144
- if (MODULE_MOCK_METHODS.has(importSpecifierName(specifier))) {
145
- mockAliases.set(specifier.local.name, resolveVariable(specifier.local, context));
146
- }
101
+ mockAliases.declareImport(
102
+ specifier.local,
103
+ node.source.value,
104
+ importSpecifierName(specifier),
105
+ );
147
106
  }
148
107
  },
149
108
  VariableDeclarator(node) {
150
- declareAlias(node.id, node.init);
109
+ mockAliases.declare(node.id, node.init);
151
110
  },
152
111
  AssignmentExpression(node) {
153
- if (node.operator === "=") declareAlias(node.left, node.right);
112
+ if (node.operator === "=") mockAliases.declare(node.left, node.right);
154
113
  },
155
114
  CallExpression(node) {
156
115
  const memberMock = isModuleMockMemberCall(node, context);
157
116
  if (memberMock && MODULE_MOCK_METHODS.has(memberMock.method)) {
158
- reportIfDisallowed(node);
117
+ reportIfDisallowed(node, memberMock);
159
118
  return;
160
119
  }
161
120
  const directCall = matchDirectMockCallApply(node, context, MODULE_MOCK_METHODS);
162
121
  if (directCall) {
163
- reportIfDisallowed(node, directCall.specifierNode, directCall.factory);
122
+ reportIfDisallowed(node, directCall.mock, directCall.specifierNode, directCall.factory);
164
123
  return;
165
124
  }
166
- if (isUnshadowedMockAlias(node.callee)) {
167
- reportIfDisallowed(node);
168
- return;
169
- }
170
- if (
171
- node.callee.type === "MemberExpression" &&
172
- propertyName(node.callee.property) === "call" &&
173
- node.callee.object.type === "Identifier" &&
174
- isUnshadowedMockAlias(node.callee.object)
175
- ) {
176
- reportIfDisallowed(node, node.arguments[1], node.arguments[2]);
177
- return;
178
- }
179
- if (
180
- node.callee.type === "MemberExpression" &&
181
- propertyName(node.callee.property) === "apply" &&
182
- node.callee.object.type === "Identifier" &&
183
- isUnshadowedMockAlias(node.callee.object)
184
- ) {
185
- const args = node.arguments[1];
186
- reportIfDisallowed(
187
- node,
188
- args?.type === "ArrayExpression" ? args.elements[0] : undefined,
189
- args?.type === "ArrayExpression" ? args.elements[1] : undefined,
190
- );
191
- }
125
+ const alias = mockAliases.matchCall(node);
126
+ if (alias) reportIfDisallowed(node, alias.mock, alias.specifierNode, alias.factory);
192
127
  },
193
128
  "Program:exit"(node) {
194
129
  for (const entry of tracker.stale()) {
@@ -13,11 +13,12 @@ function matchDirectMockCallApply(node, context, methods) {
13
13
  const direct = isModuleMockMemberCall({ callee: node.callee.object }, context);
14
14
  if (!direct || !methods.has(direct.method)) return null;
15
15
  if (propertyName(node.callee.property) === "call") {
16
- return { factory: node.arguments[2], specifierNode: node.arguments[1] };
16
+ return { factory: node.arguments[2], mock: direct, specifierNode: node.arguments[1] };
17
17
  }
18
18
  const args = node.arguments[1];
19
19
  return {
20
20
  factory: args?.type === "ArrayExpression" ? args.elements[1] : undefined,
21
+ mock: direct,
21
22
  specifierNode: args?.type === "ArrayExpression" ? args.elements[0] : undefined,
22
23
  };
23
24
  }
@@ -1,23 +1,29 @@
1
1
  "use strict";
2
2
 
3
- const { existsSync, readFileSync } = require("node:fs");
4
- const { resolve } = require("node:path");
3
+ const { existsSync, readFileSync, statSync } = require("node:fs");
4
+ const { dirname, join, resolve } = require("node:path");
5
5
  const { isInternalSpecifier, propertyName } = require("./module-mock-helpers");
6
+ const { analyzeFactory, spreadPreservesRealModule } = require("./module-mock-preserve-factory");
6
7
 
7
- function mockedExportNames(factory) {
8
+ // Matches the repo's documented TS/JS source-extension set (docs/ast-analysis.md
9
+ // "Shared File Model"), so a barrel re-exporting a .tsx/.jsx leaf (e.g. a React
10
+ // component) resolves the same way the rest of the toolchain treats source files.
11
+ const DEFAULT_REEXPORT_EXTENSIONS = [".mts", ".ts", ".tsx", ".mjs", ".js", ".jsx", ".cts", ".cjs"];
12
+
13
+ function mockedExportNames(factory, specifier, mock, context) {
8
14
  if (!factory) return null;
9
- const body = unwrapExpression(factory.type === "ObjectExpression" ? factory : factory.body);
10
- const objects = [];
11
- if (body?.type === "ObjectExpression") objects.push(body);
12
- if (body?.type === "BlockStatement") {
13
- for (const statement of body.body) collectReturnedObjects(statement, objects);
14
- }
15
+ const analysis = analyzeFactory(factory, specifier, mock, context);
16
+ const { objects } = analysis;
15
17
  if (objects.length === 0) return null;
16
18
  if (objects.some((object) => !object)) return null;
17
19
  const names = [];
18
20
  for (const object of objects) {
19
21
  for (const prop of object.properties) {
20
- if (prop.type === "SpreadElement" || prop.computed) return null;
22
+ if (prop.type === "SpreadElement") {
23
+ if (!spreadPreservesRealModule(prop, analysis, specifier, mock, context)) return null;
24
+ continue;
25
+ }
26
+ if (prop.computed) return null;
21
27
  const name = propertyName(prop.key);
22
28
  if (name && name !== "__esModule") names.push(name);
23
29
  }
@@ -25,42 +31,6 @@ function mockedExportNames(factory) {
25
31
  return names.length > 0 ? names : null;
26
32
  }
27
33
 
28
- function unwrapExpression(node) {
29
- let current = node;
30
- while (
31
- current?.type === "TSAsExpression" ||
32
- current?.type === "TSSatisfiesExpression" ||
33
- current?.type === "TSNonNullExpression" ||
34
- current?.type === "TypeCastExpression"
35
- ) {
36
- current = current.expression;
37
- }
38
- return current;
39
- }
40
-
41
- function collectReturnedObjects(node, objects) {
42
- if (
43
- node.type === "FunctionDeclaration" ||
44
- node.type === "FunctionExpression" ||
45
- node.type === "ArrowFunctionExpression"
46
- ) {
47
- return;
48
- }
49
- if (node.type === "ReturnStatement") {
50
- const argument = unwrapExpression(node.argument);
51
- objects.push(argument?.type === "ObjectExpression" ? argument : null);
52
- return;
53
- }
54
- for (const key of ["block", "body", "consequent", "alternate", "finalizer", "handler"]) {
55
- const child = node[key];
56
- if (Array.isArray(child)) {
57
- for (const item of child) collectReturnedObjects(item, objects);
58
- } else if (child?.type) {
59
- collectReturnedObjects(child, objects);
60
- }
61
- }
62
- }
63
-
64
34
  function integrationSourcePath(specifier, config) {
65
35
  const patterns = config.sourcePathTemplates ?? [];
66
36
  for (const template of patterns) {
@@ -86,40 +56,142 @@ function safeRegExp(source, flags) {
86
56
  }
87
57
  }
88
58
 
89
- function integrationExportNames(specifier, config) {
90
- const path = integrationSourcePath(specifier, config);
91
- if (!path) return null;
92
- const source = readFileSync(path, "utf8");
59
+ // Built once per `integrationExportNames` call and reused across every file the
60
+ // local re-export graph reaches. Safe to share: `String.prototype.matchAll` clones
61
+ // the regex per call and never mutates the shared `lastIndex`.
62
+ function tagPatterns(config) {
93
63
  const marker = config.markerRegex ?? String.raw`/\*\s*no-mistakes:\s*integration=[^*]+\*/`;
94
- const names = new Set();
95
64
  const declaration = safeRegExp(
96
65
  `${marker}\\s*export\\s+(?:async\\s+)?(?:function|const|let|var|class)\\s+([A-Za-z_$][\\w$]*)`,
97
66
  "g",
98
67
  );
99
- if (!declaration) return null;
100
- for (const match of source.matchAll(declaration)) names.add(match[1]);
101
68
  const defaultDeclaration = safeRegExp(
102
69
  `${marker}\\s*export\\s+default\\s+(?:async\\s+)?(?:function|class)\\b`,
103
70
  "g",
104
71
  );
105
- if (!defaultDeclaration) return null;
106
- for (const _match of source.matchAll(defaultDeclaration)) names.add("default");
107
72
  const named = safeRegExp(`${marker}\\s*export\\s*\\{([^}]+)\\}`, "g");
108
- if (!named) return null;
109
- for (const match of source.matchAll(named)) {
73
+ if (!declaration || !defaultDeclaration || !named) return null;
74
+ return { declaration, defaultDeclaration, named };
75
+ }
76
+
77
+ // `includeDefault` is false for every file reached via `export *`: ES modules never
78
+ // re-export a target's default binding through a star re-export, only through the
79
+ // root specifier itself or an explicit named re-export.
80
+ function addTaggedNames(source, patterns, names, includeDefault) {
81
+ for (const match of source.matchAll(patterns.declaration)) names.add(match[1]);
82
+ if (includeDefault) {
83
+ for (const _match of source.matchAll(patterns.defaultDeclaration)) names.add("default");
84
+ }
85
+ for (const match of source.matchAll(patterns.named)) {
110
86
  for (const part of (match[1] ?? "").split(",")) {
111
87
  const exported = part
112
88
  .trim()
113
89
  .split(/\s+as\s+/)
114
90
  .pop()
115
91
  ?.trim();
92
+ if (exported === "default" && !includeDefault) continue; // e.g. `export { x as default }`
116
93
  if (/^[A-Za-z_$][\w$]*$/.test(exported ?? "")) names.add(exported);
117
94
  }
118
95
  }
96
+ }
97
+
98
+ // Only plain `export * from '<specifier>'` re-exports propagate individual runtime
99
+ // export names; `export * as ns from ...` and type-only re-exports are intentionally
100
+ // left unmatched.
101
+ const REEXPORT_ALL = /export\s*\*\s*from\s*['"]([^'"]+)['"]/g;
102
+
103
+ // Matches whichever comes first at each position: a string/template literal (kept
104
+ // verbatim — it may be a real re-export's own specifier) or a line/block comment
105
+ // (dropped). This keeps a disabled `// export * from './leaf'` line, or the same
106
+ // text inside a `/* ... */` block, from being mistaken for a live barrel edge.
107
+ // A string literal whose *contents* merely spell out re-export-like text (rather
108
+ // than containing an actual disabled statement) is a narrower, accepted heuristic
109
+ // gap — matching the tag-marker scan's own text-based blind spots elsewhere in
110
+ // this file; resolving it would need a real lexer, not a regex pass.
111
+ const COMMENT_OR_STRING =
112
+ /"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\/\/[^\n]*|\/\*[\s\S]*?\*\//g;
113
+
114
+ function withoutComments(source) {
115
+ return source.replace(COMMENT_OR_STRING, (match) => (/^['"`]/.test(match) ? match : ""));
116
+ }
117
+
118
+ // NodeNext/ESM TypeScript projects conventionally write re-export specifiers with
119
+ // the compiled output extension (`./leaf.js`) even though the checked-in source is
120
+ // `./leaf.ts`. Each emitted extension maps to a specific, ordered set of TS/JS
121
+ // source extensions Node/TypeScript actually resolve it from — trying the full
122
+ // configured extension list instead could pick an unrelated sibling (e.g. probing
123
+ // `.mts`, which emits `.mjs`, ahead of `.ts` for a `.js` specifier).
124
+ const REEXPORT_EXTENSION_SOURCES = {
125
+ ".js": [".ts", ".tsx"],
126
+ ".jsx": [".tsx"],
127
+ ".mjs": [".mts"],
128
+ ".cjs": [".cts"],
129
+ };
130
+
131
+ // A specifier carrying a recognized compiled extension only ever resolves through
132
+ // its mapped source extensions (see REEXPORT_EXTENSION_SOURCES) — never through
133
+ // the generic "append any configured extension" or directory-index fallbacks below,
134
+ // which correspond to no real resolver behavior for an already-extension-ful path
135
+ // (e.g. `./leaf.js` never resolves to a literal `leaf.js.ts` file or a `leaf.js/`
136
+ // directory).
137
+ function resolveReexportPath(fromPath, specifier, extensions) {
138
+ const base = resolve(dirname(fromPath), specifier);
139
+ const compiledExt = Object.keys(REEXPORT_EXTENSION_SOURCES).find((ext) =>
140
+ specifier.endsWith(ext),
141
+ );
142
+ const stem = compiledExt ? base.slice(0, -compiledExt.length) : null;
143
+ const candidates = compiledExt
144
+ ? [
145
+ base,
146
+ ...REEXPORT_EXTENSION_SOURCES[compiledExt]
147
+ .filter((ext) => extensions.includes(ext))
148
+ .map((ext) => stem + ext),
149
+ ]
150
+ : [
151
+ base,
152
+ ...extensions.map((ext) => base + ext),
153
+ ...extensions.map((ext) => join(base, `index${ext}`)),
154
+ ];
155
+ for (const candidate of candidates) {
156
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
157
+ }
158
+ return null;
159
+ }
160
+
161
+ function reexportTargets(source, fromPath, extensions) {
162
+ const targets = [];
163
+ for (const match of withoutComments(source).matchAll(REEXPORT_ALL)) {
164
+ const specifier = match[1];
165
+ if (!specifier.startsWith(".")) continue; // leave bare-specifier re-exports unresolved
166
+ const resolved = resolveReexportPath(fromPath, specifier, extensions);
167
+ if (resolved) targets.push(resolved);
168
+ }
169
+ return targets;
170
+ }
171
+
172
+ function collectTaggedExports(path, extensions, patterns, names, visited, includeDefault) {
173
+ if (visited.has(path)) return; // guard against re-export cycles
174
+ visited.add(path);
175
+ const source = readFileSync(path, "utf8");
176
+ addTaggedNames(source, patterns, names, includeDefault);
177
+ for (const target of reexportTargets(source, path, extensions)) {
178
+ // `export *` never re-exports `default`, at any recursion depth.
179
+ collectTaggedExports(target, extensions, patterns, names, visited, false);
180
+ }
181
+ }
182
+
183
+ function integrationExportNames(specifier, config) {
184
+ const path = integrationSourcePath(specifier, config);
185
+ if (!path) return null;
186
+ const patterns = tagPatterns(config);
187
+ if (!patterns) return null;
188
+ const extensions = config.reexportExtensions ?? DEFAULT_REEXPORT_EXTENSIONS;
189
+ const names = new Set();
190
+ collectTaggedExports(path, extensions, patterns, names, new Set(), true);
119
191
  return names;
120
192
  }
121
193
 
122
- function integrationAllows(specifier, factory, options) {
194
+ function integrationAllows(specifier, factory, mock, context, options) {
123
195
  const config = options.integrationExports;
124
196
  if (!config) return false;
125
197
  const specifierPatterns = config.specifiers ?? [];
@@ -129,7 +201,7 @@ function integrationAllows(specifier, factory, options) {
129
201
  ) {
130
202
  return false;
131
203
  }
132
- const mocked = mockedExportNames(factory);
204
+ const mocked = mockedExportNames(factory, specifier, mock, context);
133
205
  if (!mocked) return false;
134
206
  const allowed = integrationExportNames(specifier, config);
135
207
  if (!allowed) return false;
@@ -9,6 +9,8 @@ const {
9
9
  propertyName,
10
10
  } = require("./module-mock-helpers");
11
11
 
12
+ const PRESERVE_METHODS = new Set(["mock", "doMock", "unstable_mockModule"]);
13
+
12
14
  function resolveVariable(node, context) {
13
15
  let scope = context.sourceCode.getScope(node);
14
16
  while (scope) {
@@ -34,7 +36,7 @@ function frameworkMock(object, method, context) {
34
36
  return { framework, method, namespace: expressionName(object) };
35
37
  }
36
38
 
37
- function createPreserveMockAliases(context) {
39
+ function createMockAliases(context, methods) {
38
40
  const aliases = new Map();
39
41
 
40
42
  function record(name, variable, mock) {
@@ -55,9 +57,7 @@ function createPreserveMockAliases(context) {
55
57
 
56
58
  return {
57
59
  declareImport(local, source, imported) {
58
- if (imported !== "mock" && imported !== "doMock" && imported !== "unstable_mockModule") {
59
- return;
60
- }
60
+ if (!methods.has(imported)) return;
61
61
  record(local.name, resolveVariable(local, context), {
62
62
  framework: source,
63
63
  method: imported,
@@ -70,15 +70,13 @@ function createPreserveMockAliases(context) {
70
70
  for (const property of id.properties) {
71
71
  if (property.type !== "Property") continue;
72
72
  const method = propertyName(property.key);
73
- if (method !== "mock" && method !== "doMock" && method !== "unstable_mockModule") {
74
- continue;
75
- }
73
+ if (!methods.has(method)) continue;
76
74
  recordPattern(property.value, frameworkMock(init, method, context));
77
75
  }
78
76
  }
79
77
  if (init.type === "MemberExpression" && isFrameworkBinding(init.object, context)) {
80
78
  const method = memberPropertyName(init);
81
- if (method !== "mock" && method !== "doMock" && method !== "unstable_mockModule") return;
79
+ if (!methods.has(method)) return;
82
80
  recordPattern(id, frameworkMock(init.object, method, context));
83
81
  }
84
82
  },
@@ -114,7 +112,12 @@ function createPreserveMockAliases(context) {
114
112
  };
115
113
  }
116
114
 
115
+ function createPreserveMockAliases(context) {
116
+ return createMockAliases(context, PRESERVE_METHODS);
117
+ }
118
+
117
119
  module.exports = {
120
+ createMockAliases,
118
121
  createPreserveMockAliases,
119
122
  resolveVariable,
120
123
  };
@@ -1,12 +1,16 @@
1
1
  "use strict";
2
2
 
3
- const { expressionName, literalString, memberPropertyName } = require("./module-mock-helpers");
3
+ const {
4
+ frameworkBindingModule,
5
+ literalString,
6
+ memberPropertyName,
7
+ } = require("./module-mock-helpers");
4
8
  const { resolveVariable } = require("./module-mock-preserve-aliases");
5
9
 
6
- function factoryParamName(factory) {
10
+ function factoryParam(factory) {
7
11
  const param = factory?.params?.[0];
8
12
  const unwrapped = param?.type === "AssignmentPattern" ? param.left : param;
9
- return unwrapped?.type === "Identifier" ? unwrapped.name : undefined;
13
+ return unwrapped?.type === "Identifier" ? unwrapped : null;
10
14
  }
11
15
 
12
16
  function unwrapExpression(node) {
@@ -30,57 +34,61 @@ function isFunctionNode(node) {
30
34
  );
31
35
  }
32
36
 
33
- function realModuleCall(call, specifier, paramName, mock) {
37
+ function realModuleCall(call, specifier, paramVariable, mock, context) {
34
38
  if (call?.type !== "CallExpression") return false;
35
39
  if (call.callee.type === "MemberExpression") {
36
- const objectName = expressionName(call.callee.object);
40
+ const framework = frameworkBindingModule(call.callee.object, context);
37
41
  const prop = memberPropertyName(call.callee);
38
42
  if (literalString(call.arguments[0]) !== specifier) return false;
39
- if (prop === "importActual" && mock.framework === "vitest" && objectName === mock.namespace) {
43
+ if (prop === "importActual" && mock?.framework === "vitest" && framework === "vitest") {
40
44
  return "async";
41
45
  }
42
46
  if (
43
47
  prop === "requireActual" &&
44
- mock.framework === "@jest/globals" &&
45
- objectName === mock.namespace
48
+ mock?.framework === "@jest/globals" &&
49
+ framework === "@jest/globals"
46
50
  ) {
47
51
  return "sync";
48
52
  }
49
53
  return false;
50
54
  }
51
- if (call.callee.type === "Identifier" && paramName && call.callee.name === paramName) {
55
+ if (
56
+ call.callee.type === "Identifier" &&
57
+ paramVariable &&
58
+ resolveVariable(call.callee, context) === paramVariable
59
+ ) {
52
60
  return "async";
53
61
  }
54
62
  return false;
55
63
  }
56
64
 
57
- function spreadArgumentPreserves(argument, specifier, paramName, mock) {
65
+ function spreadArgumentPreserves(argument, specifier, paramVariable, mock, context) {
58
66
  argument = unwrapExpression(argument);
59
67
  if (argument?.type === "AwaitExpression") {
60
- return Boolean(realModuleCall(argument.argument, specifier, paramName, mock));
68
+ return Boolean(realModuleCall(argument.argument, specifier, paramVariable, mock, context));
61
69
  }
62
- return realModuleCall(argument, specifier, paramName, mock) === "sync";
70
+ return realModuleCall(argument, specifier, paramVariable, mock, context) === "sync";
63
71
  }
64
72
 
65
- function collectRealModuleNames(node, realModuleNames, specifier, paramName, mock, context) {
73
+ function collectRealModuleNames(node, realModuleNames, specifier, paramVariable, mock, context) {
66
74
  if (isFunctionNode(node)) return;
67
75
  if (node.type === "VariableDeclaration" && node.kind === "const") {
68
76
  for (const declarator of node.declarations) {
69
77
  if (
70
78
  declarator.id.type === "Identifier" &&
71
- spreadArgumentPreserves(declarator.init, specifier, paramName, mock)
79
+ spreadArgumentPreserves(declarator.init, specifier, paramVariable, mock, context)
72
80
  ) {
73
81
  realModuleNames.add(resolveVariable(declarator.id, context));
74
82
  }
75
83
  }
76
84
  }
77
- for (const key of ["block", "body", "consequent", "alternate", "finalizer", "handler"]) {
85
+ for (const key of ["block", "body", "cases", "consequent", "alternate", "finalizer", "handler"]) {
78
86
  const child = node[key];
79
87
  if (Array.isArray(child)) {
80
88
  for (const item of child)
81
- collectRealModuleNames(item, realModuleNames, specifier, paramName, mock, context);
89
+ collectRealModuleNames(item, realModuleNames, specifier, paramVariable, mock, context);
82
90
  } else if (child?.type) {
83
- collectRealModuleNames(child, realModuleNames, specifier, paramName, mock, context);
91
+ collectRealModuleNames(child, realModuleNames, specifier, paramVariable, mock, context);
84
92
  }
85
93
  }
86
94
  }
@@ -92,7 +100,7 @@ function collectReturnedObjects(node, objects) {
92
100
  objects.push(argument?.type === "ObjectExpression" ? argument : null);
93
101
  return;
94
102
  }
95
- for (const key of ["block", "body", "consequent", "alternate", "finalizer", "handler"]) {
103
+ for (const key of ["block", "body", "cases", "consequent", "alternate", "finalizer", "handler"]) {
96
104
  const child = node[key];
97
105
  if (Array.isArray(child)) {
98
106
  for (const item of child) collectReturnedObjects(item, objects);
@@ -102,49 +110,49 @@ function collectReturnedObjects(node, objects) {
102
110
  }
103
111
  }
104
112
 
105
- function analyzeFactoryBody(factory, specifier, paramName, mock, context) {
106
- const body = unwrapExpression(factory?.body);
113
+ function analyzeFactory(factory, specifier, mock, context) {
114
+ const param = mock?.framework === "vitest" ? factoryParam(factory) : null;
115
+ const paramVariable = param ? resolveVariable(param, context) : null;
116
+ const body = unwrapExpression(factory?.type === "ObjectExpression" ? factory : factory?.body);
107
117
  const objects = [];
108
118
  const realModuleNames = new Set();
109
- if (!body) return { objects, realModuleNames };
110
- if (body.type === "ObjectExpression") return { objects: [body], realModuleNames };
111
- if (body.type !== "BlockStatement") return { objects, realModuleNames };
119
+ const analysis = { objects, paramVariable, realModuleNames };
120
+ if (!body) return analysis;
121
+ if (body.type === "ObjectExpression") return { ...analysis, objects: [body] };
122
+ if (body.type !== "BlockStatement") return analysis;
112
123
 
113
124
  for (const statement of body.body) {
114
- collectRealModuleNames(statement, realModuleNames, specifier, paramName, mock, context);
125
+ collectRealModuleNames(statement, realModuleNames, specifier, paramVariable, mock, context);
115
126
  collectReturnedObjects(statement, objects);
116
127
  }
117
- return { objects, realModuleNames };
128
+ return analysis;
129
+ }
130
+
131
+ function spreadPreservesRealModule(prop, analysis, specifier, mock, context) {
132
+ if (prop.type !== "SpreadElement") return false;
133
+ const { paramVariable, realModuleNames } = analysis;
134
+ if (prop.argument.type === "Identifier") {
135
+ return realModuleNames.has(resolveVariable(prop.argument, context));
136
+ }
137
+ return spreadArgumentPreserves(prop.argument, specifier, paramVariable, mock, context);
118
138
  }
119
139
 
120
- function objectSpreadsRealModule(object, specifier, paramName, realModuleNames, mock, context) {
140
+ function objectSpreadsRealModule(object, analysis, specifier, mock, context) {
121
141
  return object.properties.some((prop) => {
122
- if (prop.type !== "SpreadElement") return false;
123
- if (prop.argument.type === "Identifier") {
124
- return realModuleNames.has(resolveVariable(prop.argument, context));
125
- }
126
- return spreadArgumentPreserves(prop.argument, specifier, paramName, mock);
142
+ return spreadPreservesRealModule(prop, analysis, specifier, mock, context);
127
143
  });
128
144
  }
129
145
 
130
146
  function factoryPreservesExports(factory, specifier, mock, context) {
131
147
  if (!isFunctionNode(factory)) return false;
132
- const paramName = mock.framework === "vitest" ? factoryParamName(factory) : undefined;
133
- const { objects, realModuleNames } = analyzeFactoryBody(
134
- factory,
135
- specifier,
136
- paramName,
137
- mock,
138
- context,
139
- );
148
+ const analysis = analyzeFactory(factory, specifier, mock, context);
149
+ const { objects } = analysis;
140
150
  return (
141
151
  objects.length > 0 &&
142
152
  objects.every(
143
- (object) =>
144
- object &&
145
- objectSpreadsRealModule(object, specifier, paramName, realModuleNames, mock, context),
153
+ (object) => object && objectSpreadsRealModule(object, analysis, specifier, mock, context),
146
154
  )
147
155
  );
148
156
  }
149
157
 
150
- module.exports = { factoryPreservesExports };
158
+ module.exports = { analyzeFactory, factoryPreservesExports, spreadPreservesRealModule };