@jameswyse/oxc-config 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/REFERENCE.md +134 -0
- package/dist/format.d.ts +5 -0
- package/dist/format.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/lint.d.ts +33 -0
- package/dist/lint.js +493 -0
- package/dist/rules/companion-has-owner.d.ts +1 -0
- package/dist/rules/companion-has-owner.js +74 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.d.ts +1 -0
- package/dist/rules/electron-no-raw-ipc-renderer-exposure.js +127 -0
- package/dist/rules/electron-no-remote-module.d.ts +1 -0
- package/dist/rules/electron-no-remote-module.js +112 -0
- package/dist/rules/electron-secure-web-preferences.d.ts +1 -0
- package/dist/rules/electron-secure-web-preferences.js +108 -0
- package/dist/rules/explicit-api-states.d.ts +1 -0
- package/dist/rules/explicit-api-states.js +143 -0
- package/dist/rules/import-boundaries.d.ts +1 -0
- package/dist/rules/import-boundaries.js +99 -0
- package/dist/rules/no-ambient-clock.d.ts +1 -0
- package/dist/rules/no-ambient-clock.js +35 -0
- package/dist/rules/no-ambient-random.d.ts +1 -0
- package/dist/rules/no-ambient-random.js +71 -0
- package/dist/rules/no-comments.d.ts +1 -0
- package/dist/rules/no-comments.js +64 -0
- package/dist/rules/no-deep-ternary.d.ts +1 -0
- package/dist/rules/no-deep-ternary.js +32 -0
- package/dist/rules/no-edit-proof-assertions.d.ts +1 -0
- package/dist/rules/no-edit-proof-assertions.js +120 -0
- package/dist/rules/no-env-access.d.ts +1 -0
- package/dist/rules/no-env-access.js +35 -0
- package/dist/rules/no-fixed-test-delay.d.ts +1 -0
- package/dist/rules/no-fixed-test-delay.js +130 -0
- package/dist/rules/no-forwarding-module.d.ts +1 -0
- package/dist/rules/no-forwarding-module.js +158 -0
- package/dist/rules/no-generic-file-suffix.d.ts +1 -0
- package/dist/rules/no-generic-file-suffix.js +74 -0
- package/dist/rules/no-pass-through-function.d.ts +1 -0
- package/dist/rules/no-pass-through-function.js +106 -0
- package/dist/rules/no-swallowed-errors.d.ts +1 -0
- package/dist/rules/no-swallowed-errors.js +168 -0
- package/dist/rules/no-test-timeout-overrides.d.ts +1 -0
- package/dist/rules/no-test-timeout-overrides.js +105 -0
- package/dist/rules/no-vacuous-test-assertions.d.ts +1 -0
- package/dist/rules/no-vacuous-test-assertions.js +71 -0
- package/dist/rules/server-action-guard.d.ts +1 -0
- package/dist/rules/server-action-guard.js +109 -0
- package/dist/rules/statement-spacing.d.ts +1 -0
- package/dist/rules/statement-spacing.js +29 -0
- package/dist/rules/type-guard-name.d.ts +1 -0
- package/dist/rules/type-guard-name.js +59 -0
- package/dist/shared/index.d.ts +26 -0
- package/dist/shared/index.js +260 -0
- package/dist/shared/rule-options.d.ts +2 -0
- package/dist/shared/rule-options.js +8 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-manual-effect-error-tag.js +35 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.d.ts +1 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +48 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.d.ts +9 -0
- package/dist/vendor/anti-slop/effect/shared/tagged-values.js +65 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-array-filter-map.js +40 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-chained-type-assertions.js +58 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-conditional-empty-object-spread.js +41 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +333 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +74 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-object-parameters.js +59 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reduce-accumulator-copy.js +124 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-apply.js +26 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-reflect-get.js +26 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +63 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +37 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +61 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unknown-type-aliases.js +38 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +143 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.d.ts +1 -0
- package/dist/vendor/anti-slop/rules/no-widen-then-assert.js +288 -0
- package/dist/vendor/anti-slop/shared/array-method.d.ts +10 -0
- package/dist/vendor/anti-slop/shared/array-method.js +95 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.d.ts +23 -0
- package/dist/vendor/anti-slop/shared/dictionary-types.js +408 -0
- package/dist/vendor/anti-slop/shared/function-parameters.d.ts +6 -0
- package/dist/vendor/anti-slop/shared/function-parameters.js +41 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.d.ts +4 -0
- package/dist/vendor/anti-slop/shared/lexical-type-parameters.js +32 -0
- package/dist/vendor/anti-slop/shared/reflect-method.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/reflect-method.js +24 -0
- package/dist/vendor/anti-slop/shared/scope.d.ts +3 -0
- package/dist/vendor/anti-slop/shared/scope.js +12 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +21 -0
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +147 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.d.ts +14 -0
- package/dist/vendor/eslint-stylistic/padding-line-ast.js +19 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.d.ts +91 -0
- package/dist/vendor/eslint-stylistic/padding-line-between-statements.js +493 -0
- package/package.json +89 -0
- package/src/vendor/anti-slop/LICENSE +21 -0
- package/src/vendor/eslint-stylistic/LICENSE +22 -0
- package/tsconfig.base.json +10 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
const companionSegments = new Set(["test", "spec", "stories", "story", "bench", "e2e"]);
|
|
4
|
+
const genericSegment = /(?:^|[-_])(?:policy|contract)$/i;
|
|
5
|
+
const genericSuffix = /[a-z0-9](?:Policy|Contract)$/;
|
|
6
|
+
const standardTerms = [
|
|
7
|
+
"contentsecuritypolicy",
|
|
8
|
+
"permissionspolicy",
|
|
9
|
+
"featurepolicy",
|
|
10
|
+
"referrerpolicy",
|
|
11
|
+
"privacypolicy",
|
|
12
|
+
"crossoriginopenerpolicy",
|
|
13
|
+
"crossoriginembedderpolicy",
|
|
14
|
+
"crossoriginresourcepolicy",
|
|
15
|
+
];
|
|
16
|
+
function fileName(filename) {
|
|
17
|
+
const segments = basename(filename).split(".").slice(0, -1);
|
|
18
|
+
if (segments.at(-1) === "d") {
|
|
19
|
+
segments.pop();
|
|
20
|
+
}
|
|
21
|
+
const name = segments.at(-1);
|
|
22
|
+
if (name === undefined) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return segments.length > 1 && companionSegments.has(name.toLowerCase()) ? null : name;
|
|
26
|
+
}
|
|
27
|
+
function isGeneric(name) {
|
|
28
|
+
const normalised = name.replaceAll(/[-_]/g, "").toLowerCase();
|
|
29
|
+
return ((genericSegment.test(name) || genericSuffix.test(name)) &&
|
|
30
|
+
!standardTerms.some((term) => normalised.endsWith(term)));
|
|
31
|
+
}
|
|
32
|
+
function declaredNames(declaration) {
|
|
33
|
+
if (declaration.type === "VariableDeclaration") {
|
|
34
|
+
return declaration.declarations.flatMap((declarator) => declarator.id.type === "Identifier" ? [declarator.id.name] : []);
|
|
35
|
+
}
|
|
36
|
+
return declaration.id?.type === "Identifier" ? [declaration.id.name] : [];
|
|
37
|
+
}
|
|
38
|
+
function exportedNames(program) {
|
|
39
|
+
return program.body.flatMap((statement) => {
|
|
40
|
+
if (statement.type !== "ExportNamedDeclaration") {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
if (statement.declaration) {
|
|
44
|
+
return declaredNames(statement.declaration);
|
|
45
|
+
}
|
|
46
|
+
return statement.specifiers.map(({ exported }) => exported.type === "Identifier" ? exported.name : exported.value);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export const noGenericFileSuffix = defineRule({
|
|
50
|
+
meta: {
|
|
51
|
+
type: "suggestion",
|
|
52
|
+
docs: {
|
|
53
|
+
description: "Name files after what they contain instead of `Contract` or `Policy`. Companions follow their owner's name.",
|
|
54
|
+
},
|
|
55
|
+
schema: [],
|
|
56
|
+
messages: {
|
|
57
|
+
suffix: "Rename '{{name}}' after what it contains. `Contract` and `Policy` hide whether the file holds types, constants, validation, or configuration; prefer a name such as `quoteEligibility`.",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
create(context) {
|
|
61
|
+
const name = fileName(context.filename);
|
|
62
|
+
if (!name || !isGeneric(name)) {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
Program(node) {
|
|
67
|
+
const owned = name.toLowerCase();
|
|
68
|
+
if (!exportedNames(node).some((exported) => exported.toLowerCase() === owned)) {
|
|
69
|
+
context.report({ node, messageId: "suffix", data: { name: basename(context.filename) } });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noPassThroughFunction: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { propertyName, variableFor } from "../shared/index.js";
|
|
3
|
+
function delegatedCall(fn) {
|
|
4
|
+
let expression = fn.body;
|
|
5
|
+
if (expression?.type === "BlockStatement") {
|
|
6
|
+
const [statement, ...otherStatements] = expression.body;
|
|
7
|
+
if (statement === undefined || otherStatements.length > 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
if (statement.type === "ReturnStatement") {
|
|
11
|
+
expression = statement.argument;
|
|
12
|
+
}
|
|
13
|
+
else if (statement.type === "ExpressionStatement" && !statement.directive) {
|
|
14
|
+
expression = statement.expression;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (expression?.type === "AwaitExpression") {
|
|
21
|
+
expression = expression.argument;
|
|
22
|
+
}
|
|
23
|
+
return expression?.type === "CallExpression" && !expression.optional ? expression : null;
|
|
24
|
+
}
|
|
25
|
+
function passesParameters(fn, call) {
|
|
26
|
+
if (fn.params.length !== call.arguments.length) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return fn.params.every((parameter, index) => {
|
|
30
|
+
const argument = call.arguments[index];
|
|
31
|
+
if (parameter.type === "Identifier") {
|
|
32
|
+
return argument?.type === "Identifier" && argument.name === parameter.name;
|
|
33
|
+
}
|
|
34
|
+
return (parameter.type === "RestElement" &&
|
|
35
|
+
parameter.argument.type === "Identifier" &&
|
|
36
|
+
argument?.type === "SpreadElement" &&
|
|
37
|
+
argument.argument.type === "Identifier" &&
|
|
38
|
+
argument.argument.name === parameter.argument.name);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function isEffectEvent(sourceCode, callee) {
|
|
42
|
+
const definition = variableFor(sourceCode, callee)?.defs[0]?.node;
|
|
43
|
+
const init = definition?.type === "VariableDeclarator" ? definition.init : null;
|
|
44
|
+
const hook = init?.type === "CallExpression" ? init.callee : null;
|
|
45
|
+
if (hook?.type === "MemberExpression") {
|
|
46
|
+
return propertyName(hook) === "useEffectEvent";
|
|
47
|
+
}
|
|
48
|
+
return hook?.type === "Identifier" && hook.name === "useEffectEvent";
|
|
49
|
+
}
|
|
50
|
+
function namedFunction(fn) {
|
|
51
|
+
const parent = fn.parent;
|
|
52
|
+
if (fn.type === "FunctionDeclaration") {
|
|
53
|
+
return fn.id ? { node: fn.id, name: fn.id.name } : null;
|
|
54
|
+
}
|
|
55
|
+
if (parent.type === "VariableDeclarator" &&
|
|
56
|
+
parent.init === fn &&
|
|
57
|
+
parent.id.type === "Identifier") {
|
|
58
|
+
return { node: parent.id, name: parent.id.name };
|
|
59
|
+
}
|
|
60
|
+
if ((parent.type === "MethodDefinition" && parent.kind === "method") ||
|
|
61
|
+
(parent.type === "PropertyDefinition" && parent.value === fn)) {
|
|
62
|
+
const name = propertyName(parent);
|
|
63
|
+
return name ? { node: parent.key, name } : null;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
export const noPassThroughFunction = defineRule({
|
|
68
|
+
meta: {
|
|
69
|
+
type: "suggestion",
|
|
70
|
+
docs: {
|
|
71
|
+
description: "Report named functions that only forward their parameters to another function.",
|
|
72
|
+
},
|
|
73
|
+
schema: [],
|
|
74
|
+
messages: {
|
|
75
|
+
passThrough: "`{{name}}` only forwards its parameters to `{{callee}}`. Call `{{callee}}` directly, or give this function work that justifies it.",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
create(context) {
|
|
79
|
+
function check(fn) {
|
|
80
|
+
if (fn.generator ||
|
|
81
|
+
fn.params.length === 0 ||
|
|
82
|
+
fn.returnType?.typeAnnotation.type === "TSTypePredicate") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const named = namedFunction(fn);
|
|
86
|
+
if (named === null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const call = delegatedCall(fn);
|
|
90
|
+
if (call?.callee.type === "Identifier" &&
|
|
91
|
+
passesParameters(fn, call) &&
|
|
92
|
+
!isEffectEvent(context.sourceCode, call.callee)) {
|
|
93
|
+
context.report({
|
|
94
|
+
node: named.node,
|
|
95
|
+
messageId: "passThrough",
|
|
96
|
+
data: { name: named.name, callee: call.callee.name },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
FunctionDeclaration: check,
|
|
102
|
+
FunctionExpression: check,
|
|
103
|
+
ArrowFunctionExpression: check,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noSwallowedErrors: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { childNodes, isGlobal, propertyName, variableFor } from "../shared/index.js";
|
|
3
|
+
const nestedScopes = new Set([
|
|
4
|
+
"ArrowFunctionExpression",
|
|
5
|
+
"FunctionExpression",
|
|
6
|
+
"FunctionDeclaration",
|
|
7
|
+
"ClassBody",
|
|
8
|
+
]);
|
|
9
|
+
const parserFunctions = ["decodeURIComponent", "decodeURI", "atob", "BigInt", "structuredClone"];
|
|
10
|
+
const parserConstructors = ["URL", "RegExp"];
|
|
11
|
+
const fileSystemModules = new Set(["node:fs", "fs", "node:fs/promises", "fs/promises"]);
|
|
12
|
+
function unwrap(node) {
|
|
13
|
+
if (node?.type === "TSAsExpression" ||
|
|
14
|
+
node?.type === "TSSatisfiesExpression" ||
|
|
15
|
+
node?.type === "TSNonNullExpression") {
|
|
16
|
+
return unwrap(node.expression);
|
|
17
|
+
}
|
|
18
|
+
return node;
|
|
19
|
+
}
|
|
20
|
+
function isFallback(sourceCode, node) {
|
|
21
|
+
const value = unwrap(node);
|
|
22
|
+
if (value === null) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (value.type === "Literal") {
|
|
26
|
+
return (!("regex" in value) && !("bigint" in value) && [null, false, "", 0].includes(value.value));
|
|
27
|
+
}
|
|
28
|
+
if (value.type === "Identifier") {
|
|
29
|
+
return isGlobal(sourceCode, value, "undefined");
|
|
30
|
+
}
|
|
31
|
+
if (value.type === "UnaryExpression") {
|
|
32
|
+
return value.operator === "void" && value.argument.type === "Literal";
|
|
33
|
+
}
|
|
34
|
+
if (value.type === "ArrayExpression") {
|
|
35
|
+
return value.elements.length === 0;
|
|
36
|
+
}
|
|
37
|
+
if (value.type === "ObjectExpression") {
|
|
38
|
+
return value.properties.length === 0;
|
|
39
|
+
}
|
|
40
|
+
return (value.type === "TemplateLiteral" &&
|
|
41
|
+
value.expressions.length === 0 &&
|
|
42
|
+
value.quasis[0]?.value.cooked === "");
|
|
43
|
+
}
|
|
44
|
+
function returnsFallback(sourceCode, statements) {
|
|
45
|
+
const [statement] = statements;
|
|
46
|
+
return (statements.length === 1 &&
|
|
47
|
+
statement?.type === "ReturnStatement" &&
|
|
48
|
+
isFallback(sourceCode, statement.argument));
|
|
49
|
+
}
|
|
50
|
+
function descendants(sourceCode, root) {
|
|
51
|
+
const found = [];
|
|
52
|
+
const pending = [root];
|
|
53
|
+
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
|
|
54
|
+
found.push(node);
|
|
55
|
+
if (node === root || !nestedScopes.has(node.type)) {
|
|
56
|
+
pending.push(...childNodes(node, sourceCode.visitorKeys));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
function isFileSystemModule(sourceCode, identifier) {
|
|
62
|
+
if (identifier.type !== "Identifier") {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
const definition = variableFor(sourceCode, identifier)?.defs[0];
|
|
66
|
+
return (definition?.type === "ImportBinding" &&
|
|
67
|
+
definition.parent?.type === "ImportDeclaration" &&
|
|
68
|
+
fileSystemModules.has(definition.parent.source.value));
|
|
69
|
+
}
|
|
70
|
+
function isFileSystemCall(sourceCode, callee) {
|
|
71
|
+
if (callee.type === "Identifier") {
|
|
72
|
+
return isFileSystemModule(sourceCode, callee);
|
|
73
|
+
}
|
|
74
|
+
if (callee.type !== "MemberExpression") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const object = callee.object;
|
|
78
|
+
return (isFileSystemModule(sourceCode, object) ||
|
|
79
|
+
(object.type === "MemberExpression" &&
|
|
80
|
+
propertyName(object) === "promises" &&
|
|
81
|
+
isFileSystemModule(sourceCode, object.object)));
|
|
82
|
+
}
|
|
83
|
+
function isParser(sourceCode, node) {
|
|
84
|
+
const callee = node.callee;
|
|
85
|
+
if (node.type === "NewExpression") {
|
|
86
|
+
return (parserConstructors.some((name) => isGlobal(sourceCode, callee, name)) ||
|
|
87
|
+
(callee.type === "MemberExpression" && isGlobal(sourceCode, callee.object, "Intl")));
|
|
88
|
+
}
|
|
89
|
+
if (parserFunctions.some((name) => isGlobal(sourceCode, callee, name))) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (callee.type !== "MemberExpression") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const name = propertyName(callee);
|
|
96
|
+
return ((name === "parse" && isGlobal(sourceCode, callee.object, "JSON")) ||
|
|
97
|
+
(name === "json" && node.arguments.length === 0));
|
|
98
|
+
}
|
|
99
|
+
function isProbe(sourceCode, node) {
|
|
100
|
+
return ((node.type === "CallExpression" || node.type === "NewExpression") &&
|
|
101
|
+
(isParser(sourceCode, node) || isFileSystemCall(sourceCode, node.callee)));
|
|
102
|
+
}
|
|
103
|
+
function containsProbe(sourceCode, node) {
|
|
104
|
+
return descendants(sourceCode, node).some((descendant) => isProbe(sourceCode, descendant));
|
|
105
|
+
}
|
|
106
|
+
function onlyProbes(sourceCode, block) {
|
|
107
|
+
const nodes = descendants(sourceCode, block);
|
|
108
|
+
return (nodes.some((node) => isProbe(sourceCode, node)) &&
|
|
109
|
+
nodes.every((node) => (node.type !== "AwaitExpression" && node.type !== "YieldExpression") ||
|
|
110
|
+
(node.argument !== null && containsProbe(sourceCode, node.argument))));
|
|
111
|
+
}
|
|
112
|
+
function isExistingPromise(node) {
|
|
113
|
+
const value = node.type === "ChainExpression" ? node.expression : node;
|
|
114
|
+
if (value.type === "CallExpression" &&
|
|
115
|
+
value.callee.type === "MemberExpression" &&
|
|
116
|
+
propertyName(value.callee) === "finally") {
|
|
117
|
+
return isExistingPromise(value.callee.object);
|
|
118
|
+
}
|
|
119
|
+
return value.type === "Identifier" || value.type === "MemberExpression";
|
|
120
|
+
}
|
|
121
|
+
function swallowingCallback(sourceCode, callback) {
|
|
122
|
+
if (callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
const body = callback.body;
|
|
126
|
+
if (body === null || body.type !== "BlockStatement") {
|
|
127
|
+
return isFallback(sourceCode, body);
|
|
128
|
+
}
|
|
129
|
+
return body.body.length === 0 || returnsFallback(sourceCode, body.body);
|
|
130
|
+
}
|
|
131
|
+
export const noSwallowedErrors = defineRule({
|
|
132
|
+
meta: {
|
|
133
|
+
type: "suggestion",
|
|
134
|
+
docs: { description: "Disallow catch handlers that discard the error and return a fallback." },
|
|
135
|
+
schema: [],
|
|
136
|
+
messages: {
|
|
137
|
+
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.",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
create(context) {
|
|
141
|
+
const sourceCode = context.sourceCode;
|
|
142
|
+
return {
|
|
143
|
+
TryStatement(node) {
|
|
144
|
+
const handler = node.handler;
|
|
145
|
+
if (handler !== null &&
|
|
146
|
+
returnsFallback(sourceCode, handler.body.body) &&
|
|
147
|
+
!onlyProbes(sourceCode, node.block)) {
|
|
148
|
+
context.report({ node: handler, messageId: "swallowed" });
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
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;
|
|
158
|
+
}
|
|
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" });
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noTestTimeoutOverrides: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, propertyName, apiReference, testModules } from "../shared/index.js";
|
|
3
|
+
const optionKeys = new Set(["timeout", "retry", "retries"]);
|
|
4
|
+
const configKeys = new Set(["testTimeout", "hookTimeout", "retry"]);
|
|
5
|
+
const hooks = new Set(["beforeEach", "afterEach", "beforeAll", "afterAll"]);
|
|
6
|
+
const functionTypes = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
|
7
|
+
function testApi(sourceCode, callee) {
|
|
8
|
+
const api = apiReference(sourceCode, callee);
|
|
9
|
+
if (api && (testModules.has(api.module) || api.module === "global")) {
|
|
10
|
+
return api.name;
|
|
11
|
+
}
|
|
12
|
+
if (isGlobal(sourceCode, callee, "describe")) {
|
|
13
|
+
return "describe";
|
|
14
|
+
}
|
|
15
|
+
const base = callee.type === "MemberExpression" ? callee.object : null;
|
|
16
|
+
if (base &&
|
|
17
|
+
propertyName(callee) === "describe" &&
|
|
18
|
+
apiReference(sourceCode, base)?.name === "test") {
|
|
19
|
+
return "describe";
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
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
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
function timeoutArgument(node) {
|
|
31
|
+
return (node !== undefined &&
|
|
32
|
+
node.type !== "ObjectExpression" &&
|
|
33
|
+
node.type !== "SpreadElement" &&
|
|
34
|
+
!functionTypes.has(node.type));
|
|
35
|
+
}
|
|
36
|
+
function registrationOverride(sourceCode, node) {
|
|
37
|
+
const name = testApi(sourceCode, node.callee);
|
|
38
|
+
if (!name) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (hooks.has(name)) {
|
|
42
|
+
return timeoutArgument(node.arguments[1]) || hasOverrideKey(node.arguments[1], optionKeys);
|
|
43
|
+
}
|
|
44
|
+
if (!["test", "it", "describe"].includes(name)) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return (node.arguments.some((argument) => hasOverrideKey(argument, optionKeys)) ||
|
|
48
|
+
timeoutArgument(node.arguments[2]));
|
|
49
|
+
}
|
|
50
|
+
function runnerObject(sourceCode, node, name, modules) {
|
|
51
|
+
if (isGlobal(sourceCode, node, name)) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const api = apiReference(sourceCode, node);
|
|
55
|
+
return api?.name === name && modules.includes(api.module);
|
|
56
|
+
}
|
|
57
|
+
function importedTest(sourceCode, node) {
|
|
58
|
+
const api = apiReference(sourceCode, node);
|
|
59
|
+
return api?.name === "test" && api.module !== "global";
|
|
60
|
+
}
|
|
61
|
+
function runnerOverride(sourceCode, node) {
|
|
62
|
+
const callee = node.callee;
|
|
63
|
+
if (callee.type !== "MemberExpression") {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const method = propertyName(callee);
|
|
67
|
+
if (method === "setConfig") {
|
|
68
|
+
return (runnerObject(sourceCode, callee.object, "vi", ["vitest", "@effect/vitest"]) &&
|
|
69
|
+
hasOverrideKey(node.arguments[0], configKeys));
|
|
70
|
+
}
|
|
71
|
+
if (method === "setTimeout") {
|
|
72
|
+
return (runnerObject(sourceCode, callee.object, "jest", ["@jest/globals"]) ||
|
|
73
|
+
importedTest(sourceCode, callee.object));
|
|
74
|
+
}
|
|
75
|
+
if (method === "slow") {
|
|
76
|
+
return importedTest(sourceCode, callee.object);
|
|
77
|
+
}
|
|
78
|
+
return (method === "configure" &&
|
|
79
|
+
callee.object.type === "MemberExpression" &&
|
|
80
|
+
propertyName(callee.object) === "describe" &&
|
|
81
|
+
importedTest(sourceCode, callee.object.object) &&
|
|
82
|
+
hasOverrideKey(node.arguments[0], optionKeys));
|
|
83
|
+
}
|
|
84
|
+
export const noTestTimeoutOverrides = defineRule({
|
|
85
|
+
meta: {
|
|
86
|
+
type: "suggestion",
|
|
87
|
+
docs: {
|
|
88
|
+
description: "Make slow or flaky tests faster and deterministic instead of raising their timeout or retries.",
|
|
89
|
+
},
|
|
90
|
+
schema: [],
|
|
91
|
+
messages: {
|
|
92
|
+
override: "This test raises its timeout or retries to tolerate slow or flaky work. Make the test faster or deterministic instead.",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
create(context) {
|
|
96
|
+
const sourceCode = context.sourceCode;
|
|
97
|
+
return {
|
|
98
|
+
CallExpression(node) {
|
|
99
|
+
if (registrationOverride(sourceCode, node) || runnerOverride(sourceCode, node)) {
|
|
100
|
+
context.report({ node, messageId: "override" });
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noVacuousTestAssertions: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { variableFor, propertyName, apiReference } from "../shared/index.js";
|
|
3
|
+
const equalityMatchers = new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
4
|
+
function isSingleArgumentList(values) {
|
|
5
|
+
return values.length === 1;
|
|
6
|
+
}
|
|
7
|
+
function primitiveLiteral(node) {
|
|
8
|
+
if (node.type === "Literal") {
|
|
9
|
+
return (!("regex" in node) &&
|
|
10
|
+
(node.value === null || ["string", "number", "boolean", "bigint"].includes(typeof node.value)));
|
|
11
|
+
}
|
|
12
|
+
return (node.type === "UnaryExpression" &&
|
|
13
|
+
["+", "-"].includes(node.operator) &&
|
|
14
|
+
node.argument.type === "Literal" &&
|
|
15
|
+
["number", "bigint"].includes(typeof node.argument.value));
|
|
16
|
+
}
|
|
17
|
+
export const noVacuousTestAssertions = defineRule({
|
|
18
|
+
meta: {
|
|
19
|
+
type: "problem",
|
|
20
|
+
docs: {
|
|
21
|
+
description: "Assert runtime behaviour instead of literals or a value compared with itself.",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
vacuous: "This assertion compares fixed values or a value with itself. Assert an independently expected runtime result.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
create(context) {
|
|
29
|
+
const sourceCode = context.sourceCode;
|
|
30
|
+
return {
|
|
31
|
+
CallExpression(node) {
|
|
32
|
+
if (node.callee.type !== "MemberExpression" || !isSingleArgumentList(node.arguments)) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const matcher = propertyName(node.callee);
|
|
36
|
+
if (matcher === null || !equalityMatchers.has(matcher)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let actualCall = node.callee.object;
|
|
40
|
+
if (actualCall.type === "MemberExpression" && propertyName(actualCall) === "not") {
|
|
41
|
+
actualCall = actualCall.object;
|
|
42
|
+
}
|
|
43
|
+
if (actualCall.type !== "CallExpression" || !isSingleArgumentList(actualCall.arguments)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const api = apiReference(sourceCode, actualCall.callee);
|
|
47
|
+
if (api?.name !== "expect" ||
|
|
48
|
+
![
|
|
49
|
+
"vitest",
|
|
50
|
+
"@effect/vitest",
|
|
51
|
+
"@jest/globals",
|
|
52
|
+
"@playwright/test",
|
|
53
|
+
"playwright/test",
|
|
54
|
+
"global",
|
|
55
|
+
].includes(api.module)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const actual = actualCall.arguments[0];
|
|
59
|
+
const expected = node.arguments[0];
|
|
60
|
+
const literals = primitiveLiteral(actual) && primitiveLiteral(expected);
|
|
61
|
+
const sameBinding = actual.type === "Identifier" &&
|
|
62
|
+
expected.type === "Identifier" &&
|
|
63
|
+
actual.name === expected.name &&
|
|
64
|
+
variableFor(sourceCode, actual) === variableFor(sourceCode, expected);
|
|
65
|
+
if (literals || sameBinding) {
|
|
66
|
+
context.report({ node, messageId: "vacuous" });
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const serverActionGuard: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { optionStrings } from "../shared/rule-options.js";
|
|
3
|
+
function hasDirective(statements, directive) {
|
|
4
|
+
return statements.some((statement) => statement.type === "ExpressionStatement" && statement.directive === directive);
|
|
5
|
+
}
|
|
6
|
+
function firstStatement(statements) {
|
|
7
|
+
return statements.find((statement) => statement.type !== "ExpressionStatement" || !statement.directive);
|
|
8
|
+
}
|
|
9
|
+
function awaitsGuard(expression, guards) {
|
|
10
|
+
return (expression?.type === "AwaitExpression" &&
|
|
11
|
+
expression.argument.type === "CallExpression" &&
|
|
12
|
+
expression.argument.callee.type === "Identifier" &&
|
|
13
|
+
guards.has(expression.argument.callee.name));
|
|
14
|
+
}
|
|
15
|
+
function startsWithGuard(action, guards) {
|
|
16
|
+
if (action.body?.type !== "BlockStatement") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const statement = firstStatement(action.body.body);
|
|
20
|
+
if (statement?.type === "ExpressionStatement") {
|
|
21
|
+
return awaitsGuard(statement.expression, guards);
|
|
22
|
+
}
|
|
23
|
+
return (statement?.type === "VariableDeclaration" &&
|
|
24
|
+
awaitsGuard(statement.declarations[0]?.init, guards));
|
|
25
|
+
}
|
|
26
|
+
function functionName(node) {
|
|
27
|
+
if (node.id?.type === "Identifier") {
|
|
28
|
+
return node.id.name;
|
|
29
|
+
}
|
|
30
|
+
return node.parent.type === "VariableDeclarator" && node.parent.id.type === "Identifier"
|
|
31
|
+
? node.parent.id.name
|
|
32
|
+
: "this action";
|
|
33
|
+
}
|
|
34
|
+
export const serverActionGuard = defineRule({
|
|
35
|
+
meta: {
|
|
36
|
+
type: "problem",
|
|
37
|
+
docs: { description: "Require every server action to start by awaiting an access guard." },
|
|
38
|
+
schema: [
|
|
39
|
+
{
|
|
40
|
+
type: "object",
|
|
41
|
+
properties: { guards: { type: "array", items: { type: "string" }, minItems: 1 } },
|
|
42
|
+
required: ["guards"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
messages: {
|
|
47
|
+
missingGuard: "Server actions can be called directly. Start `{{name}}` by awaiting an access guard: {{guards}}.",
|
|
48
|
+
exportWhereDeclared: "Export each server action where it's declared, so this rule can check it starts with an access guard.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
create(context) {
|
|
52
|
+
const guards = new Set(optionStrings(context.options, "guards"));
|
|
53
|
+
const guardList = [...guards].join(", ");
|
|
54
|
+
let isServerModule = false;
|
|
55
|
+
function requireGuard(action) {
|
|
56
|
+
if (!startsWithGuard(action, guards)) {
|
|
57
|
+
context.report({
|
|
58
|
+
node: action,
|
|
59
|
+
messageId: "missingGuard",
|
|
60
|
+
data: { name: functionName(action), guards: guardList },
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function checkInline(node) {
|
|
65
|
+
if (!isServerModule &&
|
|
66
|
+
node.body?.type === "BlockStatement" &&
|
|
67
|
+
hasDirective(node.body.body, "use server")) {
|
|
68
|
+
requireGuard(node);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
Program(program) {
|
|
73
|
+
isServerModule = hasDirective(program.body, "use server");
|
|
74
|
+
},
|
|
75
|
+
ExportDefaultDeclaration(node) {
|
|
76
|
+
if (isServerModule) {
|
|
77
|
+
context.report({ node, messageId: "exportWhereDeclared" });
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
ExportNamedDeclaration(node) {
|
|
81
|
+
if (!isServerModule || node.exportKind === "type") {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const declaration = node.declaration;
|
|
85
|
+
if (declaration === null) {
|
|
86
|
+
context.report({ node, messageId: "exportWhereDeclared" });
|
|
87
|
+
}
|
|
88
|
+
else if (declaration.type === "FunctionDeclaration") {
|
|
89
|
+
requireGuard(declaration);
|
|
90
|
+
}
|
|
91
|
+
else if (declaration.type === "VariableDeclaration") {
|
|
92
|
+
for (const declarator of declaration.declarations) {
|
|
93
|
+
const action = declarator.init;
|
|
94
|
+
if (action?.type === "ArrowFunctionExpression" ||
|
|
95
|
+
action?.type === "FunctionExpression") {
|
|
96
|
+
requireGuard(action);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
context.report({ node: declarator, messageId: "exportWhereDeclared" });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
FunctionDeclaration: checkInline,
|
|
105
|
+
FunctionExpression: checkInline,
|
|
106
|
+
ArrowFunctionExpression: checkInline,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const statementSpacing: import("@oxlint/plugins").Rule;
|