@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.
Files changed (49) hide show
  1. package/README.md +30 -186
  2. package/REFERENCE.md +105 -132
  3. package/dist/index.js +26 -0
  4. package/dist/lint.d.ts +1 -0
  5. package/dist/lint.js +124 -4
  6. package/dist/rules/explicit-api-states.js +7 -1
  7. package/dist/rules/import-boundaries.js +31 -6
  8. package/dist/rules/no-derived-test-expectation.d.ts +1 -0
  9. package/dist/rules/no-derived-test-expectation.js +136 -0
  10. package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
  11. package/dist/rules/no-disabled-tls-verification.js +42 -0
  12. package/dist/rules/no-fixed-test-delay.js +46 -5
  13. package/dist/rules/no-generic-object-guard.d.ts +1 -0
  14. package/dist/rules/no-generic-object-guard.js +279 -0
  15. package/dist/rules/no-get-password-form.d.ts +1 -0
  16. package/dist/rules/no-get-password-form.js +113 -0
  17. package/dist/rules/no-hardcoded-credential.d.ts +1 -0
  18. package/dist/rules/no-hardcoded-credential.js +45 -0
  19. package/dist/rules/no-import-time-effects.d.ts +1 -0
  20. package/dist/rules/no-import-time-effects.js +90 -0
  21. package/dist/rules/no-optional-test-action.d.ts +1 -0
  22. package/dist/rules/no-optional-test-action.js +45 -0
  23. package/dist/rules/no-secret-fallback.d.ts +1 -0
  24. package/dist/rules/no-secret-fallback.js +64 -0
  25. package/dist/rules/no-swallowed-errors.js +131 -16
  26. package/dist/rules/no-test-timeout-overrides.js +22 -9
  27. package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
  28. package/dist/rules/no-unchecked-fetch-response.js +91 -0
  29. package/dist/rules/no-unchecked-index-search.d.ts +1 -0
  30. package/dist/rules/no-unchecked-index-search.js +41 -0
  31. package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
  32. package/dist/rules/no-uncontrolled-test-clock.js +112 -0
  33. package/dist/rules/no-unsettled-promise.d.ts +1 -0
  34. package/dist/rules/no-unsettled-promise.js +64 -0
  35. package/dist/rules/require-suppression-reason.d.ts +1 -0
  36. package/dist/rules/require-suppression-reason.js +32 -0
  37. package/dist/shared/index.d.ts +9 -0
  38. package/dist/shared/index.js +70 -0
  39. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
  40. package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
  41. package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
  42. package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
  43. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
  44. package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
  45. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
  46. package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
  47. package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
  48. package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
  49. 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 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;
@@ -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
+ });