eslint-plugin-no-mistakes 0.25.0 → 0.26.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.25.0",
3
+ "version": "0.26.0",
4
4
  "description": "ESLint and Oxlint rules for deterministic no-mistakes code analysis",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -193,7 +193,11 @@ function isFunctionParameterSource(node, context) {
193
193
  function findVariable(scope, name) {
194
194
  let variable = null;
195
195
  for (let current = scope; current && variable === null; current = current.upper) {
196
- variable = current.variables.find((item) => item.name === name) || null;
196
+ if (current.set && typeof current.set.get === "function") {
197
+ variable = current.set.get(name) || null;
198
+ } else {
199
+ variable = current.variables.find((item) => item.name === name) || null;
200
+ }
197
201
  }
198
202
  return variable;
199
203
  }
package/src/index.js CHANGED
@@ -4,6 +4,8 @@ const rules = {
4
4
  "await-array-methods": require("./rules/await-array-methods"),
5
5
  "nextjs-static-fetch-method": require("./rules/nextjs-static-fetch-method"),
6
6
  "nextjs-static-fetch-url": require("./rules/nextjs-static-fetch-url"),
7
+ "module-mock-boundary": require("./rules/module-mock-boundary"),
8
+ "module-mock-preserve-exports": require("./rules/module-mock-preserve-exports"),
7
9
  "nextjs-metadata-exports-location": require("./rules/nextjs-metadata-exports-location"),
8
10
  "nextjs-no-manual-script-tags": require("./rules/nextjs-no-manual-script-tags"),
9
11
  "no-delete-property": require("./rules/no-delete-property"),
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ function collectPatternNames(node, names = new Set()) {
4
+ if (!node) return names;
5
+ if (node.type === "Identifier") {
6
+ names.add(node.name);
7
+ return names;
8
+ }
9
+ const children =
10
+ node.type === "ObjectPattern"
11
+ ? node.properties.map((property) => property.value || property.argument)
12
+ : node.type === "ArrayPattern"
13
+ ? node.elements
14
+ : node.type === "RestElement"
15
+ ? [node.argument]
16
+ : node.type === "AssignmentPattern"
17
+ ? [node.left]
18
+ : [];
19
+ for (const child of children) collectPatternNames(child, names);
20
+ return names;
21
+ }
22
+
23
+ module.exports = {
24
+ collectPatternNames,
25
+ };
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+
3
+ const { repoRelativeFilename } = require("./module-mock-helpers");
4
+
5
+ function baselineKey(filename, specifier) {
6
+ return JSON.stringify([repoRelativeFilename(filename), specifier]);
7
+ }
8
+
9
+ function baselineSet(entries = []) {
10
+ return new Set(entries.map(([file, specifier]) => JSON.stringify([file, specifier])));
11
+ }
12
+
13
+ function baselineMap(entries = []) {
14
+ return new Map(
15
+ entries.map(([file, specifier, count]) => [JSON.stringify([file, specifier]), count]),
16
+ );
17
+ }
18
+
19
+ module.exports = {
20
+ baselineKey,
21
+ baselineMap,
22
+ baselineSet,
23
+ };
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const { baselineKey, baselineMap } = require("./module-mock-baseline");
5
+ const { matchDirectMockCallApply } = require("./module-mock-call-apply");
6
+ const { integrationAllows } = require("./module-mock-integration");
7
+ const {
8
+ collectPatternNames,
9
+ importSpecifierName,
10
+ isFrameworkBinding,
11
+ isInternalSpecifier,
12
+ isModuleMockMemberCall,
13
+ memberPropertyName,
14
+ moduleMockSpecifierArgument,
15
+ pathAllowed,
16
+ propertyName,
17
+ repoRelativeFilename,
18
+ } = require("./module-mock-helpers");
19
+
20
+ const MODULE_MOCK_METHODS = new Set([
21
+ "doMock",
22
+ "importMock",
23
+ "mock",
24
+ "setMock",
25
+ "unstable_mockModule",
26
+ ]);
27
+
28
+ function createBaselineTracker(filename, entries) {
29
+ const normalizedFilename = repoRelativeFilename(filename);
30
+ const baseline = baselineMap(entries);
31
+ const seen = new Map();
32
+ return {
33
+ allowed(specifier) {
34
+ const key = baselineKey(filename, specifier);
35
+ const count = (seen.get(key) ?? 0) + 1;
36
+ seen.set(key, count);
37
+ return count <= (baseline.get(key) ?? 0);
38
+ },
39
+ stale() {
40
+ const stale = [];
41
+ for (const [key, allowed] of baseline) {
42
+ const [file, specifier] = JSON.parse(key);
43
+ if (file !== normalizedFilename) continue;
44
+ const count = seen.get(key) ?? 0;
45
+ if (count < allowed) stale.push({ specifier, allowed, seen: count });
46
+ }
47
+ return stale;
48
+ },
49
+ };
50
+ }
51
+
52
+ function reportMessage(dynamic, specifier) {
53
+ if (dynamic) return "Module mock boundary requires literal specifiers.";
54
+ return `Module mock boundary does not allow mocking internal module "${specifier}".`;
55
+ }
56
+
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
+ module.exports = rule(
68
+ {
69
+ type: "problem",
70
+ docs: {
71
+ description: "enforce configured module mock boundaries",
72
+ recommended: false,
73
+ },
74
+ schema: [{ type: "object" }],
75
+ messages: {
76
+ boundary: "{{message}}",
77
+ stale: "{{message}}",
78
+ },
79
+ },
80
+ (context) => {
81
+ const options = context.options?.[0] ?? {};
82
+ const filename = context.filename;
83
+ const tracker = createBaselineTracker(filename, options.baseline);
84
+ const mockAliases = new Map();
85
+ if (!pathAllowed(filename, options)) return {};
86
+
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
+ function reportIfDisallowed(
119
+ node,
120
+ specifierNode = node.arguments[0],
121
+ factory = node.arguments[1],
122
+ ) {
123
+ const { dynamic, specifier } = moduleMockSpecifierArgument(specifierNode);
124
+ if (dynamic) {
125
+ if (options.requireLiteralSpecifiers === false) return;
126
+ context.report({ node, messageId: "boundary", data: { message: reportMessage(true) } });
127
+ return;
128
+ }
129
+ if (!specifier || !isInternalSpecifier(specifier, options)) return;
130
+ if (integrationAllows(specifier, factory, options)) return;
131
+ if (tracker.allowed(specifier)) return;
132
+ context.report({
133
+ node,
134
+ messageId: "boundary",
135
+ data: { message: reportMessage(false, specifier) },
136
+ });
137
+ }
138
+
139
+ return {
140
+ ImportDeclaration(node) {
141
+ if (node.source.value !== "vitest" && node.source.value !== "@jest/globals") return;
142
+ for (const specifier of node.specifiers) {
143
+ if (specifier.type !== "ImportSpecifier") continue;
144
+ if (MODULE_MOCK_METHODS.has(importSpecifierName(specifier))) {
145
+ mockAliases.set(specifier.local.name, resolveVariable(specifier.local, context));
146
+ }
147
+ }
148
+ },
149
+ VariableDeclarator(node) {
150
+ declareAlias(node.id, node.init);
151
+ },
152
+ AssignmentExpression(node) {
153
+ if (node.operator === "=") declareAlias(node.left, node.right);
154
+ },
155
+ CallExpression(node) {
156
+ const memberMock = isModuleMockMemberCall(node, context);
157
+ if (memberMock && MODULE_MOCK_METHODS.has(memberMock.method)) {
158
+ reportIfDisallowed(node);
159
+ return;
160
+ }
161
+ const directCall = matchDirectMockCallApply(node, context, MODULE_MOCK_METHODS);
162
+ if (directCall) {
163
+ reportIfDisallowed(node, directCall.specifierNode, directCall.factory);
164
+ return;
165
+ }
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
+ }
192
+ },
193
+ "Program:exit"(node) {
194
+ for (const entry of tracker.stale()) {
195
+ context.report({
196
+ node,
197
+ messageId: "stale",
198
+ data: {
199
+ message: `Module mock boundary baseline for "${entry.specifier}" is stale; lower count from ${entry.allowed} to ${entry.seen}.`,
200
+ },
201
+ });
202
+ }
203
+ },
204
+ };
205
+ },
206
+ );
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+
3
+ const { isModuleMockMemberCall, propertyName } = require("./module-mock-helpers");
4
+
5
+ function matchDirectMockCallApply(node, context, methods) {
6
+ if (
7
+ node.callee.type !== "MemberExpression" ||
8
+ !["call", "apply"].includes(propertyName(node.callee.property)) ||
9
+ node.callee.object.type !== "MemberExpression"
10
+ ) {
11
+ return null;
12
+ }
13
+ const direct = isModuleMockMemberCall({ callee: node.callee.object }, context);
14
+ if (!direct || !methods.has(direct.method)) return null;
15
+ if (propertyName(node.callee.property) === "call") {
16
+ return { factory: node.arguments[2], specifierNode: node.arguments[1] };
17
+ }
18
+ const args = node.arguments[1];
19
+ return {
20
+ factory: args?.type === "ArrayExpression" ? args.elements[1] : undefined,
21
+ specifierNode: args?.type === "ArrayExpression" ? args.elements[0] : undefined,
22
+ };
23
+ }
24
+
25
+ module.exports = { matchDirectMockCallApply };
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ const FRAMEWORK_MODULES = new Set(["vitest", "@jest/globals"]);
4
+
5
+ function propertyName(node) {
6
+ if (!node) return null;
7
+ return node.type === "Literal" ? String(node.value) : node.name;
8
+ }
9
+
10
+ function memberPropertyName(node) {
11
+ if (!node?.computed) return propertyName(node?.property);
12
+ return node.property?.type === "Literal" ? String(node.property.value) : null;
13
+ }
14
+
15
+ function frameworkRequireModule(def) {
16
+ const init = def.node?.init;
17
+ const requireCall =
18
+ init?.type === "MemberExpression" && memberPropertyName(init) ? init.object : init;
19
+ if (
20
+ def.type === "Variable" &&
21
+ requireCall?.type === "CallExpression" &&
22
+ requireCall.callee.type === "Identifier" &&
23
+ requireCall.callee.name === "require" &&
24
+ FRAMEWORK_MODULES.has(requireCall.arguments[0]?.value)
25
+ ) {
26
+ return requireCall.arguments[0].value;
27
+ }
28
+ return null;
29
+ }
30
+
31
+ function frameworkBindingModule(node, context) {
32
+ if (node?.type === "MemberExpression" && !node.computed) {
33
+ const module = frameworkBindingModule(node.object, context);
34
+ const prop = propertyName(node.property);
35
+ if (module === "vitest" && prop === "vi") return module;
36
+ if (module === "@jest/globals" && prop === "jest") return module;
37
+ return null;
38
+ }
39
+ if (node?.type !== "Identifier") return null;
40
+ let scope = context.sourceCode.getScope(node);
41
+ while (scope) {
42
+ const variable = scope.variables.find((candidate) => candidate.name === node.name);
43
+ if (!variable) {
44
+ scope = scope.upper;
45
+ continue;
46
+ }
47
+ for (const def of variable.defs) {
48
+ const importModule = def.type === "ImportBinding" ? def.parent?.source?.value : null;
49
+ if (FRAMEWORK_MODULES.has(importModule) && frameworkImportMatches(def, importModule)) {
50
+ return importModule;
51
+ }
52
+ const requireModule = frameworkRequireModule(def);
53
+ if (requireModule) return requireModule;
54
+ }
55
+ if (variable.defs.length === 0 && node.name === "vi") return "vitest";
56
+ if (variable.defs.length === 0 && node.name === "jest") return "@jest/globals";
57
+ return null;
58
+ }
59
+ if (node.name === "vi") return "vitest";
60
+ if (node.name === "jest") return "@jest/globals";
61
+ return null;
62
+ }
63
+
64
+ function isFrameworkBinding(node, context) {
65
+ return Boolean(frameworkBindingModule(node, context));
66
+ }
67
+
68
+ function frameworkImportMatches(def, importModule) {
69
+ if (def.node?.type === "ImportNamespaceSpecifier") return true;
70
+ const imported = def.node?.imported;
71
+ const name = imported?.type === "Literal" ? String(imported.value) : imported?.name;
72
+ if (importModule === "vitest") return name === "vi";
73
+ return name === "jest";
74
+ }
75
+
76
+ function expressionName(node) {
77
+ if (node?.type === "Identifier") return node.name;
78
+ if (node?.type !== "MemberExpression" || node.computed) return null;
79
+ const object = expressionName(node.object);
80
+ const prop = propertyName(node.property);
81
+ return object && prop ? `${object}.${prop}` : null;
82
+ }
83
+
84
+ module.exports = {
85
+ expressionName,
86
+ frameworkBindingModule,
87
+ isFrameworkBinding,
88
+ };
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+
3
+ const { collectPatternNames } = require("./ast-pattern-names");
4
+ const {
5
+ expressionName,
6
+ frameworkBindingModule,
7
+ isFrameworkBinding,
8
+ } = require("./module-mock-framework");
9
+
10
+ const MODULE_MOCK_METHODS = new Set([
11
+ "doMock",
12
+ "doUnmock",
13
+ "importMock",
14
+ "mock",
15
+ "setMock",
16
+ "unmock",
17
+ "unstable_mockModule",
18
+ ]);
19
+ const PRESERVE_METHODS = new Set(["mock", "doMock", "unstable_mockModule"]);
20
+ const DEFAULT_INTERNAL_SPECIFIERS = ["./**", "../**", "/**"];
21
+
22
+ function propertyName(node) {
23
+ if (!node) return null;
24
+ return node.type === "Literal" ? String(node.value) : node.name;
25
+ }
26
+
27
+ function memberPropertyName(node) {
28
+ if (!node?.computed) return propertyName(node?.property);
29
+ return node.property?.type === "Literal" ? String(node.property.value) : null;
30
+ }
31
+
32
+ function literalString(node) {
33
+ if (!node) return null;
34
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
35
+ if (node.type === "TemplateLiteral" && node.expressions.length === 0) {
36
+ return node.quasis.map((quasi) => quasi.value.cooked ?? quasi.value.raw).join("");
37
+ }
38
+ return null;
39
+ }
40
+
41
+ function normalizeFilename(filename) {
42
+ return filename.replace(/\\/g, "/");
43
+ }
44
+
45
+ function repoRelativeFilename(filename) {
46
+ const cwd = normalizeFilename(process.cwd());
47
+ const normalized = normalizeFilename(filename);
48
+ return normalized.startsWith(`${cwd}/`) ? normalized.slice(cwd.length + 1) : normalized;
49
+ }
50
+
51
+ function globToRegExp(pattern) {
52
+ let source = "^";
53
+ for (let index = 0; index < pattern.length; index += 1) {
54
+ const ch = pattern[index];
55
+ const next = pattern[index + 1];
56
+ if (ch === "*" && next === "*") {
57
+ if (pattern[index + 2] === "/") {
58
+ source += "(?:.*/)?";
59
+ index += 2;
60
+ } else {
61
+ source += ".*";
62
+ index += 1;
63
+ }
64
+ } else if (ch === "*") {
65
+ source += "[^/]*";
66
+ } else if (ch === "?") {
67
+ source += "[^/]";
68
+ } else {
69
+ source += ch.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
70
+ }
71
+ }
72
+ source += "$";
73
+ return new RegExp(source);
74
+ }
75
+
76
+ function safeRegExp(source) {
77
+ try {
78
+ return new RegExp(source);
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+
84
+ function stringMatches(value, patterns) {
85
+ return patterns.some((pattern) => {
86
+ if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
87
+ const regex = safeRegExp(pattern.slice(1, -1));
88
+ return regex ? regex.test(value) : false;
89
+ }
90
+ return globToRegExp(pattern).test(value);
91
+ });
92
+ }
93
+
94
+ function pathAllowed(filename, options) {
95
+ const file = repoRelativeFilename(filename);
96
+ const include = options.includePathPatterns ?? [];
97
+ const exclude = options.excludePathPatterns ?? [];
98
+ return (include.length === 0 || stringMatches(file, include)) && !stringMatches(file, exclude);
99
+ }
100
+
101
+ function isInternalSpecifier(specifier, options) {
102
+ return stringMatches(specifier, options.internalSpecifiers ?? DEFAULT_INTERNAL_SPECIFIERS);
103
+ }
104
+
105
+ function moduleMockSpecifierArgument(node) {
106
+ const direct = literalString(node);
107
+ if (direct !== null) return { dynamic: false, specifier: direct };
108
+ if (node?.type === "ImportExpression") {
109
+ const specifier = literalString(node.source);
110
+ return specifier === null ? { dynamic: true } : { dynamic: false, specifier };
111
+ }
112
+ if (node?.type === "CallExpression" && node.callee.type === "Import") {
113
+ const specifier = literalString(node.arguments[0]);
114
+ return specifier === null ? { dynamic: true } : { dynamic: false, specifier };
115
+ }
116
+ return { dynamic: true };
117
+ }
118
+
119
+ function isModuleMockMemberCall(node, context) {
120
+ if (node.callee.type !== "MemberExpression") return false;
121
+ const method = memberPropertyName(node.callee);
122
+ if (!MODULE_MOCK_METHODS.has(method)) return false;
123
+ if (!isFrameworkBinding(node.callee.object, context)) return false;
124
+ return {
125
+ framework: frameworkBindingModule(node.callee.object, context),
126
+ method,
127
+ namespace: expressionName(node.callee.object),
128
+ };
129
+ }
130
+
131
+ function isPreserveMockCall(node, context) {
132
+ const mock = isModuleMockMemberCall(node, context);
133
+ return mock && PRESERVE_METHODS.has(mock.method) ? mock : false;
134
+ }
135
+
136
+ function importSpecifierName(specifier) {
137
+ const imported = specifier.imported;
138
+ if (!imported) return null;
139
+ return imported.type === "Literal" ? String(imported.value) : imported.name;
140
+ }
141
+
142
+ module.exports = {
143
+ collectPatternNames,
144
+ expressionName,
145
+ frameworkBindingModule,
146
+ importSpecifierName,
147
+ isFrameworkBinding,
148
+ isInternalSpecifier,
149
+ isModuleMockMemberCall,
150
+ memberPropertyName,
151
+ isPreserveMockCall,
152
+ literalString,
153
+ moduleMockSpecifierArgument,
154
+ pathAllowed,
155
+ propertyName,
156
+ repoRelativeFilename,
157
+ stringMatches,
158
+ };