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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.11.1",
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.3.0",
27
- "oxlint": "^1.64.0",
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(["Variable", "Parameter", "CatchClause", "FunctionName"]);
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 variable = scope.variables.find((v) => v.name === "fetch");
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;
@@ -17,7 +17,9 @@ function collectReturnBranches(node, branches) {
17
17
  return;
18
18
  }
19
19
  if (node.type === "ReturnStatement") {
20
- branches.push(...jsxBranches(node.argument));
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
- children.push(...value.filter(isAstNode));
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 variable = scope.variables.find((candidate) => candidate.name === "setTimeout");
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 variable = scope.variables.find((candidate) => candidate.name === name);
30
- if (variable) {
31
- return (
32
- mutableTopLevel.has(variable.name) &&
33
- (variable.scope.type === "module" || variable.scope.block.type === "Program")
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
- scope = scope.upper;
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
- return name === "beforeEach" || name === "afterEach"
29
- ? "per-test"
30
- : name === "beforeAll" || name === "afterAll"
31
- ? "once"
32
- : null;
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
- return node.parent?.type === "CallExpression" && isSetupCall(node.parent);
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 === "per-test" ? suiteKey : undefined;
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 cleanupTracker = createCleanupTracker();
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 variable = scope.variables.find((candidate) => candidate.name === callback.name);
104
- const declaration = variable?.defs[0]?.node;
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
- scope = scope.upper;
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
- checkSharedMutations(declaration.body);
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
- checkSharedMutations(declaration.body);
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
- if (setupKind) {
158
- const callback = firstNamedCallbackArgument(node.arguments);
159
- if (callback) {
160
- pendingNamedSetupCallbacks.push({
161
- declaration: resolveFunctionCallback(node, callback),
162
- suiteKey: cleanupTracker.currentSuiteKey(),
163
- kind: setupKind,
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;