eslint-plugin-no-mistakes 0.26.0 → 0.28.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.26.0",
3
+ "version": "0.28.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.60.1",
25
- "@vitest/coverage-v8": "^4.1.8",
26
- "eslint": "^10.4.1",
27
- "oxlint": "^1.69.0",
24
+ "@typescript-eslint/parser": "^8.61.1",
25
+ "@vitest/coverage-v8": "^4.1.9",
26
+ "eslint": "^10.5.0",
27
+ "oxlint": "^1.71.0",
28
28
  "vitest": "^4.1.6"
29
29
  },
30
30
  "peerDependencies": {
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
 
3
3
  const rules = {
4
+ "async-call-disposition": require("./rules/async-call-disposition"),
5
+ "async-try-catch-return-await": require("./rules/async-try-catch-return-await"),
4
6
  "await-array-methods": require("./rules/await-array-methods"),
5
7
  "nextjs-static-fetch-method": require("./rules/nextjs-static-fetch-method"),
6
8
  "nextjs-static-fetch-url": require("./rules/nextjs-static-fetch-url"),
@@ -8,6 +10,7 @@ const rules = {
8
10
  "module-mock-preserve-exports": require("./rules/module-mock-preserve-exports"),
9
11
  "nextjs-metadata-exports-location": require("./rules/nextjs-metadata-exports-location"),
10
12
  "nextjs-no-manual-script-tags": require("./rules/nextjs-no-manual-script-tags"),
13
+ "no-global-fetch-outside-helper": require("./rules/no-global-fetch-outside-helper"),
11
14
  "no-delete-property": require("./rules/no-delete-property"),
12
15
  "no-import-only-test-files": require("./rules/no-import-only-test-files"),
13
16
  "no-placeholder-never-type-exports": require("./rules/no-placeholder-never-type-exports"),
@@ -27,10 +30,12 @@ const rules = {
27
30
  "playwright-unique": require("./rules/playwright-unique"),
28
31
  "react-no-nullish-react-node": require("./rules/react-no-nullish-react-node"),
29
32
  "react-no-use-promise-resolve": require("./rules/react-no-use-promise-resolve"),
33
+ "server-require-nullable-fetch-wrapper": require("./rules/server-require-nullable-fetch-wrapper"),
30
34
  "test-no-error-message-matching": require("./rules/test-no-error-message-matching"),
31
35
  "test-no-shared-state": require("./rules/test-no-shared-state"),
32
36
  "ts-no-export-renaming": require("./rules/ts-no-export-renaming"),
33
37
  "ts-no-function-aliases": require("./rules/ts-no-function-aliases"),
38
+ "ts-preserve-null-option-defaults": require("./rules/ts-preserve-null-option-defaults"),
34
39
  "vitest-mock-test-file-naming": require("./rules/vitest-mock-test-file-naming"),
35
40
  };
36
41
 
@@ -98,18 +98,22 @@ function visitNode(node, callback) {
98
98
 
99
99
  function childNodes(node) {
100
100
  const children = [];
101
- for (const [key, value] of Object.entries(node)) {
101
+ for (const key in node) {
102
102
  if (key === "parent" || key === "tokens" || key === "comments") {
103
103
  continue;
104
104
  }
105
- if (Array.isArray(value)) {
106
- for (const child of value) {
107
- if (isAstNode(child)) {
108
- children.push(child);
105
+ if (Object.prototype.hasOwnProperty.call(node, key)) {
106
+ const value = node[key];
107
+ if (Array.isArray(value)) {
108
+ for (let i = 0; i < value.length; i++) {
109
+ const child = value[i];
110
+ if (isAstNode(child)) {
111
+ children.push(child);
112
+ }
109
113
  }
114
+ } else if (isAstNode(value)) {
115
+ children.push(value);
110
116
  }
111
- } else if (isAstNode(value)) {
112
- children.push(value);
113
117
  }
114
118
  }
115
119
  return children;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+
3
+ const transparentExpressionTypes = new Set([
4
+ "ChainExpression",
5
+ "TSNonNullExpression",
6
+ "TSAsExpression",
7
+ "TSTypeAssertion",
8
+ "TSSatisfiesExpression",
9
+ ]);
10
+
11
+ function isFunction(node) {
12
+ return (
13
+ node?.type === "ArrowFunctionExpression" ||
14
+ node?.type === "FunctionDeclaration" ||
15
+ node?.type === "FunctionExpression"
16
+ );
17
+ }
18
+
19
+ function findContainingFunction(node) {
20
+ let current = node.parent;
21
+ while (current) {
22
+ if (isFunction(current)) return current;
23
+ current = current.parent;
24
+ }
25
+ }
26
+
27
+ function isTransparentExpression(node) {
28
+ return transparentExpressionTypes.has(node?.type);
29
+ }
30
+
31
+ function unwrapExpression(node) {
32
+ let current = node;
33
+ while (isTransparentExpression(current)) current = current.expression;
34
+ return current;
35
+ }
36
+
37
+ function unwrapTransparentParent(node) {
38
+ let current = node;
39
+ while (isTransparentExpression(current.parent)) current = current.parent;
40
+ return current;
41
+ }
42
+
43
+ function visitorKeys(context, node) {
44
+ return context.sourceCode.visitorKeys[node.type] || [];
45
+ }
46
+
47
+ function traverse(context, node, visit, root = node) {
48
+ if (!node) return;
49
+ if (node !== root && isFunction(node)) return;
50
+ visit(node);
51
+ for (const key of visitorKeys(context, node)) {
52
+ const value = node[key];
53
+ if (Array.isArray(value)) {
54
+ for (const child of value) {
55
+ if (child?.type) traverse(context, child, visit, root);
56
+ }
57
+ } else if (value?.type) {
58
+ traverse(context, value, visit, root);
59
+ }
60
+ }
61
+ }
62
+
63
+ function isUnconditionalBeforeReturn(node, block) {
64
+ let current = node;
65
+ while (current && current !== block) {
66
+ const parent = current.parent;
67
+ if (!parent || parent.type === "IfStatement" || parent.type.endsWith("Expression")) {
68
+ return false;
69
+ }
70
+ if (
71
+ parent.type.endsWith("Statement") &&
72
+ parent.type !== "ExpressionStatement" &&
73
+ parent.type !== "BlockStatement"
74
+ ) {
75
+ return false;
76
+ }
77
+ current = parent;
78
+ }
79
+ return current === block;
80
+ }
81
+
82
+ module.exports = {
83
+ findContainingFunction,
84
+ isFunction,
85
+ isTransparentExpression,
86
+ isUnconditionalBeforeReturn,
87
+ traverse,
88
+ unwrapExpression,
89
+ unwrapTransparentParent,
90
+ };
@@ -0,0 +1,203 @@
1
+ "use strict";
2
+
3
+ const { rule } = require("../helpers");
4
+ const {
5
+ findContainingFunction,
6
+ isFunction,
7
+ isTransparentExpression,
8
+ unwrapTransparentParent,
9
+ } = require("./async-ast");
10
+ const { targetOptionsSchema } = require("./async-schema");
11
+ const { createTargetMatcher, memberPropertyName } = require("./async-targets");
12
+
13
+ function isPromiseAllCall(node) {
14
+ return (
15
+ node?.type === "CallExpression" &&
16
+ node.callee.type === "MemberExpression" &&
17
+ !node.callee.computed &&
18
+ node.callee.object.type === "Identifier" &&
19
+ node.callee.object.name === "Promise" &&
20
+ memberPropertyName(node.callee) === "all"
21
+ );
22
+ }
23
+
24
+ function directlyDisposed(node) {
25
+ const expression = unwrapTransparentParent(node);
26
+ const parent = expression.parent;
27
+ if (parent?.type === "ReturnStatement") {
28
+ const fn = findContainingFunction(parent);
29
+ return !isCallArgument(fn);
30
+ }
31
+ if (parent?.type === "ArrowFunctionExpression" && parent.body === expression) {
32
+ if (isCallCallee(parent)) return directlyDisposed(parent.parent);
33
+ return !isCallArgument(parent);
34
+ }
35
+ return (
36
+ parent?.type === "AwaitExpression" ||
37
+ (parent?.type === "UnaryExpression" && parent.operator === "void")
38
+ );
39
+ }
40
+
41
+ function isCallArgument(node) {
42
+ return Boolean(node?.parent?.type === "CallExpression" && node.parent.arguments.includes(node));
43
+ }
44
+
45
+ function isCallCallee(node) {
46
+ return node?.parent?.type === "CallExpression" && node.parent.callee === node;
47
+ }
48
+
49
+ function isPromiseAllIterable(node, promiseAll) {
50
+ return promiseAll.arguments[0] === node && (node.type === "ArrayExpression" || isMapCall(node));
51
+ }
52
+
53
+ function isMapCall(node) {
54
+ return (
55
+ node?.type === "CallExpression" &&
56
+ node.callee.type === "MemberExpression" &&
57
+ memberPropertyName(node.callee) === "map"
58
+ );
59
+ }
60
+
61
+ function isMapCallback(fn) {
62
+ const call = fn.parent;
63
+ return isMapCall(call) && call.arguments.includes(fn);
64
+ }
65
+
66
+ function expressionReturnedFromCallback(node, fn) {
67
+ let current = node;
68
+ let returnStatement = null;
69
+ while (current && current !== fn) {
70
+ if (current.parent === fn && fn.body === current && fn.body.type !== "BlockStatement") {
71
+ return true;
72
+ }
73
+ if (current.parent?.type === "ReturnStatement") {
74
+ returnStatement = current.parent;
75
+ }
76
+ current = current.parent;
77
+ }
78
+ return Boolean(returnStatement);
79
+ }
80
+
81
+ function canReachPromiseAll(node, promiseAll) {
82
+ let current = node;
83
+ while (current && current !== promiseAll) {
84
+ const parent = current.parent;
85
+ if (isTransparentExpression(parent)) {
86
+ current = parent;
87
+ continue;
88
+ }
89
+ if (parent?.type === "MemberExpression") return false;
90
+ if (parent === promiseAll) return isPromiseAllIterable(current, promiseAll);
91
+ if (isFunction(parent)) {
92
+ if (!expressionReturnedFromCallback(node, parent) || !isMapCallback(parent)) {
93
+ return false;
94
+ }
95
+ current = parent.parent;
96
+ continue;
97
+ }
98
+ if (parent?.type === "ArrayExpression") {
99
+ if (parent.parent !== promiseAll || promiseAll.arguments[0] !== parent) return false;
100
+ current = parent;
101
+ continue;
102
+ }
103
+ if (parent?.type === "ObjectExpression") return false;
104
+ if (
105
+ parent?.type === "CallExpression" &&
106
+ parent !== promiseAll &&
107
+ parent.arguments.includes(current)
108
+ ) {
109
+ return false;
110
+ }
111
+ current = parent;
112
+ }
113
+ return current === promiseAll;
114
+ }
115
+
116
+ function observedExpression(node) {
117
+ let current = node;
118
+ while (current.parent) {
119
+ const parent = current.parent;
120
+ if (isTransparentExpression(parent)) {
121
+ current = parent;
122
+ continue;
123
+ }
124
+ if (
125
+ parent.type === "MemberExpression" &&
126
+ parent.object === current &&
127
+ parent.parent?.type === "CallExpression" &&
128
+ parent.parent.callee === parent
129
+ ) {
130
+ current = parent;
131
+ continue;
132
+ }
133
+ if (parent.type === "CallExpression" && parent.callee === current) {
134
+ current = parent;
135
+ continue;
136
+ }
137
+ break;
138
+ }
139
+ return current;
140
+ }
141
+
142
+ function promiseAllDisposed(node) {
143
+ let current = node.parent;
144
+ while (current) {
145
+ if (isPromiseAllCall(current) && canReachPromiseAll(node, current)) {
146
+ return (
147
+ directlyDisposed(current) ||
148
+ directlyDisposed(observedExpression(current)) ||
149
+ promiseAllDisposed(current)
150
+ );
151
+ }
152
+ current = current.parent;
153
+ }
154
+ return false;
155
+ }
156
+
157
+ function isDisposed(node) {
158
+ const observed = observedExpression(node);
159
+ return directlyDisposed(node) || directlyDisposed(observed) || promiseAllDisposed(node);
160
+ }
161
+
162
+ module.exports = rule(
163
+ {
164
+ type: "problem",
165
+ docs: {
166
+ description: "require explicit async disposition for configured async calls",
167
+ recommended: false,
168
+ },
169
+ hasSuggestions: true,
170
+ schema: targetOptionsSchema,
171
+ messages: {
172
+ disposition:
173
+ "Handle this async promise explicitly with await, return, Promise.all(...), or void.",
174
+ markVoid: "Prefix the call with void to mark this promise as intentionally floating.",
175
+ },
176
+ },
177
+ (context) => {
178
+ const matcher = createTargetMatcher(context);
179
+ if (!matcher.hasTargets) return {};
180
+
181
+ return {
182
+ ...matcher.visitors,
183
+ CallExpression(node) {
184
+ if (!matcher.isTargetCall(node) || isDisposed(node)) return;
185
+ context.report({
186
+ node,
187
+ messageId: "disposition",
188
+ suggest:
189
+ node.parent?.type === "ExpressionStatement"
190
+ ? [
191
+ {
192
+ messageId: "markVoid",
193
+ fix(fixer) {
194
+ return fixer.insertTextBefore(node, "void ");
195
+ },
196
+ },
197
+ ]
198
+ : [],
199
+ });
200
+ },
201
+ };
202
+ },
203
+ );
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ function optionsSchema(propertyName) {
4
+ return [
5
+ {
6
+ type: "object",
7
+ properties: {
8
+ [propertyName]: {
9
+ type: "array",
10
+ items: {
11
+ type: "object",
12
+ properties: {
13
+ sourceSpecifierPatterns: { type: "array", items: { type: "string" } },
14
+ calleeNamePatterns: { type: "array", items: { type: "string" } },
15
+ },
16
+ additionalProperties: false,
17
+ },
18
+ },
19
+ },
20
+ additionalProperties: false,
21
+ },
22
+ ];
23
+ }
24
+
25
+ const handlerOptionsSchema = optionsSchema("handlers");
26
+ const targetOptionsSchema = optionsSchema("targets");
27
+
28
+ module.exports = { handlerOptionsSchema, targetOptionsSchema };
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+
3
+ const { unwrapExpression } = require("./async-ast");
4
+ const { propertyName } = require("./module-mock-helpers");
5
+
6
+ function safeRegExp(source) {
7
+ try {
8
+ return new RegExp(source);
9
+ } catch {
10
+ return null;
11
+ }
12
+ }
13
+
14
+ function patternToRegExp(pattern) {
15
+ if (pattern.startsWith("/") && pattern.endsWith("/") && pattern.length > 2) {
16
+ return safeRegExp(pattern.slice(1, -1));
17
+ }
18
+ let source = "^";
19
+ for (let index = 0; index < pattern.length; index += 1) {
20
+ const ch = pattern[index];
21
+ const next = pattern[index + 1];
22
+ if (ch === "*" && next === "*") {
23
+ if (pattern[index + 2] === "/") {
24
+ source += "(?:.*/)?";
25
+ index += 2;
26
+ } else {
27
+ source += ".*";
28
+ index += 1;
29
+ }
30
+ } else if (ch === "*") {
31
+ source += "[^/]*";
32
+ } else if (ch === "?") {
33
+ source += "[^/]";
34
+ } else {
35
+ source += ch.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
36
+ }
37
+ }
38
+ return safeRegExp(`${source}$`);
39
+ }
40
+
41
+ function compileTargets(options, optionKey) {
42
+ return (options[optionKey] || [])
43
+ .map((target) => ({
44
+ sourceSpecifierPatterns: (target.sourceSpecifierPatterns || [])
45
+ .map(patternToRegExp)
46
+ .filter(Boolean),
47
+ calleeNamePatterns: (target.calleeNamePatterns || []).map(patternToRegExp).filter(Boolean),
48
+ }))
49
+ .filter(
50
+ (target) => target.sourceSpecifierPatterns.length > 0 && target.calleeNamePatterns.length > 0,
51
+ );
52
+ }
53
+
54
+ function matchesAny(value, patterns) {
55
+ return typeof value === "string" && patterns.some((pattern) => pattern.test(value));
56
+ }
57
+
58
+ function targetMatches(targets, source, calleeName) {
59
+ return targets.some(
60
+ (target) =>
61
+ matchesAny(source, target.sourceSpecifierPatterns) &&
62
+ matchesAny(calleeName, target.calleeNamePatterns),
63
+ );
64
+ }
65
+
66
+ function findVariable(scope, name) {
67
+ while (scope) {
68
+ const variable = scope.variables.find((candidate) => candidate.name === name);
69
+ if (variable) return variable;
70
+ scope = scope.upper;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function resolveVariable(node, context) {
76
+ return findVariable(context.sourceCode.getScope(node), node.name);
77
+ }
78
+
79
+ function importSpecifierName(specifier) {
80
+ const imported = specifier.imported;
81
+ return imported.type === "Literal" ? String(imported.value) : imported.name;
82
+ }
83
+
84
+ function requireSource(node) {
85
+ const expression = unwrapExpression(node);
86
+ return expression?.type === "CallExpression" &&
87
+ expression.callee.type === "Identifier" &&
88
+ expression.callee.name === "require" &&
89
+ typeof expression.arguments[0]?.value === "string"
90
+ ? expression.arguments[0].value
91
+ : null;
92
+ }
93
+
94
+ function bindingIdentifier(node) {
95
+ if (node?.type === "Identifier") return node;
96
+ return node?.type === "AssignmentPattern" && node.left.type === "Identifier" ? node.left : null;
97
+ }
98
+
99
+ function memberPropertyName(node) {
100
+ if (!node.computed) return propertyName(node.property);
101
+ return node.property?.type === "Literal" ? String(node.property.value) : null;
102
+ }
103
+
104
+ function createTargetMatcher(context, optionKey = "targets") {
105
+ const targets = compileTargets(context.options?.[0] || {}, optionKey);
106
+ const sourceSpecifierPatterns = targets.flatMap((target) => target.sourceSpecifierPatterns);
107
+ const directBindings = new Map();
108
+ const namespaceBindings = new Map();
109
+
110
+ function recordDirect(id, source, calleeName) {
111
+ if (id?.type !== "Identifier" || !targetMatches(targets, source, calleeName)) return;
112
+ const variable = resolveVariable(id, context);
113
+ if (variable) directBindings.set(variable, { source, calleeName });
114
+ }
115
+
116
+ function recordNamespace(id, source) {
117
+ if (id.type !== "Identifier" || !matchesAny(source, sourceSpecifierPatterns)) {
118
+ return;
119
+ }
120
+ const variable = resolveVariable(id, context);
121
+ if (variable) namespaceBindings.set(variable, source);
122
+ }
123
+
124
+ function recordRequireDeclarator(node) {
125
+ const source = requireSource(node.init);
126
+ if (!source) return;
127
+ if (node.id.type === "Identifier") {
128
+ recordNamespace(node.id, source);
129
+ recordDirect(node.id, source, node.id.name);
130
+ return;
131
+ }
132
+ if (node.id.type === "ObjectPattern") {
133
+ for (const property of node.id.properties) {
134
+ if (property.type !== "Property") continue;
135
+ recordDirect(bindingIdentifier(property.value), source, propertyName(property.key));
136
+ }
137
+ }
138
+ }
139
+
140
+ function recordProgramRequires(node) {
141
+ for (const statement of node.body) {
142
+ const declarations =
143
+ statement.type === "VariableDeclaration"
144
+ ? statement.declarations
145
+ : statement.type === "ExportNamedDeclaration" &&
146
+ statement.declaration?.type === "VariableDeclaration"
147
+ ? statement.declaration.declarations
148
+ : [];
149
+ for (const declaration of declarations) recordRequireDeclarator(declaration);
150
+ }
151
+ }
152
+
153
+ function isDirectTarget(node) {
154
+ if (node.type !== "Identifier") return false;
155
+ const variable = resolveVariable(node, context);
156
+ return Boolean(variable && directBindings.has(variable));
157
+ }
158
+
159
+ function isNamespaceTarget(node) {
160
+ if (node.type !== "MemberExpression") return false;
161
+ const name = memberPropertyName(node);
162
+ if (!name) return false;
163
+ const source =
164
+ requireSource(node.object) ||
165
+ (node.object.type === "Identifier"
166
+ ? namespaceBindings.get(resolveVariable(node.object, context))
167
+ : null);
168
+ return Boolean(source && targetMatches(targets, source, name));
169
+ }
170
+
171
+ return {
172
+ hasTargets: targets.length > 0,
173
+ isTargetCall(node) {
174
+ return isDirectTarget(node.callee) || isNamespaceTarget(node.callee);
175
+ },
176
+ visitors: {
177
+ Program: recordProgramRequires,
178
+ ImportDeclaration(node) {
179
+ const source = node.source.value;
180
+ for (const specifier of node.specifiers) {
181
+ if (specifier.type === "ImportNamespaceSpecifier") {
182
+ recordNamespace(specifier.local, source);
183
+ } else if (specifier.type === "ImportDefaultSpecifier") {
184
+ recordDirect(specifier.local, source, specifier.local.name);
185
+ } else if (specifier.type === "ImportSpecifier") {
186
+ recordDirect(specifier.local, source, importSpecifierName(specifier));
187
+ }
188
+ }
189
+ },
190
+ VariableDeclarator(node) {
191
+ recordRequireDeclarator(node);
192
+ },
193
+ },
194
+ };
195
+ }
196
+
197
+ module.exports = {
198
+ createTargetMatcher,
199
+ memberPropertyName,
200
+ };