eslint-plugin-no-mistakes 0.11.1 → 0.12.1
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 +3 -3
- package/src/helpers.js +16 -2
- package/src/returned-jsx.js +8 -2
- package/src/rules/playwright-no-set-timeout.js +5 -1
- package/src/rules/test-no-shared-state-analysis.js +16 -9
- package/src/rules/test-no-shared-state-helpers.js +16 -17
- package/src/rules/test-no-shared-state.js +43 -39
- package/src/rules/vitest-mock-test-file-naming.js +19 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-no-mistakes",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@typescript-eslint/parser": "^8.48.0",
|
|
25
25
|
"@vitest/coverage-v8": "^4.1.6",
|
|
26
|
-
"eslint": "^10.
|
|
27
|
-
"oxlint": "^1.
|
|
26
|
+
"eslint": "^10.4.0",
|
|
27
|
+
"oxlint": "^1.65.0",
|
|
28
28
|
"vitest": "^4.1.6"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
package/src/helpers.js
CHANGED
|
@@ -134,14 +134,28 @@ function selectorValueNode(attribute) {
|
|
|
134
134
|
return expression && expression.type !== "JSXEmptyExpression" ? expression : null;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const LOCAL_BINDING_TYPES = new Set([
|
|
137
|
+
const LOCAL_BINDING_TYPES = new Set([
|
|
138
|
+
"Variable",
|
|
139
|
+
"Parameter",
|
|
140
|
+
"CatchClause",
|
|
141
|
+
"FunctionName",
|
|
142
|
+
"ImportBinding",
|
|
143
|
+
"ClassName",
|
|
144
|
+
]);
|
|
138
145
|
|
|
139
146
|
function isFetchShadowed(scope) {
|
|
140
147
|
while (scope) {
|
|
141
|
-
const
|
|
148
|
+
const get = scope.set?.get;
|
|
149
|
+
const variable = typeof get === "function" ? get.call(scope.set, "fetch") : null;
|
|
142
150
|
if (variable) {
|
|
143
151
|
return variable.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
|
|
144
152
|
}
|
|
153
|
+
if (!scope.set || typeof get !== "function") {
|
|
154
|
+
const fallback = scope.variables?.find((item) => item.name === "fetch");
|
|
155
|
+
if (fallback) {
|
|
156
|
+
return fallback.defs.some((def) => LOCAL_BINDING_TYPES.has(def.type));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
145
159
|
scope = scope.upper;
|
|
146
160
|
}
|
|
147
161
|
return false;
|
package/src/returned-jsx.js
CHANGED
|
@@ -17,7 +17,9 @@ function collectReturnBranches(node, branches) {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
if (node.type === "ReturnStatement") {
|
|
20
|
-
|
|
20
|
+
for (const branch of jsxBranches(node.argument)) {
|
|
21
|
+
branches.push(branch);
|
|
22
|
+
}
|
|
21
23
|
return;
|
|
22
24
|
}
|
|
23
25
|
if (node.type === "ClassDeclaration" || node.type === "ClassExpression") {
|
|
@@ -101,7 +103,11 @@ function childNodes(node) {
|
|
|
101
103
|
continue;
|
|
102
104
|
}
|
|
103
105
|
if (Array.isArray(value)) {
|
|
104
|
-
|
|
106
|
+
for (const child of value) {
|
|
107
|
+
if (isAstNode(child)) {
|
|
108
|
+
children.push(child);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
105
111
|
} else if (isAstNode(value)) {
|
|
106
112
|
children.push(value);
|
|
107
113
|
}
|
|
@@ -25,7 +25,11 @@ function propertyName(node, computed = false) {
|
|
|
25
25
|
function isGlobalSetTimeout(node, context) {
|
|
26
26
|
let scope = context.sourceCode.getScope(node);
|
|
27
27
|
while (scope) {
|
|
28
|
-
const
|
|
28
|
+
const get = scope.set?.get;
|
|
29
|
+
const variable =
|
|
30
|
+
typeof get === "function"
|
|
31
|
+
? get.call(scope.set, "setTimeout")
|
|
32
|
+
: scope.variables.find((candidate) => candidate.name === "setTimeout");
|
|
29
33
|
if (variable) return variable.defs.length === 0;
|
|
30
34
|
scope = scope.upper;
|
|
31
35
|
}
|
|
@@ -23,17 +23,24 @@ function isInsideUncalledNestedFunction(node, testDepth, setupDepth) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function isModuleMutable(context, mutableTopLevel, node, name) {
|
|
26
|
+
function isModuleMutable({ context, mutableTopLevel, node, name }) {
|
|
27
27
|
let scope = context.sourceCode.getScope(node);
|
|
28
28
|
while (scope) {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
const get = scope.set?.get;
|
|
30
|
+
const variable = typeof get === "function" ? get.call(scope.set, name) : null;
|
|
31
|
+
const resolvedVariable =
|
|
32
|
+
variable ||
|
|
33
|
+
(typeof get !== "function" ? scope.variables?.find((item) => item.name === name) : null);
|
|
34
|
+
|
|
35
|
+
if (!resolvedVariable) {
|
|
36
|
+
scope = scope.upper;
|
|
37
|
+
continue;
|
|
35
38
|
}
|
|
36
|
-
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
mutableTopLevel.has(resolvedVariable.name) &&
|
|
42
|
+
(resolvedVariable.scope.type === "module" || resolvedVariable.scope.block.type === "Program")
|
|
43
|
+
);
|
|
37
44
|
}
|
|
38
45
|
return false;
|
|
39
46
|
}
|
|
@@ -112,7 +119,7 @@ function createRegistryReports(context, mutableTopLevel, cleanupTracker, isCaptu
|
|
|
112
119
|
testDepth > 0 &&
|
|
113
120
|
setupDepth === 0 &&
|
|
114
121
|
!isCaptured(name) &&
|
|
115
|
-
isModuleMutable(context, mutableTopLevel, node, name)
|
|
122
|
+
isModuleMutable({ context, mutableTopLevel, node, name })
|
|
116
123
|
) {
|
|
117
124
|
pending.push({ node, path, suiteKey: cleanupTracker.currentSuiteKey() });
|
|
118
125
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const TEST_CALLEES = new Set(["it", "test", "describe"]);
|
|
4
4
|
const SETUP_CALLEES = new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
|
|
5
|
+
const PER_TEST_CALLEES = new Set(["beforeEach", "afterEach"]);
|
|
5
6
|
const MUTATING_METHODS = new Set(
|
|
6
7
|
"add clear copyWithin delete fill pop push reverse set shift sort splice unshift".split(" "),
|
|
7
8
|
);
|
|
@@ -25,15 +26,13 @@ function isTestCall(node) {
|
|
|
25
26
|
|
|
26
27
|
function setupCallbackKind(node) {
|
|
27
28
|
const name = calleeName(node.callee);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function isSetupCall(node) {
|
|
36
|
-
return SETUP_CALLEES.has(calleeName(node.callee));
|
|
29
|
+
if (PER_TEST_CALLEES.has(name)) return "per-test";
|
|
30
|
+
if (name === "beforeAll") return "before-once";
|
|
31
|
+
if (name === "afterAll") return "once";
|
|
32
|
+
if (node.callee.type !== "MemberExpression" || !TEST_CALLEES.has(name)) return null;
|
|
33
|
+
if (PER_TEST_CALLEES.has(node.callee.property.name)) return "per-test";
|
|
34
|
+
if (node.callee.property.name === "beforeAll") return "before-once";
|
|
35
|
+
return node.callee.property.name === "afterAll" ? "once" : null;
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
function collectPatternNames(node, names = new Set()) {
|
|
@@ -101,7 +100,8 @@ function isInlineTestCallback(node) {
|
|
|
101
100
|
}
|
|
102
101
|
|
|
103
102
|
function isInlineSetupCallback(node) {
|
|
104
|
-
|
|
103
|
+
const p = node.parent;
|
|
104
|
+
return p?.type === "CallExpression" && SETUP_CALLEES.has(calleeName(p.callee));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
function isCalledFunction(node) {
|
|
@@ -152,12 +152,13 @@ function firstNamedCallbackArgument(args) {
|
|
|
152
152
|
return args[0]?.type === "Identifier" ? args[0] : undefined;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
function createCleanupTracker() {
|
|
155
|
+
function createCleanupTracker(options = {}) {
|
|
156
156
|
const pathsBySuite = new Map();
|
|
157
157
|
const suiteStack = [];
|
|
158
|
-
let activeSuiteKey;
|
|
159
|
-
let replaySuiteKey;
|
|
158
|
+
let activeSuiteKey, replaySuiteKey;
|
|
160
159
|
let nextSuiteId = 0;
|
|
160
|
+
const ao = options.allowBeforeAllAssignments;
|
|
161
|
+
const sKinds = new Set(ao ? ["per-test", "before-once"] : ["per-test"]);
|
|
161
162
|
|
|
162
163
|
function currentSuiteKey() {
|
|
163
164
|
return replaySuiteKey ?? suiteStack.join("/");
|
|
@@ -175,7 +176,7 @@ function createCleanupTracker() {
|
|
|
175
176
|
|
|
176
177
|
return {
|
|
177
178
|
beginSetup(kind, suiteKey = currentSuiteKey()) {
|
|
178
|
-
activeSuiteKey = kind
|
|
179
|
+
activeSuiteKey = sKinds.has(kind) ? suiteKey : undefined;
|
|
179
180
|
},
|
|
180
181
|
clearReplaySuite() {
|
|
181
182
|
replaySuiteKey = undefined;
|
|
@@ -194,8 +195,7 @@ function createCleanupTracker() {
|
|
|
194
195
|
remember(path) {
|
|
195
196
|
if (!path || activeSuiteKey === undefined) return;
|
|
196
197
|
const paths = pathsBySuite.get(activeSuiteKey) ?? new Set();
|
|
197
|
-
paths.add(path);
|
|
198
|
-
pathsBySuite.set(activeSuiteKey, paths);
|
|
198
|
+
pathsBySuite.set(activeSuiteKey, paths.add(path));
|
|
199
199
|
},
|
|
200
200
|
setReplaySuite(suiteKey) {
|
|
201
201
|
replaySuiteKey = suiteKey;
|
|
@@ -214,7 +214,6 @@ module.exports = {
|
|
|
214
214
|
isInlineTestCallback,
|
|
215
215
|
isMutableInitializer,
|
|
216
216
|
calleeName,
|
|
217
|
-
isSetupCall,
|
|
218
217
|
isTestCall,
|
|
219
218
|
mutatingCallPropertyName,
|
|
220
219
|
mutatingCallTarget,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const { rule } = require("../helpers");
|
|
4
|
+
|
|
4
5
|
const {
|
|
5
6
|
createRegistryReports,
|
|
6
7
|
createViMockTracker,
|
|
@@ -9,6 +10,7 @@ const {
|
|
|
9
10
|
isResetAssignment,
|
|
10
11
|
walkSharedMutations,
|
|
11
12
|
} = require("./test-no-shared-state-analysis");
|
|
13
|
+
|
|
12
14
|
const {
|
|
13
15
|
calleeName,
|
|
14
16
|
collectPatternNames,
|
|
@@ -29,7 +31,7 @@ module.exports = rule(
|
|
|
29
31
|
{
|
|
30
32
|
type: "problem",
|
|
31
33
|
docs: { description: "disallow mutable module-scope test state", recommended: false },
|
|
32
|
-
schema: [],
|
|
34
|
+
schema: [{ type: "object", properties: { allowBeforeAllAssignments: { type: "boolean" } } }],
|
|
33
35
|
messages: {
|
|
34
36
|
shared:
|
|
35
37
|
"Shared mutable module-scope state between tests: use local variables inside each test instead.",
|
|
@@ -39,7 +41,8 @@ module.exports = rule(
|
|
|
39
41
|
const mutableTopLevel = new Set();
|
|
40
42
|
const pendingNamedCallbacks = [];
|
|
41
43
|
const pendingNamedSetupCallbacks = [];
|
|
42
|
-
const
|
|
44
|
+
const ruleOptions = context.options?.[0] ?? {};
|
|
45
|
+
const cleanupTracker = createCleanupTracker(ruleOptions);
|
|
43
46
|
const viMockTracker = createViMockTracker(context, mutableTopLevel);
|
|
44
47
|
const registryReports = createRegistryReports(
|
|
45
48
|
context,
|
|
@@ -56,10 +59,9 @@ module.exports = rule(
|
|
|
56
59
|
testDepth > 0 &&
|
|
57
60
|
setupDepth === 0 &&
|
|
58
61
|
!viMockTracker.isCaptured(name) &&
|
|
59
|
-
isModuleMutable(context, mutableTopLevel, node, name)
|
|
60
|
-
)
|
|
62
|
+
isModuleMutable({ context, mutableTopLevel, node, name })
|
|
63
|
+
)
|
|
61
64
|
context.report({ node, messageId: "shared" });
|
|
62
|
-
}
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
function reportAssignment(node) {
|
|
@@ -73,10 +75,9 @@ module.exports = rule(
|
|
|
73
75
|
name &&
|
|
74
76
|
setupDepth > 0 &&
|
|
75
77
|
mutableTopLevel.has(name) &&
|
|
76
|
-
isModuleMutable(context, mutableTopLevel, node, name)
|
|
77
|
-
)
|
|
78
|
+
isModuleMutable({ context, mutableTopLevel, node, name })
|
|
79
|
+
)
|
|
78
80
|
cleanupTracker.remember(path);
|
|
79
|
-
}
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
function rememberCall(node) {
|
|
@@ -97,16 +98,36 @@ module.exports = rule(
|
|
|
97
98
|
rememberSetupCleanup(node, mutationRootName(node.left), mutationPath(node.left.object));
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
const mutationWalk = {
|
|
102
|
+
onAssignment: (assignment) => {
|
|
103
|
+
rememberAssignmentCleanup(assignment);
|
|
104
|
+
reportAssignment(assignment);
|
|
105
|
+
},
|
|
106
|
+
onCall: rememberCall,
|
|
107
|
+
onUpdate: (update) => reportIfShared(update, mutationRootName(update.argument)),
|
|
108
|
+
};
|
|
109
|
+
|
|
100
110
|
function resolveFunctionCallback(node, callback) {
|
|
101
111
|
let scope = context.sourceCode.getScope(node);
|
|
102
112
|
while (scope) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
113
|
+
const get = scope.set?.get;
|
|
114
|
+
const resolvedVariable =
|
|
115
|
+
(typeof get === "function" ? get.call(scope.set, callback.name) : null) ||
|
|
116
|
+
(typeof get !== "function"
|
|
117
|
+
? scope.variables?.find((item) => item.name === callback.name)
|
|
118
|
+
: null);
|
|
119
|
+
|
|
120
|
+
if (!resolvedVariable) {
|
|
121
|
+
scope = scope.upper;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const declaration = resolvedVariable.defs[0]?.node;
|
|
105
126
|
if (declaration?.type === "FunctionDeclaration") return declaration;
|
|
106
127
|
if (declaration?.type === "VariableDeclarator" && isFunctionNode(declaration.init)) {
|
|
107
128
|
return declaration.init;
|
|
108
129
|
}
|
|
109
|
-
|
|
130
|
+
return null;
|
|
110
131
|
}
|
|
111
132
|
return null;
|
|
112
133
|
}
|
|
@@ -127,7 +148,7 @@ module.exports = rule(
|
|
|
127
148
|
const previousSetupDepth = setupDepth;
|
|
128
149
|
setupDepth = 1;
|
|
129
150
|
cleanupTracker.beginSetup(kind, suiteKey);
|
|
130
|
-
|
|
151
|
+
walkSharedMutations(declaration.body, mutationWalk);
|
|
131
152
|
setupDepth = previousSetupDepth;
|
|
132
153
|
cleanupTracker.endSetup();
|
|
133
154
|
}
|
|
@@ -135,7 +156,7 @@ module.exports = rule(
|
|
|
135
156
|
for (const { declaration, suiteKey } of pendingNamedCallbacks) {
|
|
136
157
|
if (!declaration) continue;
|
|
137
158
|
cleanupTracker.setReplaySuite(suiteKey);
|
|
138
|
-
|
|
159
|
+
walkSharedMutations(declaration.body, mutationWalk);
|
|
139
160
|
cleanupTracker.clearReplaySuite();
|
|
140
161
|
}
|
|
141
162
|
registryReports.flush();
|
|
@@ -146,7 +167,7 @@ module.exports = rule(
|
|
|
146
167
|
if (isTestCall(node)) {
|
|
147
168
|
testDepth += 1;
|
|
148
169
|
const callback = namedCallbackArgument(node.arguments);
|
|
149
|
-
if (callback) {
|
|
170
|
+
if (callback && !setupCallbackKind(node)) {
|
|
150
171
|
pendingNamedCallbacks.push({
|
|
151
172
|
declaration: resolveFunctionCallback(node, callback),
|
|
152
173
|
suiteKey: cleanupTracker.currentSuiteKey(),
|
|
@@ -154,15 +175,13 @@ module.exports = rule(
|
|
|
154
175
|
}
|
|
155
176
|
}
|
|
156
177
|
const setupKind = setupCallbackKind(node);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
});
|
|
165
|
-
}
|
|
178
|
+
const setupCallback = setupKind && firstNamedCallbackArgument(node.arguments);
|
|
179
|
+
if (setupCallback) {
|
|
180
|
+
pendingNamedSetupCallbacks.push({
|
|
181
|
+
declaration: resolveFunctionCallback(node, setupCallback),
|
|
182
|
+
suiteKey: cleanupTracker.currentSuiteKey(),
|
|
183
|
+
kind: setupKind,
|
|
184
|
+
});
|
|
166
185
|
}
|
|
167
186
|
if (setupKind) {
|
|
168
187
|
setupDepth += 1;
|
|
@@ -185,24 +204,9 @@ module.exports = rule(
|
|
|
185
204
|
cleanupTracker.endSetup();
|
|
186
205
|
}
|
|
187
206
|
const isInsideNested = isInsideUncalledNestedFunction(node, testDepth, setupDepth);
|
|
188
|
-
if (!isInsideNested)
|
|
189
|
-
rememberCall(node);
|
|
190
|
-
}
|
|
207
|
+
if (!isInsideNested) rememberCall(node);
|
|
191
208
|
if (calleeName(node.callee) === "describe") cleanupTracker.exitSuite();
|
|
192
209
|
},
|
|
193
210
|
};
|
|
194
|
-
|
|
195
|
-
function checkSharedMutations(node) {
|
|
196
|
-
walkSharedMutations(node, {
|
|
197
|
-
onAssignment: (assignment) => {
|
|
198
|
-
rememberAssignmentCleanup(assignment);
|
|
199
|
-
reportAssignment(assignment);
|
|
200
|
-
},
|
|
201
|
-
onCall: (call) => {
|
|
202
|
-
rememberCall(call);
|
|
203
|
-
},
|
|
204
|
-
onUpdate: (update) => reportIfShared(update, mutationRootName(update.argument)),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
211
|
},
|
|
208
212
|
);
|
|
@@ -19,6 +19,20 @@ const MOCK_METHODS = new Set([
|
|
|
19
19
|
"setSystemTime",
|
|
20
20
|
]);
|
|
21
21
|
|
|
22
|
+
const MOCK_CHAIN_METHODS = new Set([
|
|
23
|
+
"mockImplementation",
|
|
24
|
+
"mockImplementationOnce",
|
|
25
|
+
"mockReturnValue",
|
|
26
|
+
"mockReturnValueOnce",
|
|
27
|
+
"mockResolvedValue",
|
|
28
|
+
"mockResolvedValueOnce",
|
|
29
|
+
"mockRejectedValue",
|
|
30
|
+
"mockRejectedValueOnce",
|
|
31
|
+
"mockReset",
|
|
32
|
+
"mockRestore",
|
|
33
|
+
"mockClear",
|
|
34
|
+
]);
|
|
35
|
+
|
|
22
36
|
function isTestFile(filename) {
|
|
23
37
|
return TEST_FILE_PATTERN.test(filename.replace(/\\/g, "/"));
|
|
24
38
|
}
|
|
@@ -83,9 +97,13 @@ module.exports = rule(
|
|
|
83
97
|
},
|
|
84
98
|
(context) => {
|
|
85
99
|
let usesMocking = false;
|
|
100
|
+
function isMockChainCall(node) {
|
|
101
|
+
if (node.callee.type !== "MemberExpression") return false;
|
|
102
|
+
return MOCK_CHAIN_METHODS.has(propertyName(node.callee.property));
|
|
103
|
+
}
|
|
86
104
|
return {
|
|
87
105
|
CallExpression(node) {
|
|
88
|
-
if (isMockingCall(node, context)) usesMocking = true;
|
|
106
|
+
if (isMockingCall(node, context) || isMockChainCall(node)) usesMocking = true;
|
|
89
107
|
},
|
|
90
108
|
"Program:exit"(node) {
|
|
91
109
|
if (!isTestFile(context.filename)) return;
|