@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,136 @@
1
+ import { dirname, join } from "node:path";
2
+ import { defineRule } from "@oxlint/plugins";
3
+ import { apiReference, childNodes, projectPath, propertyName, variableFor, } from "../shared/index.js";
4
+ const comparisonMatchers = new Set([
5
+ "toBe",
6
+ "toEqual",
7
+ "toStrictEqual",
8
+ "toMatchObject",
9
+ "toContain",
10
+ "toContainEqual",
11
+ "toMatch",
12
+ "toHaveProperty",
13
+ "toBeCloseTo",
14
+ ]);
15
+ const testSupport = /(?:^|[/.])(?:tests?|__tests__|e2e|testing|fixtures?|mocks?|fakes?|stubs?|builders?|factories|support|helpers?)(?:[/.]|$)/u;
16
+ const projectSpecifier = /^(?:\.{1,2}\/|@\/|~\/|#)/u;
17
+ function isProductionImport(sourceCode, directory, callee) {
18
+ const definition = variableFor(sourceCode, callee)?.defs[0];
19
+ const declaration = definition?.parent;
20
+ if (definition?.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
21
+ return false;
22
+ }
23
+ const source = declaration.source.value;
24
+ const path = source.startsWith(".") ? join(directory, source) : source;
25
+ return projectSpecifier.test(source) && !testSupport.test(path.replaceAll("\\", "/"));
26
+ }
27
+ function calledName(sourceCode, directory, callee) {
28
+ if (callee.type === "Identifier") {
29
+ return isProductionImport(sourceCode, directory, callee) ? callee.name : null;
30
+ }
31
+ const name = callee.type === "MemberExpression" ? propertyName(callee) : null;
32
+ const namespace = callee.type === "MemberExpression" ? callee.object : null;
33
+ return namespace?.type === "Identifier" &&
34
+ name !== null &&
35
+ variableFor(sourceCode, namespace)?.defs[0]?.node.type === "ImportNamespaceSpecifier" &&
36
+ isProductionImport(sourceCode, directory, namespace)
37
+ ? `${namespace.name}.${name}`
38
+ : null;
39
+ }
40
+ function constantValue(sourceCode, node) {
41
+ const variable = node.type === "Identifier" ? variableFor(sourceCode, node) : null;
42
+ const definition = variable?.defs[0];
43
+ return variable !== null &&
44
+ definition?.type === "Variable" &&
45
+ definition.parent?.type === "VariableDeclaration" &&
46
+ definition.parent.kind === "const" &&
47
+ definition.node.type === "VariableDeclarator" &&
48
+ definition.node.id.type === "Identifier" &&
49
+ definition.node.init !== null
50
+ ? { variable, value: definition.node.init }
51
+ : null;
52
+ }
53
+ function isExpectedArgument(node) {
54
+ const call = node.parent;
55
+ return (call?.type === "CallExpression" &&
56
+ call.arguments.some((argument) => argument === node) &&
57
+ call.callee.type === "MemberExpression" &&
58
+ comparisonMatchers.has(propertyName(call.callee) ?? ""));
59
+ }
60
+ function subjectRoots(sourceCode, argument) {
61
+ const constant = constantValue(sourceCode, argument);
62
+ return constant === null ? [argument] : [argument, constant.value];
63
+ }
64
+ function expectedRoot(sourceCode, argument) {
65
+ const constant = constantValue(sourceCode, argument);
66
+ return constant !== null &&
67
+ constant.variable.references.every((reference) => !reference.isRead() || isExpectedArgument(reference.identifier))
68
+ ? constant.value
69
+ : argument;
70
+ }
71
+ function productionCalls(sourceCode, directory, root) {
72
+ const names = [];
73
+ const pending = [root];
74
+ for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
75
+ const name = node.type === "CallExpression" ? calledName(sourceCode, directory, node.callee) : null;
76
+ if (name !== null) {
77
+ names.push(name);
78
+ }
79
+ if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") {
80
+ pending.push(...childNodes(node, sourceCode.visitorKeys));
81
+ }
82
+ }
83
+ return names;
84
+ }
85
+ function positiveExpectation(sourceCode, node) {
86
+ let current = node;
87
+ while (current.type === "MemberExpression") {
88
+ if (propertyName(current) === "not") {
89
+ return null;
90
+ }
91
+ current = current.object;
92
+ }
93
+ return current.type === "CallExpression" &&
94
+ apiReference(sourceCode, current.callee)?.name === "expect"
95
+ ? current
96
+ : null;
97
+ }
98
+ export const noDerivedTestExpectation = defineRule({
99
+ meta: {
100
+ type: "suggestion",
101
+ docs: {
102
+ description: "Disallow expected values computed by the production code under test.",
103
+ },
104
+ schema: [],
105
+ messages: {
106
+ derived: "The expected value calls `{{name}}` from production code, so a bug in it changes both sides and the assertion still passes. Write the expected value out, or derive it independently.",
107
+ },
108
+ },
109
+ create(context) {
110
+ const sourceCode = context.sourceCode;
111
+ const directory = dirname(projectPath(context.cwd, context.filename));
112
+ return {
113
+ CallExpression(node) {
114
+ const matcher = node.callee.type === "MemberExpression" ? propertyName(node.callee) : null;
115
+ const expectation = node.callee.type === "MemberExpression" &&
116
+ matcher !== null &&
117
+ comparisonMatchers.has(matcher)
118
+ ? positiveExpectation(sourceCode, node.callee.object)
119
+ : null;
120
+ if (expectation === null) {
121
+ return;
122
+ }
123
+ const subjectCalls = new Set(expectation.arguments
124
+ .flatMap((argument) => subjectRoots(sourceCode, argument))
125
+ .flatMap((root) => productionCalls(sourceCode, directory, root)));
126
+ for (const argument of node.arguments) {
127
+ const name = productionCalls(sourceCode, directory, expectedRoot(sourceCode, argument)).find((call) => !subjectCalls.has(call));
128
+ if (name !== undefined) {
129
+ context.report({ node: argument, messageId: "derived", data: { name } });
130
+ return;
131
+ }
132
+ }
133
+ },
134
+ };
135
+ },
136
+ });
@@ -0,0 +1 @@
1
+ export declare const noDisabledTlsVerification: import("@oxlint/plugins").Rule;
@@ -0,0 +1,42 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { isGlobal, staticPropertyName, unwrapExpression } from "../shared/index.js";
3
+ function isFalse(node) {
4
+ const value = unwrapExpression(node);
5
+ return value.type === "Literal" && value.value === false;
6
+ }
7
+ function isDisabledFlag(node) {
8
+ const value = unwrapExpression(node);
9
+ return (value.type === "Literal" && (value.value === "0" || value.value === 0)) || isFalse(value);
10
+ }
11
+ export const noDisabledTlsVerification = defineRule({
12
+ meta: {
13
+ type: "problem",
14
+ docs: { description: "Disallow turning off TLS certificate verification." },
15
+ schema: [],
16
+ messages: {
17
+ disabled: "This turns off TLS certificate verification, so any server can impersonate the real one. Trust the right certificate authority instead, such as with the `ca` option or `NODE_EXTRA_CA_CERTS`.",
18
+ },
19
+ },
20
+ create(context) {
21
+ const sourceCode = context.sourceCode;
22
+ return {
23
+ Property(node) {
24
+ if (staticPropertyName(node) === "rejectUnauthorized" && isFalse(node.value)) {
25
+ context.report({ node, messageId: "disabled" });
26
+ }
27
+ },
28
+ AssignmentExpression(node) {
29
+ const target = unwrapExpression(node.left);
30
+ const environment = target.type === "MemberExpression" ? unwrapExpression(target.object) : null;
31
+ if (target.type === "MemberExpression" &&
32
+ staticPropertyName(target) === "NODE_TLS_REJECT_UNAUTHORIZED" &&
33
+ environment?.type === "MemberExpression" &&
34
+ staticPropertyName(environment) === "env" &&
35
+ isGlobal(sourceCode, unwrapExpression(environment.object), "process") &&
36
+ isDisabledFlag(node.right)) {
37
+ context.report({ node, messageId: "disabled" });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ });
@@ -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;