@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
@@ -1,6 +1,21 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { variableFor, isGlobal, propertyName, isPlaywright, apiReference, testContext, } from "../shared/index.js";
3
- function numericLiteral(node) {
2
+ import { variableFor, isGlobal, propertyName, isPlaywright, apiReference, isEffectModule, testContext, } from "../shared/index.js";
3
+ function constantValue(sourceCode, node) {
4
+ if (node?.type !== "Identifier") {
5
+ return node;
6
+ }
7
+ const [definition, ...others] = variableFor(sourceCode, node)?.defs ?? [];
8
+ const declarator = definition?.node;
9
+ return others.length === 0 &&
10
+ definition?.type === "Variable" &&
11
+ definition.parent?.type === "VariableDeclaration" &&
12
+ definition.parent.kind === "const" &&
13
+ declarator?.type === "VariableDeclarator"
14
+ ? declarator.init
15
+ : node;
16
+ }
17
+ function numericLiteral(sourceCode, argument) {
18
+ const node = constantValue(sourceCode, argument);
4
19
  return ((node?.type === "Literal" && typeof node.value === "number") ||
5
20
  (node?.type === "UnaryExpression" &&
6
21
  ["+", "-"].includes(node.operator) &&
@@ -64,7 +79,7 @@ function timerPromise(sourceCode, node) {
64
79
  const timer = singleExpression(executor.body);
65
80
  if (timer?.type !== "CallExpression" ||
66
81
  !globalTimer(sourceCode, timer.callee) ||
67
- !numericLiteral(timer.arguments[1])) {
82
+ !numericLiteral(sourceCode, timer.arguments[1])) {
68
83
  return false;
69
84
  }
70
85
  return resolvesWithoutValue(sourceCode, timer.arguments[0], resolve);
@@ -86,6 +101,26 @@ function playwrightPage(sourceCode, node, enclosing) {
86
101
  variableFor(sourceCode, binding) === variableFor(sourceCode, node));
87
102
  });
88
103
  }
104
+ function isFixedDuration(sourceCode, argument) {
105
+ const node = constantValue(sourceCode, argument);
106
+ if (node?.type === "Literal") {
107
+ return typeof node.value === "string" || typeof node.value === "number";
108
+ }
109
+ const owner = node?.type === "CallExpression" && node.callee.type === "MemberExpression"
110
+ ? node.callee.object
111
+ : null;
112
+ return (node?.type === "CallExpression" &&
113
+ owner !== null &&
114
+ isEffectModule(sourceCode, owner, "Duration") &&
115
+ node.arguments.length > 0 &&
116
+ node.arguments.every((item) => numericLiteral(sourceCode, item)));
117
+ }
118
+ function isEffectSleep(sourceCode, node) {
119
+ return (node.callee.type === "MemberExpression" &&
120
+ propertyName(node.callee) === "sleep" &&
121
+ isEffectModule(sourceCode, node.callee.object, "Effect") &&
122
+ isFixedDuration(sourceCode, node.arguments[0]));
123
+ }
89
124
  export const noFixedTestDelay = defineRule({
90
125
  meta: {
91
126
  type: "problem",
@@ -99,6 +134,12 @@ export const noFixedTestDelay = defineRule({
99
134
  const sourceCode = context.sourceCode;
100
135
  return {
101
136
  CallExpression(node) {
137
+ if (node.parent.type === "YieldExpression" &&
138
+ node.parent.delegate &&
139
+ isEffectSleep(sourceCode, node) &&
140
+ testContext(sourceCode, node)) {
141
+ context.report({ node, messageId: "delay" });
142
+ }
102
143
  if (node.parent.type !== "AwaitExpression") {
103
144
  return;
104
145
  }
@@ -109,10 +150,10 @@ export const noFixedTestDelay = defineRule({
109
150
  const api = apiReference(sourceCode, node.callee);
110
151
  const importedTimer = api?.name === "setTimeout" &&
111
152
  ["node:timers/promises", "timers/promises"].includes(api.module) &&
112
- numericLiteral(node.arguments[0]);
153
+ numericLiteral(sourceCode, node.arguments[0]);
113
154
  const browserTimer = node.callee.type === "MemberExpression" &&
114
155
  propertyName(node.callee) === "waitForTimeout" &&
115
- numericLiteral(node.arguments[0]) &&
156
+ numericLiteral(sourceCode, node.arguments[0]) &&
116
157
  playwrightPage(sourceCode, node.callee.object, enclosing);
117
158
  if (importedTimer || browserTimer) {
118
159
  context.report({ node, messageId: "delay" });
@@ -0,0 +1 @@
1
+ export declare const noGenericObjectGuard: import("@oxlint/plugins").Rule;
@@ -0,0 +1,279 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { isGlobal, propertyName, unwrapExpression } from "../shared/index.js";
3
+ function unwrap(node) {
4
+ const value = unwrapExpression(node);
5
+ return value.type === "ChainExpression" ? unwrap(value.expression) : value;
6
+ }
7
+ function combined(checks) {
8
+ if (checks.includes(null)) {
9
+ return null;
10
+ }
11
+ if (checks.includes("object")) {
12
+ return "object";
13
+ }
14
+ return checks.includes("function") ? "function" : "supporting";
15
+ }
16
+ function isSubject(node, subject) {
17
+ const value = unwrap(node);
18
+ return value.type === "Identifier" && value.name === subject;
19
+ }
20
+ function isLiteral(node, value) {
21
+ const literal = unwrap(node);
22
+ return literal.type === "Literal" && literal.value === value;
23
+ }
24
+ function isUndefined(node) {
25
+ const value = unwrap(node);
26
+ return value.type === "Identifier" && value.name === "undefined";
27
+ }
28
+ function comparedPair(node, subject, matches) {
29
+ return ((isSubject(node.left, subject) && matches(node.right)) ||
30
+ (isSubject(node.right, subject) && matches(node.left)));
31
+ }
32
+ function isTypeofSubject(node, subject) {
33
+ const value = unwrap(node);
34
+ return value.type === "UnaryExpression" && value.operator === "typeof"
35
+ ? isSubject(value.argument, subject)
36
+ : false;
37
+ }
38
+ function typeofCheck(node, subject) {
39
+ const [typeofSide, literal] = isTypeofSubject(node.left, subject)
40
+ ? [node.left, node.right]
41
+ : [node.right, node.left];
42
+ if (!isTypeofSubject(typeofSide, subject) || !["===", "=="].includes(node.operator)) {
43
+ return null;
44
+ }
45
+ if (isLiteral(literal, "object")) {
46
+ return "object";
47
+ }
48
+ return isLiteral(literal, "function") ? "function" : null;
49
+ }
50
+ function isArrayCheck(sourceCode, node, subject) {
51
+ const call = unwrap(node);
52
+ return (call.type === "CallExpression" &&
53
+ call.callee.type === "MemberExpression" &&
54
+ propertyName(call.callee) === "isArray" &&
55
+ isGlobal(sourceCode, call.callee.object, "Array") &&
56
+ call.arguments.length === 1 &&
57
+ call.arguments[0] !== undefined &&
58
+ isSubject(call.arguments[0], subject));
59
+ }
60
+ function isPrototypeRead(sourceCode, node, subject) {
61
+ const call = unwrap(node);
62
+ return (call.type === "CallExpression" &&
63
+ call.callee.type === "MemberExpression" &&
64
+ propertyName(call.callee) === "getPrototypeOf" &&
65
+ isGlobal(sourceCode, call.callee.object, "Object") &&
66
+ call.arguments[0] !== undefined &&
67
+ isSubject(call.arguments[0], subject));
68
+ }
69
+ function isObjectPrototype(sourceCode, node) {
70
+ const value = unwrap(node);
71
+ return (value.type === "MemberExpression" &&
72
+ propertyName(value) === "prototype" &&
73
+ isGlobal(sourceCode, value.object, "Object"));
74
+ }
75
+ function isObjectTag(sourceCode, node, subject) {
76
+ const call = unwrap(node);
77
+ const tag = call.type === "CallExpression" && call.callee.type === "MemberExpression"
78
+ ? call.callee.object
79
+ : null;
80
+ return (call.type === "CallExpression" &&
81
+ call.callee.type === "MemberExpression" &&
82
+ propertyName(call.callee) === "call" &&
83
+ call.arguments[0] !== undefined &&
84
+ isSubject(call.arguments[0], subject) &&
85
+ tag?.type === "MemberExpression" &&
86
+ propertyName(tag) === "toString" &&
87
+ isObjectPrototype(sourceCode, tag.object));
88
+ }
89
+ function provesObject(sourceCode, value, expected, subject) {
90
+ return ((isPrototypeRead(sourceCode, value, subject) &&
91
+ (isLiteral(expected, null) || isObjectPrototype(sourceCode, expected))) ||
92
+ (isObjectTag(sourceCode, value, subject) && isLiteral(expected, "[object Object]")));
93
+ }
94
+ function equalityCheck(sourceCode, comparison, subject) {
95
+ const typeofResult = typeofCheck(comparison, subject);
96
+ if (typeofResult !== null) {
97
+ return typeofResult;
98
+ }
99
+ if (provesObject(sourceCode, comparison.left, comparison.right, subject) ||
100
+ provesObject(sourceCode, comparison.right, comparison.left, subject)) {
101
+ return "object";
102
+ }
103
+ return null;
104
+ }
105
+ function objectCheck(sourceCode, node, subject) {
106
+ const value = unwrap(node);
107
+ if (value.type === "LogicalExpression" && value.operator !== "??") {
108
+ return combined([
109
+ objectCheck(sourceCode, value.left, subject),
110
+ objectCheck(sourceCode, value.right, subject),
111
+ ]);
112
+ }
113
+ if (value.type === "UnaryExpression" && value.operator === "!") {
114
+ const argument = unwrap(value.argument);
115
+ if (isArrayCheck(sourceCode, argument, subject)) {
116
+ return "supporting";
117
+ }
118
+ return argument.type === "UnaryExpression" && argument.operator === "!"
119
+ ? objectCheck(sourceCode, argument.argument, subject)
120
+ : null;
121
+ }
122
+ if (isSubject(value, subject)) {
123
+ return "supporting";
124
+ }
125
+ if (value.type === "CallExpression" && isGlobal(sourceCode, value.callee, "Boolean")) {
126
+ const [argument] = value.arguments;
127
+ return argument !== undefined && isSubject(argument, subject) ? "supporting" : null;
128
+ }
129
+ if (value.type !== "BinaryExpression") {
130
+ return null;
131
+ }
132
+ if (value.operator === "instanceof") {
133
+ return isSubject(value.left, subject) && isGlobal(sourceCode, value.right, "Object")
134
+ ? "object"
135
+ : null;
136
+ }
137
+ if (["!==", "!="].includes(value.operator)) {
138
+ return comparedPair(value, subject, (other) => isLiteral(other, null) || isUndefined(other))
139
+ ? "supporting"
140
+ : null;
141
+ }
142
+ return ["===", "=="].includes(value.operator) ? equalityCheck(sourceCode, value, subject) : null;
143
+ }
144
+ function rejection(sourceCode, node, subject) {
145
+ const value = unwrap(node);
146
+ if (value.type === "LogicalExpression" && value.operator !== "??") {
147
+ return combined([
148
+ rejection(sourceCode, value.left, subject),
149
+ rejection(sourceCode, value.right, subject),
150
+ ]);
151
+ }
152
+ if (value.type === "UnaryExpression" && value.operator === "!") {
153
+ return objectCheck(sourceCode, value.argument, subject);
154
+ }
155
+ if (isArrayCheck(sourceCode, value, subject)) {
156
+ return "supporting";
157
+ }
158
+ if (value.type !== "BinaryExpression") {
159
+ return null;
160
+ }
161
+ if (["===", "=="].includes(value.operator)) {
162
+ return comparedPair(value, subject, (other) => isLiteral(other, null) || isUndefined(other))
163
+ ? "supporting"
164
+ : null;
165
+ }
166
+ return ["!==", "!="].includes(value.operator)
167
+ ? equalityCheck(sourceCode, { left: value.left, operator: "===", right: value.right }, subject)
168
+ : null;
169
+ }
170
+ function isNamedHelper(fn) {
171
+ const holder = fn.parent;
172
+ return (fn.type === "FunctionDeclaration" ||
173
+ (holder.type === "VariableDeclarator" && holder.init === fn) ||
174
+ (holder.type === "Property" && holder.value === fn) ||
175
+ (holder.type === "PropertyDefinition" && holder.value === fn) ||
176
+ holder.type === "MethodDefinition");
177
+ }
178
+ const openTypes = new Set(["TSUnknownKeyword", "TSAnyKeyword"]);
179
+ const weakClaims = new Set(["TSObjectKeyword", "TSUnknownKeyword", "TSAnyKeyword"]);
180
+ function isWeakClaim(claim) {
181
+ if (weakClaims.has(claim.type) ||
182
+ (claim.type === "TSTypeLiteral" && claim.members.length === 0)) {
183
+ return true;
184
+ }
185
+ const [argument] = claim.type === "TSTypeReference" ? (claim.typeArguments?.params ?? []) : [];
186
+ return (claim.type === "TSTypeReference" &&
187
+ claim.typeName.type === "Identifier" &&
188
+ claim.typeName.name === "NonNullable" &&
189
+ argument !== undefined &&
190
+ isWeakClaim(argument));
191
+ }
192
+ function unprovenClaim(sourceCode, fn, parameter) {
193
+ const predicate = fn.returnType?.typeAnnotation;
194
+ const parameterType = parameter.typeAnnotation?.typeAnnotation;
195
+ if (predicate?.type !== "TSTypePredicate" ||
196
+ predicate.parameterName.type !== "Identifier" ||
197
+ predicate.parameterName.name !== parameter.name ||
198
+ predicate.typeAnnotation === null ||
199
+ parameterType === undefined ||
200
+ !openTypes.has(parameterType.type)) {
201
+ return null;
202
+ }
203
+ const claim = predicate.typeAnnotation.typeAnnotation;
204
+ return isWeakClaim(claim) ? null : sourceCode.getText(claim);
205
+ }
206
+ function returnedCheck(sourceCode, node, subject) {
207
+ return isLiteral(node, true) ? "supporting" : objectCheck(sourceCode, node, subject);
208
+ }
209
+ function rejectedCheck(sourceCode, statement, subject) {
210
+ if (statement.type !== "IfStatement" || statement.alternate !== null) {
211
+ return null;
212
+ }
213
+ const consequent = statement.consequent;
214
+ const exit = consequent.type === "BlockStatement" && consequent.body.length === 1
215
+ ? consequent.body[0]
216
+ : consequent;
217
+ return exit?.type === "ReturnStatement" &&
218
+ exit.argument !== null &&
219
+ isLiteral(exit.argument, false)
220
+ ? rejection(sourceCode, statement.test, subject)
221
+ : null;
222
+ }
223
+ function guardProof(sourceCode, fn, subject) {
224
+ if (fn.body === null) {
225
+ return null;
226
+ }
227
+ if (fn.body.type !== "BlockStatement") {
228
+ return returnedCheck(sourceCode, fn.body, subject);
229
+ }
230
+ const statements = fn.body.body;
231
+ const last = statements.at(-1);
232
+ if (last?.type !== "ReturnStatement" || last.argument === null) {
233
+ return null;
234
+ }
235
+ return combined([
236
+ ...statements.slice(0, -1).map((statement) => rejectedCheck(sourceCode, statement, subject)),
237
+ returnedCheck(sourceCode, last.argument, subject),
238
+ ]);
239
+ }
240
+ export const noGenericObjectGuard = defineRule({
241
+ meta: {
242
+ type: "suggestion",
243
+ docs: {
244
+ description: "Disallow guards such as isRecord that only prove a value is a non-null object, and guards that claim a type they never check.",
245
+ },
246
+ schema: [],
247
+ messages: {
248
+ unproven: "This guard claims `{{subject}}` is `{{claim}}` but only checks that it exists. Check the fields the type promises, or parse the value with a schema.",
249
+ guard: "This guard only proves `{{subject}}` is an object, which says nothing about its fields. Parse the value with a schema where it enters, or narrow inline where only its object-ness matters.",
250
+ },
251
+ },
252
+ create(context) {
253
+ const sourceCode = context.sourceCode;
254
+ function check(fn) {
255
+ const [parameter, ...rest] = fn.params;
256
+ if (parameter?.type !== "Identifier" || rest.length > 0) {
257
+ return;
258
+ }
259
+ const proof = guardProof(sourceCode, fn, parameter.name);
260
+ if (proof === "object" && isNamedHelper(fn)) {
261
+ context.report({ node: fn, messageId: "guard", data: { subject: parameter.name } });
262
+ return;
263
+ }
264
+ const claim = unprovenClaim(sourceCode, fn, parameter);
265
+ if (claim !== null && proof === "supporting") {
266
+ context.report({
267
+ node: fn,
268
+ messageId: "unproven",
269
+ data: { subject: parameter.name, claim },
270
+ });
271
+ }
272
+ }
273
+ return {
274
+ ArrowFunctionExpression: check,
275
+ FunctionDeclaration: check,
276
+ FunctionExpression: check,
277
+ };
278
+ },
279
+ });
@@ -0,0 +1 @@
1
+ export declare const noGetPasswordForm: import("@oxlint/plugins").Rule;
@@ -0,0 +1,113 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ const secretAutocomplete = new Set(["current-password", "new-password", "one-time-code"]);
3
+ function isOmitted(item) {
4
+ const value = item.value?.type === "JSXExpressionContainer" ? item.value.expression : null;
5
+ return ((value?.type === "Identifier" && value.name === "undefined") ||
6
+ (value?.type === "Literal" && value.value === null));
7
+ }
8
+ function attribute(element, name) {
9
+ return element.attributes.find((item) => item.type === "JSXAttribute" &&
10
+ item.name.type === "JSXIdentifier" &&
11
+ item.name.name === name &&
12
+ !isOmitted(item));
13
+ }
14
+ function stringValue(item) {
15
+ const value = item?.value?.type === "JSXExpressionContainer" ? item.value.expression : item?.value;
16
+ if (value?.type === "TemplateLiteral" && value.expressions.length === 0) {
17
+ return value.quasis[0]?.value.cooked ?? null;
18
+ }
19
+ return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
20
+ }
21
+ function literalValue(item) {
22
+ return stringValue(item)?.toLowerCase() ?? null;
23
+ }
24
+ function isElement(element, name) {
25
+ return element.name.type === "JSXIdentifier" && element.name.name === name;
26
+ }
27
+ function isSecretInput(element) {
28
+ return (isElement(element, "input") &&
29
+ (literalValue(attribute(element, "type")) === "password" ||
30
+ (literalValue(attribute(element, "autoComplete")) ?? "")
31
+ .split(/\s+/u)
32
+ .some((token) => secretAutocomplete.has(token))));
33
+ }
34
+ function isGetMethod(item) {
35
+ const method = literalValue(item);
36
+ return method !== null && method !== "post" && method !== "dialog";
37
+ }
38
+ function submitsByGet(element) {
39
+ const action = attribute(element, "action");
40
+ const method = attribute(element, "method");
41
+ return (!element.attributes.some((item) => item.type === "JSXSpreadAttribute") &&
42
+ (action === undefined || literalValue(action) !== null) &&
43
+ (method === undefined || isGetMethod(method)));
44
+ }
45
+ function submitsByGetOverride(element) {
46
+ const type = literalValue(attribute(element, "type"));
47
+ const method = attribute(element, "formMethod");
48
+ const submits = isElement(element, "button")
49
+ ? type !== "button" && type !== "reset"
50
+ : isElement(element, "input") && (type === "submit" || type === "image");
51
+ return submits && method !== undefined && isGetMethod(method);
52
+ }
53
+ function owningForms(control, forms) {
54
+ const target = attribute(control, "form");
55
+ if (target !== undefined) {
56
+ const id = stringValue(target);
57
+ return id === null || id === ""
58
+ ? []
59
+ : forms.filter((form) => stringValue(attribute(form.openingElement, "id")) === id);
60
+ }
61
+ for (let ancestor = control.parent.parent; ancestor !== null; ancestor = ancestor.parent) {
62
+ if (ancestor.type === "JSXElement" && isElement(ancestor.openingElement, "form")) {
63
+ return [ancestor];
64
+ }
65
+ }
66
+ return [];
67
+ }
68
+ export const noGetPasswordForm = defineRule({
69
+ meta: {
70
+ type: "problem",
71
+ docs: { description: "Require forms with password fields to submit by POST." },
72
+ schema: [],
73
+ messages: {
74
+ get: 'This form has a password field but submits by GET, so a submission before JavaScript loads puts the password in the URL. Add `method="post"` or pass a function `action`.',
75
+ override: 'This button submits a form with a password field by GET, so a submission before JavaScript loads puts the password in the URL. Remove `formMethod` or set it to "post".',
76
+ },
77
+ },
78
+ create(context) {
79
+ const forms = [];
80
+ const controls = [];
81
+ return {
82
+ JSXElement(node) {
83
+ if (isElement(node.openingElement, "form")) {
84
+ forms.push(node);
85
+ }
86
+ },
87
+ JSXOpeningElement(node) {
88
+ if (isElement(node, "input") || isElement(node, "button")) {
89
+ controls.push(node);
90
+ }
91
+ },
92
+ "Program:exit"() {
93
+ const owned = new Map();
94
+ for (const control of controls) {
95
+ for (const form of owningForms(control, forms)) {
96
+ owned.set(form, [...(owned.get(form) ?? []), control]);
97
+ }
98
+ }
99
+ for (const [form, items] of owned) {
100
+ if (!items.some(isSecretInput)) {
101
+ continue;
102
+ }
103
+ if (submitsByGet(form.openingElement)) {
104
+ context.report({ node: form.openingElement, messageId: "get" });
105
+ }
106
+ for (const submitter of items.filter(submitsByGetOverride)) {
107
+ context.report({ node: submitter, messageId: "override" });
108
+ }
109
+ }
110
+ },
111
+ };
112
+ },
113
+ });
@@ -0,0 +1 @@
1
+ export declare const noHardcodedCredential: import("@oxlint/plugins").Rule;
@@ -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;