@jameswyse/oxc-config 0.1.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 -163
- package/REFERENCE.md +105 -132
- package/dist/format.d.ts +5 -3
- package/dist/index.js +26 -0
- package/dist/lint.d.ts +3 -1
- package/dist/lint.js +153 -14
- package/dist/rules/explicit-api-states.js +7 -1
- package/dist/rules/import-boundaries.js +41 -13
- 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,45 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
const credentials = [
|
|
3
|
+
[
|
|
4
|
+
"private key",
|
|
5
|
+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?(?:-----END|$)/gu,
|
|
6
|
+
],
|
|
7
|
+
["AWS access key", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/gu],
|
|
8
|
+
["GitHub token", /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b/gu],
|
|
9
|
+
["Stripe live key", /\b(?:sk|rk)_live_[0-9A-Za-z]{20,}\b/gu],
|
|
10
|
+
["Slack token", /\bxox[abposr]-[0-9A-Za-z-]{10,}\b/gu],
|
|
11
|
+
["Google API key", /\bAIza[0-9A-Za-z_-]{35}\b/gu],
|
|
12
|
+
["Anthropic or OpenAI key", /\bsk-(?:ant-[a-z0-9]+-|proj-)[A-Za-z0-9_-]{32,}/gu],
|
|
13
|
+
];
|
|
14
|
+
const placeholder = /example|fake|dummy|placeholder|redacted|x{6,}|abcdefgh|12345678/iu;
|
|
15
|
+
function credentialIn(text) {
|
|
16
|
+
return (credentials.find(([, pattern]) => [...text.matchAll(pattern)].some((match) => !placeholder.test(match[0])))?.[0] ?? null);
|
|
17
|
+
}
|
|
18
|
+
export const noHardcodedCredential = defineRule({
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: { description: "Disallow credentials written into source code." },
|
|
22
|
+
schema: [],
|
|
23
|
+
messages: {
|
|
24
|
+
credential: "This looks like a real {{kind}}. Revoke it, then load the credential from the environment or a secret store.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
function check(node, text) {
|
|
29
|
+
const kind = credentialIn(text);
|
|
30
|
+
if (kind !== null) {
|
|
31
|
+
context.report({ node, messageId: "credential", data: { kind } });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
Literal(node) {
|
|
36
|
+
if (typeof node.value === "string") {
|
|
37
|
+
check(node, node.value);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
TemplateElement(node) {
|
|
41
|
+
check(node, node.value.raw);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noImportTimeEffects: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { childNodes, isGlobalValue, propertyName, unwrapExpression } from "../shared/index.js";
|
|
3
|
+
const functionTypes = new Set([
|
|
4
|
+
"ArrowFunctionExpression",
|
|
5
|
+
"FunctionExpression",
|
|
6
|
+
"FunctionDeclaration",
|
|
7
|
+
]);
|
|
8
|
+
const startedTimers = ["setInterval", "setTimeout", "fetch"];
|
|
9
|
+
const openedResources = ["BroadcastChannel", "WebSocket", "EventSource", "Worker", "SharedWorker"];
|
|
10
|
+
const registrations = new Set(["addEventListener", "listen"]);
|
|
11
|
+
function isDeferred(node) {
|
|
12
|
+
return (functionTypes.has(node.type) ||
|
|
13
|
+
((node.type === "PropertyDefinition" || node.type === "AccessorProperty") && !node.static));
|
|
14
|
+
}
|
|
15
|
+
function isInsideFunction(node) {
|
|
16
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
17
|
+
if (functionTypes.has(ancestor.type)) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function immediateBody(node) {
|
|
24
|
+
const callee = node.type === "CallExpression" ? unwrapExpression(node.callee) : null;
|
|
25
|
+
return callee?.type === "ArrowFunctionExpression" ||
|
|
26
|
+
(callee?.type === "FunctionExpression" && !callee.generator)
|
|
27
|
+
? callee.body
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
30
|
+
function effect(sourceCode, node) {
|
|
31
|
+
if (node.type === "AwaitExpression") {
|
|
32
|
+
return isInsideFunction(node) ? null : "await";
|
|
33
|
+
}
|
|
34
|
+
if (node.type === "NewExpression") {
|
|
35
|
+
return openedResources.find((name) => isGlobalValue(sourceCode, node.callee, name)) ?? null;
|
|
36
|
+
}
|
|
37
|
+
if (node.type !== "CallExpression") {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const timer = startedTimers.find((name) => isGlobalValue(sourceCode, node.callee, name));
|
|
41
|
+
if (timer !== undefined) {
|
|
42
|
+
return timer;
|
|
43
|
+
}
|
|
44
|
+
const method = node.callee.type === "MemberExpression" ? propertyName(node.callee) : null;
|
|
45
|
+
if (method !== null && registrations.has(method)) {
|
|
46
|
+
return method;
|
|
47
|
+
}
|
|
48
|
+
return node.callee.type === "MemberExpression" &&
|
|
49
|
+
(method === "on" || method === "once") &&
|
|
50
|
+
isGlobalValue(sourceCode, node.callee.object, "process")
|
|
51
|
+
? `process.${method}`
|
|
52
|
+
: null;
|
|
53
|
+
}
|
|
54
|
+
export const noImportTimeEffects = defineRule({
|
|
55
|
+
meta: {
|
|
56
|
+
type: "suggestion",
|
|
57
|
+
docs: {
|
|
58
|
+
description: "Disallow timers, I/O, open resources and handler registration when a module is imported.",
|
|
59
|
+
},
|
|
60
|
+
schema: [],
|
|
61
|
+
messages: {
|
|
62
|
+
effect: "`{{effect}}` runs when this module is imported, so every importer starts it, including tests. Move it into a function the entrypoint calls.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
create(context) {
|
|
66
|
+
const sourceCode = context.sourceCode;
|
|
67
|
+
return {
|
|
68
|
+
Program(program) {
|
|
69
|
+
const pending = [...program.body];
|
|
70
|
+
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
|
|
71
|
+
const found = effect(sourceCode, node);
|
|
72
|
+
if (found !== null) {
|
|
73
|
+
context.report({ node, messageId: "effect", data: { effect: found } });
|
|
74
|
+
}
|
|
75
|
+
const body = immediateBody(node);
|
|
76
|
+
if (body !== null) {
|
|
77
|
+
pending.push(body);
|
|
78
|
+
}
|
|
79
|
+
if (!isDeferred(node)) {
|
|
80
|
+
pending.push(...childNodes(node, sourceCode.visitorKeys));
|
|
81
|
+
}
|
|
82
|
+
else if ((node.type === "PropertyDefinition" || node.type === "AccessorProperty") &&
|
|
83
|
+
node.computed) {
|
|
84
|
+
pending.push(node.key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noOptionalTestAction: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isShortCircuited, testContext } from "../shared/index.js";
|
|
3
|
+
function isStep(chain) {
|
|
4
|
+
const step = chain.parent.type === "AwaitExpression" ? chain.parent : chain;
|
|
5
|
+
const position = step.parent;
|
|
6
|
+
return (position.type === "ExpressionStatement" ||
|
|
7
|
+
position.type === "ReturnStatement" ||
|
|
8
|
+
(position.type === "ArrowFunctionExpression" && position.body === step));
|
|
9
|
+
}
|
|
10
|
+
function enclosingFunction(node) {
|
|
11
|
+
for (let ancestor = node.parent; ancestor !== null; ancestor = ancestor.parent) {
|
|
12
|
+
if (ancestor.type === "ArrowFunctionExpression" ||
|
|
13
|
+
ancestor.type === "FunctionExpression" ||
|
|
14
|
+
ancestor.type === "FunctionDeclaration") {
|
|
15
|
+
return ancestor;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export const noOptionalTestAction = defineRule({
|
|
21
|
+
meta: {
|
|
22
|
+
type: "problem",
|
|
23
|
+
docs: { description: "Disallow test steps that an optional call can silently skip." },
|
|
24
|
+
schema: [],
|
|
25
|
+
messages: {
|
|
26
|
+
optional: "This optional call skips the step when its target is missing, so the test can pass without doing it. Get the target with a query that fails when it's absent.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
create(context) {
|
|
30
|
+
const sourceCode = context.sourceCode;
|
|
31
|
+
return {
|
|
32
|
+
ChainExpression(node) {
|
|
33
|
+
const call = node.expression;
|
|
34
|
+
if (call.type !== "CallExpression" || !isShortCircuited(call) || !isStep(node)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const test = testContext(sourceCode, node);
|
|
38
|
+
if ((test?.api.name === "test" || test?.api.name === "it") &&
|
|
39
|
+
enclosingFunction(node) === test.callback) {
|
|
40
|
+
context.report({ node: call, messageId: "optional" });
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noSecretFallback: import("@oxlint/plugins").Rule;
|
|
@@ -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;
|