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.
|
|
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.
|
|
25
|
-
"@vitest/coverage-v8": "^4.1.
|
|
24
|
+
"@typescript-eslint/parser": "^8.62.1",
|
|
25
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
26
26
|
"eslint": "^10.6.0",
|
|
27
|
-
"oxlint": "^1.
|
|
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 =
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
101
|
+
mockAliases.declareImport(
|
|
102
|
+
specifier.local,
|
|
103
|
+
node.source.value,
|
|
104
|
+
importSpecifierName(specifier),
|
|
105
|
+
);
|
|
147
106
|
}
|
|
148
107
|
},
|
|
149
108
|
VariableDeclarator(node) {
|
|
150
|
-
|
|
109
|
+
mockAliases.declare(node.id, node.init);
|
|
151
110
|
},
|
|
152
111
|
AssignmentExpression(node) {
|
|
153
|
-
if (node.operator === "=")
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
|
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"
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
43
|
+
if (prop === "importActual" && mock?.framework === "vitest" && framework === "vitest") {
|
|
40
44
|
return "async";
|
|
41
45
|
}
|
|
42
46
|
if (
|
|
43
47
|
prop === "requireActual" &&
|
|
44
|
-
mock
|
|
45
|
-
|
|
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 (
|
|
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,
|
|
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,
|
|
68
|
+
return Boolean(realModuleCall(argument.argument, specifier, paramVariable, mock, context));
|
|
61
69
|
}
|
|
62
|
-
return realModuleCall(argument, specifier,
|
|
70
|
+
return realModuleCall(argument, specifier, paramVariable, mock, context) === "sync";
|
|
63
71
|
}
|
|
64
72
|
|
|
65
|
-
function collectRealModuleNames(node, realModuleNames, specifier,
|
|
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,
|
|
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,
|
|
89
|
+
collectRealModuleNames(item, realModuleNames, specifier, paramVariable, mock, context);
|
|
82
90
|
} else if (child?.type) {
|
|
83
|
-
collectRealModuleNames(child, realModuleNames, specifier,
|
|
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
|
|
106
|
-
const
|
|
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
|
-
|
|
110
|
-
if (body
|
|
111
|
-
if (body.type
|
|
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,
|
|
125
|
+
collectRealModuleNames(statement, realModuleNames, specifier, paramVariable, mock, context);
|
|
115
126
|
collectReturnedObjects(statement, objects);
|
|
116
127
|
}
|
|
117
|
-
return
|
|
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,
|
|
140
|
+
function objectSpreadsRealModule(object, analysis, specifier, mock, context) {
|
|
121
141
|
return object.properties.some((prop) => {
|
|
122
|
-
|
|
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
|
|
133
|
-
const { objects
|
|
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 };
|