@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.
Files changed (50) hide show
  1. package/README.md +30 -163
  2. package/REFERENCE.md +105 -132
  3. package/dist/format.d.ts +5 -3
  4. package/dist/index.js +26 -0
  5. package/dist/lint.d.ts +3 -1
  6. package/dist/lint.js +153 -14
  7. package/dist/rules/explicit-api-states.js +7 -1
  8. package/dist/rules/import-boundaries.js +41 -13
  9. package/dist/rules/no-derived-test-expectation.d.ts +1 -0
  10. package/dist/rules/no-derived-test-expectation.js +136 -0
  11. package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
  12. package/dist/rules/no-disabled-tls-verification.js +42 -0
  13. package/dist/rules/no-fixed-test-delay.js +46 -5
  14. package/dist/rules/no-generic-object-guard.d.ts +1 -0
  15. package/dist/rules/no-generic-object-guard.js +279 -0
  16. package/dist/rules/no-get-password-form.d.ts +1 -0
  17. package/dist/rules/no-get-password-form.js +113 -0
  18. package/dist/rules/no-hardcoded-credential.d.ts +1 -0
  19. package/dist/rules/no-hardcoded-credential.js +45 -0
  20. package/dist/rules/no-import-time-effects.d.ts +1 -0
  21. package/dist/rules/no-import-time-effects.js +90 -0
  22. package/dist/rules/no-optional-test-action.d.ts +1 -0
  23. package/dist/rules/no-optional-test-action.js +45 -0
  24. package/dist/rules/no-secret-fallback.d.ts +1 -0
  25. package/dist/rules/no-secret-fallback.js +64 -0
  26. package/dist/rules/no-swallowed-errors.js +131 -16
  27. package/dist/rules/no-test-timeout-overrides.js +22 -9
  28. package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
  29. package/dist/rules/no-unchecked-fetch-response.js +91 -0
  30. package/dist/rules/no-unchecked-index-search.d.ts +1 -0
  31. package/dist/rules/no-unchecked-index-search.js +41 -0
  32. package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
  33. package/dist/rules/no-uncontrolled-test-clock.js +112 -0
  34. package/dist/rules/no-unsettled-promise.d.ts +1 -0
  35. package/dist/rules/no-unsettled-promise.js +64 -0
  36. package/dist/rules/require-suppression-reason.d.ts +1 -0
  37. package/dist/rules/require-suppression-reason.js +32 -0
  38. package/dist/shared/index.d.ts +9 -0
  39. package/dist/shared/index.js +70 -0
  40. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
  41. package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
  42. package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
  43. package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
  44. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
  45. package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
  46. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
  47. package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
  48. package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
  49. package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
  50. 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 isExistingPromise(node) {
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" || value.type === "MemberExpression";
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: { description: "Disallow catch handlers that discard the error and return a fallback." },
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 [callback, ...extraArguments] = node.arguments;
153
- if (node.callee.type !== "MemberExpression" ||
154
- propertyName(node.callee) !== "catch" ||
155
- callback === undefined ||
156
- extraArguments.length > 0) {
157
- return;
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 promise = node.callee.object;
160
- if (!isExistingPromise(promise) &&
161
- !isProbe(sourceCode, promise) &&
162
- swallowingCallback(sourceCode, callback)) {
163
- context.report({ node: callback, messageId: "swallowed" });
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
- return (node?.type === "ObjectExpression" &&
25
- node.properties.some((property) => {
26
- const name = property.type === "Property" ? propertyName(property) : null;
27
- return name !== null && keys.has(name);
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
- return (node !== undefined &&
32
- node.type !== "ObjectExpression" &&
33
- node.type !== "SpreadElement" &&
34
- !functionTypes.has(node.type));
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;