@jameswyse/oxc-config 0.2.0 → 0.3.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/README.md +30 -186
- package/REFERENCE.md +105 -132
- package/dist/index.js +26 -0
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +124 -4
- package/dist/rules/explicit-api-states.js +7 -1
- package/dist/rules/import-boundaries.js +31 -6
- package/dist/rules/no-derived-test-expectation.d.ts +1 -0
- package/dist/rules/no-derived-test-expectation.js +136 -0
- package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
- package/dist/rules/no-disabled-tls-verification.js +42 -0
- package/dist/rules/no-fixed-test-delay.js +46 -5
- package/dist/rules/no-generic-object-guard.d.ts +1 -0
- package/dist/rules/no-generic-object-guard.js +279 -0
- package/dist/rules/no-get-password-form.d.ts +1 -0
- package/dist/rules/no-get-password-form.js +113 -0
- package/dist/rules/no-hardcoded-credential.d.ts +1 -0
- package/dist/rules/no-hardcoded-credential.js +45 -0
- package/dist/rules/no-import-time-effects.d.ts +1 -0
- package/dist/rules/no-import-time-effects.js +90 -0
- package/dist/rules/no-optional-test-action.d.ts +1 -0
- package/dist/rules/no-optional-test-action.js +45 -0
- package/dist/rules/no-secret-fallback.d.ts +1 -0
- package/dist/rules/no-secret-fallback.js +64 -0
- package/dist/rules/no-swallowed-errors.js +131 -16
- package/dist/rules/no-test-timeout-overrides.js +22 -9
- package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
- package/dist/rules/no-unchecked-fetch-response.js +91 -0
- package/dist/rules/no-unchecked-index-search.d.ts +1 -0
- package/dist/rules/no-unchecked-index-search.js +41 -0
- package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
- package/dist/rules/no-uncontrolled-test-clock.js +112 -0
- package/dist/rules/no-unsettled-promise.d.ts +1 -0
- package/dist/rules/no-unsettled-promise.js +64 -0
- package/dist/rules/require-suppression-reason.d.ts +1 -0
- package/dist/rules/require-suppression-reason.js +32 -0
- package/dist/shared/index.d.ts +9 -0
- package/dist/shared/index.js +70 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
- package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
- package/package.json +7 -7
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, staticPropertyName, unwrapExpression } from "../shared/index.js";
|
|
3
|
+
const secretName = /(?:SECRET|TOKEN|PASSWORD|PASSPHRASE|PRIVATE|CREDENTIAL|SALT|SIGNING|API_?KEY|ACCESS_?KEY|_KEY$|^KEY$)/iu;
|
|
4
|
+
function isEnvironment(sourceCode, node) {
|
|
5
|
+
const environment = unwrapExpression(node);
|
|
6
|
+
if (environment.type !== "MemberExpression" || staticPropertyName(environment) !== "env") {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return (isGlobal(sourceCode, unwrapExpression(environment.object), "process") ||
|
|
10
|
+
(environment.object.type === "MetaProperty" && environment.object.property.name === "meta"));
|
|
11
|
+
}
|
|
12
|
+
function secretRead(sourceCode, node) {
|
|
13
|
+
const read = unwrapExpression(node);
|
|
14
|
+
const name = read.type === "MemberExpression" ? staticPropertyName(read) : null;
|
|
15
|
+
return read.type === "MemberExpression" &&
|
|
16
|
+
name !== null &&
|
|
17
|
+
secretName.test(name) &&
|
|
18
|
+
isEnvironment(sourceCode, read.object)
|
|
19
|
+
? name
|
|
20
|
+
: null;
|
|
21
|
+
}
|
|
22
|
+
function isLiteralFallback(node) {
|
|
23
|
+
const fallback = unwrapExpression(node);
|
|
24
|
+
return ((fallback.type === "Literal" && typeof fallback.value === "string") ||
|
|
25
|
+
(fallback.type === "TemplateLiteral" && fallback.expressions.length === 0));
|
|
26
|
+
}
|
|
27
|
+
export const noSecretFallback = defineRule({
|
|
28
|
+
meta: {
|
|
29
|
+
type: "problem",
|
|
30
|
+
docs: { description: "Disallow literal fallbacks for secret environment variables." },
|
|
31
|
+
schema: [],
|
|
32
|
+
messages: {
|
|
33
|
+
fallback: "`{{name}}` falls back to a value written in the source, so a missing secret silently becomes a known one. Fail at startup when it is missing.",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
create(context) {
|
|
37
|
+
const sourceCode = context.sourceCode;
|
|
38
|
+
return {
|
|
39
|
+
LogicalExpression(node) {
|
|
40
|
+
const name = node.operator === "&&" ? null : secretRead(sourceCode, node.left);
|
|
41
|
+
if (name !== null && isLiteralFallback(node.right)) {
|
|
42
|
+
context.report({ node, messageId: "fallback", data: { name } });
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
VariableDeclarator(node) {
|
|
46
|
+
if (node.id.type !== "ObjectPattern" ||
|
|
47
|
+
node.init === null ||
|
|
48
|
+
!isEnvironment(sourceCode, node.init)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const property of node.id.properties) {
|
|
52
|
+
const name = property.type === "Property" ? staticPropertyName(property) : null;
|
|
53
|
+
if (property.type === "Property" &&
|
|
54
|
+
name !== null &&
|
|
55
|
+
secretName.test(name) &&
|
|
56
|
+
property.value.type === "AssignmentPattern" &&
|
|
57
|
+
isLiteralFallback(property.value.right)) {
|
|
58
|
+
context.report({ node: property, messageId: "fallback", data: { name } });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { childNodes, isGlobal, propertyName, variableFor } from "../shared/index.js";
|
|
2
|
+
import { childNodes, isEffectModule, isGlobal, propertyName, unwrapExpression, variableFor, } from "../shared/index.js";
|
|
3
3
|
const nestedScopes = new Set([
|
|
4
4
|
"ArrowFunctionExpression",
|
|
5
5
|
"FunctionExpression",
|
|
@@ -109,14 +109,31 @@ function onlyProbes(sourceCode, block) {
|
|
|
109
109
|
nodes.every((node) => (node.type !== "AwaitExpression" && node.type !== "YieldExpression") ||
|
|
110
110
|
(node.argument !== null && containsProbe(sourceCode, node.argument))));
|
|
111
111
|
}
|
|
112
|
-
function
|
|
112
|
+
function isSharedBinding(sourceCode, node) {
|
|
113
|
+
const variable = variableFor(sourceCode, node);
|
|
114
|
+
return (variable === null ||
|
|
115
|
+
variable.defs.some((definition) => definition.type !== "Variable") ||
|
|
116
|
+
variable.references.filter((reference) => reference.isRead()).length > 1);
|
|
117
|
+
}
|
|
118
|
+
function storedValue(sourceCode, node) {
|
|
119
|
+
const value = unwrapExpression(node);
|
|
120
|
+
const declarator = value.type === "Identifier" ? variableFor(sourceCode, value)?.defs[0]?.node : undefined;
|
|
121
|
+
return declarator?.type === "VariableDeclarator" &&
|
|
122
|
+
declarator.parent.type === "VariableDeclaration" &&
|
|
123
|
+
declarator.parent.kind === "const" &&
|
|
124
|
+
declarator.init !== null
|
|
125
|
+
? unwrapExpression(declarator.init)
|
|
126
|
+
: value;
|
|
127
|
+
}
|
|
128
|
+
function isExistingPromise(sourceCode, node) {
|
|
113
129
|
const value = node.type === "ChainExpression" ? node.expression : node;
|
|
114
130
|
if (value.type === "CallExpression" &&
|
|
115
131
|
value.callee.type === "MemberExpression" &&
|
|
116
132
|
propertyName(value.callee) === "finally") {
|
|
117
|
-
return isExistingPromise(value.callee.object);
|
|
133
|
+
return isExistingPromise(sourceCode, value.callee.object);
|
|
118
134
|
}
|
|
119
|
-
return value.type === "Identifier"
|
|
135
|
+
return ((value.type === "Identifier" && isSharedBinding(sourceCode, value)) ||
|
|
136
|
+
value.type === "MemberExpression");
|
|
120
137
|
}
|
|
121
138
|
function swallowingCallback(sourceCode, callback) {
|
|
122
139
|
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
|
|
@@ -128,13 +145,94 @@ function swallowingCallback(sourceCode, callback) {
|
|
|
128
145
|
}
|
|
129
146
|
return body.body.length === 0 || returnsFallback(sourceCode, body.body);
|
|
130
147
|
}
|
|
148
|
+
function rejectionHandler(node) {
|
|
149
|
+
if (node.callee.type !== "MemberExpression") {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const name = propertyName(node.callee);
|
|
153
|
+
const [onFulfilled, onRejected, ...extraArguments] = node.arguments;
|
|
154
|
+
const handler = name === "catch" && onRejected === undefined
|
|
155
|
+
? onFulfilled
|
|
156
|
+
: name === "then" && extraArguments.length === 0
|
|
157
|
+
? onRejected
|
|
158
|
+
: undefined;
|
|
159
|
+
return handler === undefined ? null : { promise: node.callee.object, handler };
|
|
160
|
+
}
|
|
161
|
+
const effectRecoveries = new Set(["catchAll", "catchAllCause", "orElse"]);
|
|
162
|
+
function effectMember(sourceCode, node) {
|
|
163
|
+
return node.type === "MemberExpression" && isEffectModule(sourceCode, node.object, "Effect")
|
|
164
|
+
? propertyName(node)
|
|
165
|
+
: null;
|
|
166
|
+
}
|
|
167
|
+
function isEffectFallback(sourceCode, node) {
|
|
168
|
+
const value = unwrap(node);
|
|
169
|
+
if (value === null) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const member = effectMember(sourceCode, value);
|
|
173
|
+
if (member === "void" || member === "succeedNone") {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
if (value.type !== "CallExpression" || effectMember(sourceCode, value.callee) !== "succeed") {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const [result] = value.arguments;
|
|
180
|
+
return (result === undefined || (result.type !== "SpreadElement" && isFallback(sourceCode, result)));
|
|
181
|
+
}
|
|
182
|
+
function returnedValue(callback) {
|
|
183
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const body = callback.body;
|
|
187
|
+
if (body === null) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
if (body.type !== "BlockStatement") {
|
|
191
|
+
return body;
|
|
192
|
+
}
|
|
193
|
+
const [statement] = body.body;
|
|
194
|
+
return body.body.length === 1 && statement?.type === "ReturnStatement"
|
|
195
|
+
? statement.argument
|
|
196
|
+
: undefined;
|
|
197
|
+
}
|
|
198
|
+
function swallowingEffectRecovery(sourceCode, node) {
|
|
199
|
+
const name = effectMember(sourceCode, node.callee);
|
|
200
|
+
const handler = node.arguments.at(-1);
|
|
201
|
+
if (name === null || handler === undefined) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const value = returnedValue(handler);
|
|
205
|
+
if (value === undefined) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (name === "orElseSucceed") {
|
|
209
|
+
return isFallback(sourceCode, value) ? handler : null;
|
|
210
|
+
}
|
|
211
|
+
return effectRecoveries.has(name) && isEffectFallback(sourceCode, value) ? handler : null;
|
|
212
|
+
}
|
|
213
|
+
function settlement(sourceCode, expression) {
|
|
214
|
+
let value = expression;
|
|
215
|
+
while (value.type === "AwaitExpression" ||
|
|
216
|
+
(value.type === "UnaryExpression" && value.operator === "void")) {
|
|
217
|
+
value = value.argument;
|
|
218
|
+
}
|
|
219
|
+
return value.type === "CallExpression" &&
|
|
220
|
+
value.callee.type === "MemberExpression" &&
|
|
221
|
+
propertyName(value.callee) === "allSettled" &&
|
|
222
|
+
isGlobal(sourceCode, value.callee.object, "Promise")
|
|
223
|
+
? value
|
|
224
|
+
: null;
|
|
225
|
+
}
|
|
131
226
|
export const noSwallowedErrors = defineRule({
|
|
132
227
|
meta: {
|
|
133
228
|
type: "suggestion",
|
|
134
|
-
docs: {
|
|
229
|
+
docs: {
|
|
230
|
+
description: "Disallow error handlers that discard the error and return a fallback, and unread settled results.",
|
|
231
|
+
},
|
|
135
232
|
schema: [],
|
|
136
233
|
messages: {
|
|
137
234
|
swallowed: "This handler discards the error and returns a fallback, so callers cannot tell failure from an empty result. Prevent the failure, let it propagate, or return a failure the caller must handle.",
|
|
235
|
+
settled: "These settled results are never read, so every rejection disappears. Handle the rejected results, or use `Promise.all` so failures propagate.",
|
|
138
236
|
},
|
|
139
237
|
},
|
|
140
238
|
create(context) {
|
|
@@ -149,18 +247,35 @@ export const noSwallowedErrors = defineRule({
|
|
|
149
247
|
}
|
|
150
248
|
},
|
|
151
249
|
CallExpression(node) {
|
|
152
|
-
const
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
250
|
+
const rejection = rejectionHandler(node);
|
|
251
|
+
if (rejection !== null &&
|
|
252
|
+
!isExistingPromise(sourceCode, rejection.promise) &&
|
|
253
|
+
!isProbe(sourceCode, storedValue(sourceCode, rejection.promise)) &&
|
|
254
|
+
swallowingCallback(sourceCode, rejection.handler)) {
|
|
255
|
+
context.report({ node: rejection.handler, messageId: "swallowed" });
|
|
158
256
|
}
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
257
|
+
const recovery = swallowingEffectRecovery(sourceCode, node);
|
|
258
|
+
if (recovery !== null) {
|
|
259
|
+
context.report({ node: recovery, messageId: "swallowed" });
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
ExpressionStatement(node) {
|
|
263
|
+
const discarded = settlement(sourceCode, node.expression);
|
|
264
|
+
if (discarded !== null) {
|
|
265
|
+
context.report({ node: discarded, messageId: "settled" });
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
VariableDeclarator(node) {
|
|
269
|
+
const voided = node.init?.type === "UnaryExpression" && node.init.operator === "void";
|
|
270
|
+
const unread = node.init === null || (!voided && node.parent.parent?.type === "ExportNamedDeclaration")
|
|
271
|
+
? null
|
|
272
|
+
: settlement(sourceCode, node.init);
|
|
273
|
+
if (unread !== null &&
|
|
274
|
+
(voided ||
|
|
275
|
+
sourceCode
|
|
276
|
+
.getDeclaredVariables(node)
|
|
277
|
+
.every((variable) => !variable.references.some((reference) => reference.isRead())))) {
|
|
278
|
+
context.report({ node: unread, messageId: "settled" });
|
|
164
279
|
}
|
|
165
280
|
},
|
|
166
281
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { isGlobal, propertyName, apiReference, testModules } from "../shared/index.js";
|
|
2
|
+
import { isGlobal, propertyName, apiReference, staticPropertyName, testModules, unwrapExpression, } from "../shared/index.js";
|
|
3
3
|
const optionKeys = new Set(["timeout", "retry", "retries"]);
|
|
4
4
|
const configKeys = new Set(["testTimeout", "hookTimeout", "retry"]);
|
|
5
5
|
const hooks = new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
|
|
@@ -20,18 +20,27 @@ function testApi(sourceCode, callee) {
|
|
|
20
20
|
}
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
|
+
function isZero(node) {
|
|
24
|
+
const value = node === undefined ? undefined : unwrapExpression(node);
|
|
25
|
+
return value?.type === "Literal" && value.value === 0;
|
|
26
|
+
}
|
|
23
27
|
function hasOverrideKey(node, keys) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
const options = node === undefined ? undefined : unwrapExpression(node);
|
|
29
|
+
return (options?.type === "ObjectExpression" &&
|
|
30
|
+
options.properties.some((property) => {
|
|
31
|
+
const name = property.type === "Property" ? staticPropertyName(property) : null;
|
|
32
|
+
return (property.type === "Property" &&
|
|
33
|
+
name !== null &&
|
|
34
|
+
keys.has(name) &&
|
|
35
|
+
!(name.startsWith("retr") && isZero(property.value)));
|
|
28
36
|
}));
|
|
29
37
|
}
|
|
30
38
|
function timeoutArgument(node) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const value = node === undefined ? undefined : unwrapExpression(node);
|
|
40
|
+
return (value !== undefined &&
|
|
41
|
+
value.type !== "ObjectExpression" &&
|
|
42
|
+
value.type !== "SpreadElement" &&
|
|
43
|
+
!functionTypes.has(value.type));
|
|
35
44
|
}
|
|
36
45
|
function registrationOverride(sourceCode, node) {
|
|
37
46
|
const name = testApi(sourceCode, node.callee);
|
|
@@ -72,6 +81,10 @@ function runnerOverride(sourceCode, node) {
|
|
|
72
81
|
return (runnerObject(sourceCode, callee.object, "jest", ["@jest/globals"]) ||
|
|
73
82
|
importedTest(sourceCode, callee.object));
|
|
74
83
|
}
|
|
84
|
+
if (method === "retryTimes") {
|
|
85
|
+
return (runnerObject(sourceCode, callee.object, "jest", ["@jest/globals"]) &&
|
|
86
|
+
!isZero(node.arguments[0]));
|
|
87
|
+
}
|
|
75
88
|
if (method === "slow") {
|
|
76
89
|
return importedTest(sourceCode, callee.object);
|
|
77
90
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUncheckedFetchResponse: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, isGlobalValue, outermostExpression, staticPropertyName, unwrapExpression, } from "../shared/index.js";
|
|
3
|
+
const bodyReaders = new Set(["json", "text", "arrayBuffer", "blob", "formData", "bytes"]);
|
|
4
|
+
const statusChecks = new Set(["ok", "status"]);
|
|
5
|
+
const metadata = new Set(["headers", "url", "redirected", "type"]);
|
|
6
|
+
function unwrap(node) {
|
|
7
|
+
const value = unwrapExpression(node);
|
|
8
|
+
return value.type === "AwaitExpression" ? unwrap(value.argument) : value;
|
|
9
|
+
}
|
|
10
|
+
function isFetch(sourceCode, node) {
|
|
11
|
+
const value = node === null ? null : unwrap(node);
|
|
12
|
+
return value?.type === "CallExpression" && isGlobalValue(sourceCode, value.callee, "fetch");
|
|
13
|
+
}
|
|
14
|
+
function isLogged(sourceCode, node) {
|
|
15
|
+
const position = node.parent;
|
|
16
|
+
return (position?.type === "TemplateLiteral" ||
|
|
17
|
+
(position?.type === "CallExpression" &&
|
|
18
|
+
position.arguments.some((argument) => argument === node) &&
|
|
19
|
+
position.callee.type === "MemberExpression" &&
|
|
20
|
+
isGlobal(sourceCode, position.callee.object, "console")));
|
|
21
|
+
}
|
|
22
|
+
function use(sourceCode, reference) {
|
|
23
|
+
const response = outermostExpression(reference.identifier);
|
|
24
|
+
const member = response.parent;
|
|
25
|
+
if (member?.type !== "MemberExpression" || member.object !== response) {
|
|
26
|
+
return "other";
|
|
27
|
+
}
|
|
28
|
+
const name = staticPropertyName(member);
|
|
29
|
+
if (name !== null && statusChecks.has(name)) {
|
|
30
|
+
return isLogged(sourceCode, outermostExpression(member)) ? "metadata" : "check";
|
|
31
|
+
}
|
|
32
|
+
if (name !== null && metadata.has(name)) {
|
|
33
|
+
return "metadata";
|
|
34
|
+
}
|
|
35
|
+
return name !== null &&
|
|
36
|
+
bodyReaders.has(name) &&
|
|
37
|
+
member.parent.type === "CallExpression" &&
|
|
38
|
+
member.parent.callee === member
|
|
39
|
+
? "body"
|
|
40
|
+
: "other";
|
|
41
|
+
}
|
|
42
|
+
function readsBodyUnchecked(sourceCode, variables) {
|
|
43
|
+
const uses = variables.flatMap((variable) => variable.references
|
|
44
|
+
.filter((reference) => reference.isRead())
|
|
45
|
+
.map((reference) => use(sourceCode, reference)));
|
|
46
|
+
return uses.includes("body") && uses.every((item) => item === "body" || item === "metadata");
|
|
47
|
+
}
|
|
48
|
+
export const noUncheckedFetchResponse = defineRule({
|
|
49
|
+
meta: {
|
|
50
|
+
type: "problem",
|
|
51
|
+
docs: { description: "Require a status check before reading a fetch response body." },
|
|
52
|
+
schema: [],
|
|
53
|
+
messages: {
|
|
54
|
+
unchecked: "This reads the response body without checking `ok` or `status`, so an error response is treated as data. Check the status first.",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
create(context) {
|
|
58
|
+
const sourceCode = context.sourceCode;
|
|
59
|
+
return {
|
|
60
|
+
VariableDeclarator(node) {
|
|
61
|
+
if (node.id.type === "Identifier" &&
|
|
62
|
+
isFetch(sourceCode, node.init) &&
|
|
63
|
+
readsBodyUnchecked(sourceCode, sourceCode.getDeclaredVariables(node))) {
|
|
64
|
+
context.report({ node, messageId: "unchecked" });
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
CallExpression(node) {
|
|
68
|
+
const member = node.callee;
|
|
69
|
+
const name = member.type === "MemberExpression" ? staticPropertyName(member) : null;
|
|
70
|
+
if (member.type !== "MemberExpression" || name === null) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (bodyReaders.has(name) && isFetch(sourceCode, member.object)) {
|
|
74
|
+
context.report({ node, messageId: "unchecked" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const [callback] = node.arguments;
|
|
78
|
+
if (name === "then" &&
|
|
79
|
+
isFetch(sourceCode, member.object) &&
|
|
80
|
+
(callback?.type === "ArrowFunctionExpression" ||
|
|
81
|
+
callback?.type === "FunctionExpression") &&
|
|
82
|
+
callback.params[0]?.type === "Identifier" &&
|
|
83
|
+
readsBodyUnchecked(sourceCode, sourceCode
|
|
84
|
+
.getDeclaredVariables(callback)
|
|
85
|
+
.filter((variable) => variable.defs.some((definition) => definition.type === "Parameter")))) {
|
|
86
|
+
context.report({ node: callback, messageId: "unchecked" });
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUncheckedIndexSearch: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { staticPropertyName, unwrapExpression } from "../shared/index.js";
|
|
3
|
+
const searches = new Set(["indexOf", "lastIndexOf", "findIndex", "findLastIndex"]);
|
|
4
|
+
const indexedArguments = new Map([
|
|
5
|
+
["splice", 1],
|
|
6
|
+
["toSpliced", 1],
|
|
7
|
+
["with", 1],
|
|
8
|
+
["at", 1],
|
|
9
|
+
["slice", 2],
|
|
10
|
+
]);
|
|
11
|
+
function searchCall(node) {
|
|
12
|
+
const call = unwrapExpression(node);
|
|
13
|
+
const name = call.type === "CallExpression" && call.callee.type === "MemberExpression"
|
|
14
|
+
? staticPropertyName(call.callee)
|
|
15
|
+
: null;
|
|
16
|
+
return name !== null && searches.has(name) ? name : null;
|
|
17
|
+
}
|
|
18
|
+
export const noUncheckedIndexSearch = defineRule({
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: { description: "Require a missing-item check before using a searched index." },
|
|
22
|
+
schema: [],
|
|
23
|
+
messages: {
|
|
24
|
+
unchecked: "`{{search}}` returns -1 when nothing matches, and here -1 counts from the end, so a missing item silently targets the last element. Store the index and handle -1 first.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
CallExpression(node) {
|
|
30
|
+
const method = node.callee.type === "MemberExpression" ? staticPropertyName(node.callee) : null;
|
|
31
|
+
const count = method === null ? undefined : indexedArguments.get(method);
|
|
32
|
+
for (const argument of node.arguments.slice(0, count ?? 0)) {
|
|
33
|
+
const search = searchCall(argument);
|
|
34
|
+
if (search !== null) {
|
|
35
|
+
context.report({ node: argument, messageId: "unchecked", data: { search } });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUncontrolledTestClock: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { importedBinding, isGlobalValue, isPlaywright, staticPropertyName, testContext, unwrapExpression, variableFor, } from "../shared/index.js";
|
|
3
|
+
const fakeClockMethods = new Set(["install", "setFixedTime"]);
|
|
4
|
+
const timerLists = new Set(["apis", "toFake", "doNotFake"]);
|
|
5
|
+
function nameOf(node) {
|
|
6
|
+
const value = unwrapExpression(node);
|
|
7
|
+
return value.type === "Identifier" ? value.name : staticPropertyName(value);
|
|
8
|
+
}
|
|
9
|
+
function fakesDate(options) {
|
|
10
|
+
const value = options === undefined ? undefined : unwrapExpression(options);
|
|
11
|
+
if (value?.type !== "ObjectExpression") {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return value.properties.every((property) => {
|
|
15
|
+
const name = property.type === "Property" ? staticPropertyName(property) : null;
|
|
16
|
+
const list = property.type === "Property" ? unwrapExpression(property.value) : null;
|
|
17
|
+
if (name === null || !timerLists.has(name) || list?.type !== "ArrayExpression") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const values = list.elements.map((element) => element?.type === "Literal" ? element.value : undefined);
|
|
21
|
+
if (values.length === 0 || values.includes(undefined)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return name === "doNotFake" ? !values.includes("Date") : values.includes("Date");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function isNodeMock(sourceCode, node) {
|
|
28
|
+
const binding = importedBinding(sourceCode, unwrapExpression(node));
|
|
29
|
+
return nameOf(node) === "mock" || (binding?.source === "node:test" && binding.name === "mock");
|
|
30
|
+
}
|
|
31
|
+
function isLocalFunction(sourceCode, node) {
|
|
32
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
33
|
+
return definition !== undefined && definition.type !== "ImportBinding";
|
|
34
|
+
}
|
|
35
|
+
function controlsClock(sourceCode, call) {
|
|
36
|
+
const callee = unwrapExpression(call.callee);
|
|
37
|
+
const name = isLocalFunction(sourceCode, callee) ? null : nameOf(callee);
|
|
38
|
+
if (name === "setSystemTime") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (name === "useFakeTimers") {
|
|
42
|
+
return fakesDate(call.arguments[0]);
|
|
43
|
+
}
|
|
44
|
+
if (name === null || callee.type !== "MemberExpression") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const owner = unwrapExpression(callee.object);
|
|
48
|
+
return ((nameOf(owner) === "clock" && fakeClockMethods.has(name)) ||
|
|
49
|
+
(name === "enable" &&
|
|
50
|
+
owner.type === "MemberExpression" &&
|
|
51
|
+
nameOf(owner) === "timers" &&
|
|
52
|
+
isNodeMock(sourceCode, owner.object) &&
|
|
53
|
+
fakesDate(call.arguments[0])));
|
|
54
|
+
}
|
|
55
|
+
export const noUncontrolledTestClock = defineRule({
|
|
56
|
+
meta: {
|
|
57
|
+
type: "problem",
|
|
58
|
+
docs: { description: "Require tests that read the current time to control the clock." },
|
|
59
|
+
schema: [],
|
|
60
|
+
messages: {
|
|
61
|
+
clock: "This test reads the real clock, so its result depends on when it runs. Use a fixed date, or control time with fake timers.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
create(context) {
|
|
65
|
+
const sourceCode = context.sourceCode;
|
|
66
|
+
const drivesBrowser = sourceCode.ast.body.some((statement) => statement.type === "ImportDeclaration" && isPlaywright(statement.source.value));
|
|
67
|
+
if (drivesBrowser) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const reads = [];
|
|
71
|
+
const controlledTests = new Set();
|
|
72
|
+
let controlledFile = false;
|
|
73
|
+
function enclosingTest(node) {
|
|
74
|
+
const test = testContext(sourceCode, node);
|
|
75
|
+
return test?.api.name === "test" || test?.api.name === "it" ? test.callback : null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
CallExpression(node) {
|
|
79
|
+
if (controlsClock(sourceCode, node)) {
|
|
80
|
+
const test = enclosingTest(node);
|
|
81
|
+
if (test === null) {
|
|
82
|
+
controlledFile = true;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
controlledTests.add(test);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.callee.type === "MemberExpression" &&
|
|
89
|
+
staticPropertyName(node.callee) === "now" &&
|
|
90
|
+
isGlobalValue(sourceCode, node.callee.object, "Date")) {
|
|
91
|
+
reads.push(node);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
NewExpression(node) {
|
|
95
|
+
if (node.arguments.length === 0 && isGlobalValue(sourceCode, node.callee, "Date")) {
|
|
96
|
+
reads.push(node);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"Program:exit"() {
|
|
100
|
+
if (controlledFile) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const node of reads) {
|
|
104
|
+
const test = enclosingTest(node);
|
|
105
|
+
if (test === null || !controlledTests.has(test)) {
|
|
106
|
+
context.report({ node, messageId: "clock" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnsettledPromise: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, isShortCircuited } from "../shared/index.js";
|
|
3
|
+
function isOptionallyReached(reference, executor) {
|
|
4
|
+
let child = reference.identifier;
|
|
5
|
+
for (let node = child.parent; node !== null && node !== executor; child = node, node = node.parent) {
|
|
6
|
+
const argument = child;
|
|
7
|
+
if (node.type === "CallExpression" &&
|
|
8
|
+
node.arguments.some((candidate) => candidate === argument) &&
|
|
9
|
+
isShortCircuited(node)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function doesNothing(executor) {
|
|
16
|
+
const body = executor.body;
|
|
17
|
+
if (body?.type === "BlockStatement") {
|
|
18
|
+
return body.body.length === 0;
|
|
19
|
+
}
|
|
20
|
+
return ((body?.type === "Identifier" && body.name === "undefined") ||
|
|
21
|
+
(body?.type === "UnaryExpression" &&
|
|
22
|
+
body.operator === "void" &&
|
|
23
|
+
body.argument.type === "Literal"));
|
|
24
|
+
}
|
|
25
|
+
function endsByThrowing(executor) {
|
|
26
|
+
return (executor.body?.type === "BlockStatement" && executor.body.body.at(-1)?.type === "ThrowStatement");
|
|
27
|
+
}
|
|
28
|
+
export const noUnsettledPromise = defineRule({
|
|
29
|
+
meta: {
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: { description: "Require a Promise executor that runs code to settle the promise." },
|
|
32
|
+
schema: [],
|
|
33
|
+
messages: {
|
|
34
|
+
unsettled: "This executor never calls `resolve` or `reject`, so the promise never settles. Return the underlying promise instead, or settle it when the work finishes.",
|
|
35
|
+
optional: "This executor only settles inside an optional call, so the promise never settles when the target is missing. Settle it on both paths.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
create(context) {
|
|
39
|
+
const sourceCode = context.sourceCode;
|
|
40
|
+
return {
|
|
41
|
+
NewExpression(node) {
|
|
42
|
+
const [executor] = node.arguments;
|
|
43
|
+
if (!isGlobal(sourceCode, node.callee, "Promise") ||
|
|
44
|
+
(executor?.type !== "ArrowFunctionExpression" &&
|
|
45
|
+
executor?.type !== "FunctionExpression") ||
|
|
46
|
+
doesNothing(executor) ||
|
|
47
|
+
endsByThrowing(executor) ||
|
|
48
|
+
executor.params.some((parameter) => parameter.type !== "Identifier")) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const references = sourceCode
|
|
52
|
+
.getDeclaredVariables(executor)
|
|
53
|
+
.filter((variable) => variable.defs.some((definition) => definition.type === "Parameter"))
|
|
54
|
+
.flatMap((variable) => variable.references);
|
|
55
|
+
if (references.length === 0) {
|
|
56
|
+
context.report({ node: executor, messageId: "unsettled" });
|
|
57
|
+
}
|
|
58
|
+
else if (references.every((reference) => isOptionallyReached(reference, executor))) {
|
|
59
|
+
context.report({ node: executor, messageId: "optional" });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const requireSuppressionReason: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
const disableDirective = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?(?=\s|$)(?<rest>.*)$/su;
|
|
3
|
+
export const requireSuppressionReason = defineRule({
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: { description: "Require each lint suppression to name its rules and give a reason." },
|
|
7
|
+
schema: [],
|
|
8
|
+
messages: {
|
|
9
|
+
rule: "Name the rules this directive suppresses, so it can't hide other diagnostics.",
|
|
10
|
+
reason: "Give this suppression a reason after `--` that says why the reported code is correct here.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
return {
|
|
15
|
+
Program() {
|
|
16
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
17
|
+
const rest = disableDirective.exec(comment.value)?.groups?.["rest"];
|
|
18
|
+
if (rest === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const [rules = "", reason = ""] = rest.split(/\s-{2,}(?:\s|$)/u, 2);
|
|
22
|
+
if (rules.trim() === "") {
|
|
23
|
+
context.report({ loc: comment.loc, messageId: "rule" });
|
|
24
|
+
}
|
|
25
|
+
else if (reason.trim() === "") {
|
|
26
|
+
context.report({ loc: comment.loc, messageId: "reason" });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|