@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,71 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobalValue, isInjectionSeam, propertyName, variableFor } from "../shared/index.js";
|
|
3
|
+
const cryptoModules = ["node:crypto", "crypto"];
|
|
4
|
+
const webRandom = new Set(["randomUUID"]);
|
|
5
|
+
const nodeRandom = new Set(["randomUUID", "randomInt"]);
|
|
6
|
+
function importedName(specifier) {
|
|
7
|
+
return specifier.imported.type === "Identifier" ? specifier.imported.name : null;
|
|
8
|
+
}
|
|
9
|
+
function cryptoImport(sourceCode, node) {
|
|
10
|
+
if (node.type !== "Identifier") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
14
|
+
if (definition?.type !== "ImportBinding" ||
|
|
15
|
+
definition.parent?.type !== "ImportDeclaration" ||
|
|
16
|
+
!cryptoModules.includes(definition.parent.source.value)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const specifier = definition.node;
|
|
20
|
+
if (specifier.type === "ImportSpecifier") {
|
|
21
|
+
return importedName(specifier) === "webcrypto" ? webRandom : null;
|
|
22
|
+
}
|
|
23
|
+
return nodeRandom;
|
|
24
|
+
}
|
|
25
|
+
function randomMembers(sourceCode, node) {
|
|
26
|
+
return isGlobalValue(sourceCode, node, "crypto") ? webRandom : cryptoImport(sourceCode, node);
|
|
27
|
+
}
|
|
28
|
+
export const noAmbientRandom = defineRule({
|
|
29
|
+
meta: {
|
|
30
|
+
type: "suggestion",
|
|
31
|
+
docs: { description: "Generate random numbers and identifiers only in a random owner." },
|
|
32
|
+
schema: [],
|
|
33
|
+
messages: {
|
|
34
|
+
random: "Accept random values or an identifier factory from the caller. If this module owns randomness, add it to the `random` owners.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
const sourceCode = context.sourceCode;
|
|
39
|
+
return {
|
|
40
|
+
MemberExpression(node) {
|
|
41
|
+
const name = propertyName(node);
|
|
42
|
+
if (((name === "random" && isGlobalValue(sourceCode, node.object, "Math")) ||
|
|
43
|
+
(name !== null && randomMembers(sourceCode, node.object)?.has(name))) &&
|
|
44
|
+
!isInjectionSeam(node)) {
|
|
45
|
+
context.report({ node, messageId: "random" });
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
ImportDeclaration(node) {
|
|
49
|
+
if (!cryptoModules.includes(node.source.value) || node.importKind === "type") {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const specifier of node.specifiers) {
|
|
53
|
+
if (specifier.type !== "ImportSpecifier" || specifier.importKind === "type") {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const name = importedName(specifier);
|
|
57
|
+
if (name === null || !nodeRandom.has(name)) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const variable of sourceCode.getDeclaredVariables(specifier)) {
|
|
61
|
+
for (const { identifier } of variable.references) {
|
|
62
|
+
if (!isInjectionSeam(identifier)) {
|
|
63
|
+
context.report({ node: identifier, messageId: "random" });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noComments: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { optionStrings } from "../shared/rule-options.js";
|
|
3
|
+
const directivePatterns = [
|
|
4
|
+
/^(?:oxlint|eslint)-(?:disable|enable)(?:-next-line|-line)?(?:\s|$)/u,
|
|
5
|
+
/^@ts-(?:expect-error|ignore|nocheck|check)(?:\s|$)/u,
|
|
6
|
+
/^\/\s*<(?:reference|amd-module|amd-dependency)\s/u,
|
|
7
|
+
/^[#@]__(?:PURE|NO_SIDE_EFFECTS)__$/u,
|
|
8
|
+
/^(?:oxfmt|prettier|biome)-ignore(?:-start|-end|-all)?(?:\s|$)/u,
|
|
9
|
+
/^(?:c8|v8|istanbul)\s+ignore(?:\s|$)/u,
|
|
10
|
+
/^@vite-ignore$/u,
|
|
11
|
+
/^webpack[A-Z]\w*:/u,
|
|
12
|
+
/^@(?:vitest|jest)-environment\s/u,
|
|
13
|
+
/^@jsx(?:ImportSource|Runtime|Frag)?\s/u,
|
|
14
|
+
];
|
|
15
|
+
function commentText(comment) {
|
|
16
|
+
return comment.value
|
|
17
|
+
.split("\n")
|
|
18
|
+
.map((line) => line.replace(/^\s*\*?/u, "").trim())
|
|
19
|
+
.filter((line) => line.length > 0)
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
|
22
|
+
const javaScriptFile = /\.(?:js|jsx|mjs|cjs)$/u;
|
|
23
|
+
const typeTag = /@(?:(?:type|typedef|callback|template|satisfies|import|overload|this|enum|extends|augments|implements)\b|(?:param|arg|argument|returns?|property|prop)\s+\{)/u;
|
|
24
|
+
function isTypeAnnotation(comment) {
|
|
25
|
+
return comment.type === "Block" && comment.value.startsWith("*") && typeTag.test(comment.value);
|
|
26
|
+
}
|
|
27
|
+
export const noComments = defineRule({
|
|
28
|
+
meta: {
|
|
29
|
+
type: "suggestion",
|
|
30
|
+
docs: {
|
|
31
|
+
description: "Express intent in code instead of comments; allow tool directives and JavaScript type annotations.",
|
|
32
|
+
},
|
|
33
|
+
schema: [
|
|
34
|
+
{
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { allow: { type: "array", items: { type: "string" } } },
|
|
37
|
+
additionalProperties: false,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: {
|
|
41
|
+
comment: "Remove this comment. Carry intent in a name, type, test or validating function, and put a reason the code cannot show in the commit message.",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
create(context) {
|
|
45
|
+
const allowed = [
|
|
46
|
+
...directivePatterns,
|
|
47
|
+
...optionStrings(context.options, "allow").map((source) => new RegExp(source, "u")),
|
|
48
|
+
];
|
|
49
|
+
const typedDocs = javaScriptFile.test(context.filename);
|
|
50
|
+
return {
|
|
51
|
+
Program() {
|
|
52
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
53
|
+
if (comment.type === "Shebang" || (typedDocs && isTypeAnnotation(comment))) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const text = commentText(comment);
|
|
57
|
+
if (!allowed.some((pattern) => pattern.test(text))) {
|
|
58
|
+
context.report({ loc: comment.loc, messageId: "comment" });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noDeepTernary: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
export const noDeepTernary = defineRule({
|
|
3
|
+
meta: {
|
|
4
|
+
type: "suggestion",
|
|
5
|
+
docs: { description: "Allow one level of nested ternaries without deeper conditional chains." },
|
|
6
|
+
schema: [],
|
|
7
|
+
messages: {
|
|
8
|
+
depth: "This ternary is nested more than one level. Use named branches, a lookup or a switch.",
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
create(context) {
|
|
12
|
+
return {
|
|
13
|
+
ConditionalExpression(node) {
|
|
14
|
+
let depth = 1;
|
|
15
|
+
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
16
|
+
if (["ArrowFunctionExpression", "FunctionExpression", "FunctionDeclaration"].includes(ancestor.type)) {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
if (ancestor.type === "ConditionalExpression") {
|
|
20
|
+
depth += 1;
|
|
21
|
+
if (depth > 3) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (depth === 3) {
|
|
27
|
+
context.report({ node, messageId: "depth" });
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noEditProofAssertions: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { variableFor, propertyName, apiReference } from "../shared/index.js";
|
|
3
|
+
import { isStringLiteral } from "../vendor/anti-slop/effect/shared/tagged-values.js";
|
|
4
|
+
const expectModules = new Set([
|
|
5
|
+
"vitest",
|
|
6
|
+
"@effect/vitest",
|
|
7
|
+
"@jest/globals",
|
|
8
|
+
"@playwright/test",
|
|
9
|
+
"playwright/test",
|
|
10
|
+
"global",
|
|
11
|
+
]);
|
|
12
|
+
const presenceMatchers = new Map([
|
|
13
|
+
["toBeDefined", false],
|
|
14
|
+
["toBeTruthy", false],
|
|
15
|
+
["toBeUndefined", true],
|
|
16
|
+
["toBeFalsy", true],
|
|
17
|
+
["toBeNull", true],
|
|
18
|
+
]);
|
|
19
|
+
const equalityMatchers = new Set(["toBe", "toEqual", "toStrictEqual"]);
|
|
20
|
+
function expectation(sourceCode, node) {
|
|
21
|
+
if (node.callee.type !== "MemberExpression") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const matcher = propertyName(node.callee);
|
|
25
|
+
let target = node.callee.object;
|
|
26
|
+
let negated = false;
|
|
27
|
+
if (target.type === "MemberExpression" && propertyName(target) === "not") {
|
|
28
|
+
negated = true;
|
|
29
|
+
target = target.object;
|
|
30
|
+
}
|
|
31
|
+
if (!matcher || target.type !== "CallExpression") {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const [actual] = target.arguments;
|
|
35
|
+
if (actual === undefined) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const callee = target.callee.type === "MemberExpression" && propertyName(target.callee) === "soft"
|
|
39
|
+
? target.callee.object
|
|
40
|
+
: target.callee;
|
|
41
|
+
const api = apiReference(sourceCode, callee);
|
|
42
|
+
if (api?.name !== "expect" || !expectModules.has(api.module)) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return { matcher, negated, actual, expected: node.arguments };
|
|
46
|
+
}
|
|
47
|
+
function dynamicImport(node) {
|
|
48
|
+
return node?.type === "AwaitExpression" && node.argument.type === "ImportExpression";
|
|
49
|
+
}
|
|
50
|
+
function importKind(sourceCode, node) {
|
|
51
|
+
if (node.type !== "Identifier") {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const variable = variableFor(sourceCode, node);
|
|
55
|
+
const definition = variable?.defs.length === 1 ? variable.defs[0] : undefined;
|
|
56
|
+
if (definition === undefined) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (definition.type === "ImportBinding") {
|
|
60
|
+
return definition.node.type === "ImportNamespaceSpecifier" ? "namespace" : "binding";
|
|
61
|
+
}
|
|
62
|
+
if (definition.type === "Variable" &&
|
|
63
|
+
definition.parent?.type === "VariableDeclaration" &&
|
|
64
|
+
definition.parent.kind === "const" &&
|
|
65
|
+
definition.node.type === "VariableDeclarator" &&
|
|
66
|
+
dynamicImport(definition.node.init)) {
|
|
67
|
+
return definition.node.id.type === "Identifier" ? "namespace" : "binding";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function importedValue(sourceCode, node) {
|
|
72
|
+
if (node.type === "Identifier") {
|
|
73
|
+
return importKind(sourceCode, node) !== null;
|
|
74
|
+
}
|
|
75
|
+
return (node.type === "MemberExpression" &&
|
|
76
|
+
propertyName(node) !== null &&
|
|
77
|
+
importKind(sourceCode, node.object) === "namespace");
|
|
78
|
+
}
|
|
79
|
+
function editProof(sourceCode, { matcher, negated, actual, expected }) {
|
|
80
|
+
if (presenceMatchers.has(matcher)) {
|
|
81
|
+
return presenceMatchers.get(matcher) === negated && importedValue(sourceCode, actual);
|
|
82
|
+
}
|
|
83
|
+
const [first] = expected;
|
|
84
|
+
if (negated || !isStringLiteral(first)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
const functionType = first.value === "function";
|
|
88
|
+
if (matcher === "toBeTypeOf") {
|
|
89
|
+
return functionType || importedValue(sourceCode, actual);
|
|
90
|
+
}
|
|
91
|
+
if (equalityMatchers.has(matcher)) {
|
|
92
|
+
return (actual.type === "UnaryExpression" &&
|
|
93
|
+
actual.operator === "typeof" &&
|
|
94
|
+
(functionType || importedValue(sourceCode, actual.argument)));
|
|
95
|
+
}
|
|
96
|
+
return matcher === "toHaveProperty" && importKind(sourceCode, actual) === "namespace";
|
|
97
|
+
}
|
|
98
|
+
export const noEditProofAssertions = defineRule({
|
|
99
|
+
meta: {
|
|
100
|
+
type: "problem",
|
|
101
|
+
docs: {
|
|
102
|
+
description: "Assert behaviour instead of proving that an import exists or that a value is a function.",
|
|
103
|
+
},
|
|
104
|
+
schema: [],
|
|
105
|
+
messages: {
|
|
106
|
+
editProof: "This assertion only proves that an import exists or that a value is a function. Call it and assert the behaviour it provides.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
create(context) {
|
|
110
|
+
const sourceCode = context.sourceCode;
|
|
111
|
+
return {
|
|
112
|
+
CallExpression(node) {
|
|
113
|
+
const assertion = expectation(sourceCode, node);
|
|
114
|
+
if (assertion && editProof(sourceCode, assertion)) {
|
|
115
|
+
context.report({ node, messageId: "editProof" });
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noEnvAccess: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, propertyName } from "../shared/index.js";
|
|
3
|
+
function isImportMeta(node) {
|
|
4
|
+
return (node?.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta");
|
|
5
|
+
}
|
|
6
|
+
function isEnvironmentSource(sourceCode, node) {
|
|
7
|
+
return isGlobal(sourceCode, node, "process") || isImportMeta(node);
|
|
8
|
+
}
|
|
9
|
+
export const noEnvAccess = defineRule({
|
|
10
|
+
meta: {
|
|
11
|
+
type: "suggestion",
|
|
12
|
+
docs: { description: "Read environment variables only in the configuration owner." },
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: {
|
|
15
|
+
env: "Read environment variables in the configuration owner and pass parsed values in. If this file parses configuration, add it to the `env` owners.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
create(context) {
|
|
19
|
+
const sourceCode = context.sourceCode;
|
|
20
|
+
return {
|
|
21
|
+
MemberExpression(node) {
|
|
22
|
+
if (propertyName(node) === "env" && isEnvironmentSource(sourceCode, node.object)) {
|
|
23
|
+
context.report({ node, messageId: "env" });
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
VariableDeclarator(node) {
|
|
27
|
+
if (node.id.type === "ObjectPattern" &&
|
|
28
|
+
isEnvironmentSource(sourceCode, node.init) &&
|
|
29
|
+
node.id.properties.some((property) => property.type === "Property" && propertyName(property) === "env")) {
|
|
30
|
+
context.report({ node, messageId: "env" });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noFixedTestDelay: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { variableFor, isGlobal, propertyName, isPlaywright, apiReference, testContext, } from "../shared/index.js";
|
|
3
|
+
function numericLiteral(node) {
|
|
4
|
+
return ((node?.type === "Literal" && typeof node.value === "number") ||
|
|
5
|
+
(node?.type === "UnaryExpression" &&
|
|
6
|
+
["+", "-"].includes(node.operator) &&
|
|
7
|
+
node.argument.type === "Literal" &&
|
|
8
|
+
typeof node.argument.value === "number"));
|
|
9
|
+
}
|
|
10
|
+
function globalTimer(sourceCode, callee) {
|
|
11
|
+
if (isGlobal(sourceCode, callee, "setTimeout")) {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return (callee.type === "MemberExpression" &&
|
|
15
|
+
propertyName(callee) === "setTimeout" &&
|
|
16
|
+
["globalThis", "global", "window"].some((name) => isGlobal(sourceCode, callee.object, name)));
|
|
17
|
+
}
|
|
18
|
+
function singleExpression(body) {
|
|
19
|
+
if (body === null) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
if (body.type !== "BlockStatement") {
|
|
23
|
+
return body;
|
|
24
|
+
}
|
|
25
|
+
if (body.body.length !== 1) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const statement = body.body[0];
|
|
29
|
+
if (statement?.type === "ExpressionStatement") {
|
|
30
|
+
return statement.expression;
|
|
31
|
+
}
|
|
32
|
+
return statement?.type === "ReturnStatement" ? statement.argument : null;
|
|
33
|
+
}
|
|
34
|
+
function resolvesWithoutValue(sourceCode, callback, resolve) {
|
|
35
|
+
if (callback?.type === "Identifier") {
|
|
36
|
+
return variableFor(sourceCode, callback) === variableFor(sourceCode, resolve);
|
|
37
|
+
}
|
|
38
|
+
if (!callback ||
|
|
39
|
+
(callback.type !== "ArrowFunctionExpression" && callback.type !== "FunctionExpression") ||
|
|
40
|
+
callback.params.length !== 0 ||
|
|
41
|
+
callback.generator) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const call = singleExpression(callback.body);
|
|
45
|
+
return (call?.type === "CallExpression" &&
|
|
46
|
+
call.arguments.length === 0 &&
|
|
47
|
+
call.callee.type === "Identifier" &&
|
|
48
|
+
variableFor(sourceCode, call.callee) === variableFor(sourceCode, resolve));
|
|
49
|
+
}
|
|
50
|
+
function timerPromise(sourceCode, node) {
|
|
51
|
+
if (!isGlobal(sourceCode, node.callee, "Promise")) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const executor = node.arguments[0];
|
|
55
|
+
if (!executor ||
|
|
56
|
+
(executor.type !== "ArrowFunctionExpression" && executor.type !== "FunctionExpression") ||
|
|
57
|
+
executor.generator) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const resolve = executor.params[0];
|
|
61
|
+
if (resolve?.type !== "Identifier") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const timer = singleExpression(executor.body);
|
|
65
|
+
if (timer?.type !== "CallExpression" ||
|
|
66
|
+
!globalTimer(sourceCode, timer.callee) ||
|
|
67
|
+
!numericLiteral(timer.arguments[1])) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return resolvesWithoutValue(sourceCode, timer.arguments[0], resolve);
|
|
71
|
+
}
|
|
72
|
+
function playwrightPage(sourceCode, node, enclosing) {
|
|
73
|
+
if (!isPlaywright(enclosing.api.module) || node.type !== "Identifier") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const fixtures = enclosing.callback.params[0];
|
|
77
|
+
if (fixtures?.type !== "ObjectPattern") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return fixtures.properties.some((property) => {
|
|
81
|
+
if (property.type !== "Property" || propertyName(property) !== "page") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const binding = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
85
|
+
return (binding.type === "Identifier" &&
|
|
86
|
+
variableFor(sourceCode, binding) === variableFor(sourceCode, node));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
export const noFixedTestDelay = defineRule({
|
|
90
|
+
meta: {
|
|
91
|
+
type: "problem",
|
|
92
|
+
docs: { description: "Wait for observable test outcomes instead of a fixed timer." },
|
|
93
|
+
schema: [],
|
|
94
|
+
messages: {
|
|
95
|
+
delay: "Wait for an observable condition or control the clock instead of waiting for a fixed delay.",
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
create(context) {
|
|
99
|
+
const sourceCode = context.sourceCode;
|
|
100
|
+
return {
|
|
101
|
+
CallExpression(node) {
|
|
102
|
+
if (node.parent.type !== "AwaitExpression") {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const enclosing = testContext(sourceCode, node);
|
|
106
|
+
if (!enclosing) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const api = apiReference(sourceCode, node.callee);
|
|
110
|
+
const importedTimer = api?.name === "setTimeout" &&
|
|
111
|
+
["node:timers/promises", "timers/promises"].includes(api.module) &&
|
|
112
|
+
numericLiteral(node.arguments[0]);
|
|
113
|
+
const browserTimer = node.callee.type === "MemberExpression" &&
|
|
114
|
+
propertyName(node.callee) === "waitForTimeout" &&
|
|
115
|
+
numericLiteral(node.arguments[0]) &&
|
|
116
|
+
playwrightPage(sourceCode, node.callee.object, enclosing);
|
|
117
|
+
if (importedTimer || browserTimer) {
|
|
118
|
+
context.report({ node, messageId: "delay" });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
NewExpression(node) {
|
|
122
|
+
if (node.parent.type === "AwaitExpression" &&
|
|
123
|
+
testContext(sourceCode, node) &&
|
|
124
|
+
timerPromise(sourceCode, node)) {
|
|
125
|
+
context.report({ node, messageId: "delay" });
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noForwardingModule: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, matchesGlob, resolve } from "node:path";
|
|
3
|
+
import { defineRule } from "@oxlint/plugins";
|
|
4
|
+
import { projectPath } from "../shared/index.js";
|
|
5
|
+
const routeFiles = new Set([
|
|
6
|
+
"page",
|
|
7
|
+
"layout",
|
|
8
|
+
"template",
|
|
9
|
+
"route",
|
|
10
|
+
"loading",
|
|
11
|
+
"error",
|
|
12
|
+
"global-error",
|
|
13
|
+
"not-found",
|
|
14
|
+
"forbidden",
|
|
15
|
+
"unauthorized",
|
|
16
|
+
"default",
|
|
17
|
+
"opengraph-image",
|
|
18
|
+
"twitter-image",
|
|
19
|
+
"icon",
|
|
20
|
+
"apple-icon",
|
|
21
|
+
"sitemap",
|
|
22
|
+
"robots",
|
|
23
|
+
"manifest",
|
|
24
|
+
]);
|
|
25
|
+
const rootFiles = new Set(["middleware", "proxy", "instrumentation", "instrumentation-client"]);
|
|
26
|
+
const manifests = new Map();
|
|
27
|
+
const entrypoints = new Map();
|
|
28
|
+
function manifestDirectory(directory) {
|
|
29
|
+
const cached = manifests.get(directory);
|
|
30
|
+
if (cached !== undefined) {
|
|
31
|
+
return cached;
|
|
32
|
+
}
|
|
33
|
+
const parent = dirname(directory);
|
|
34
|
+
const manifest = existsSync(join(directory, "package.json"))
|
|
35
|
+
? directory
|
|
36
|
+
: parent === directory
|
|
37
|
+
? null
|
|
38
|
+
: manifestDirectory(parent);
|
|
39
|
+
manifests.set(directory, manifest);
|
|
40
|
+
return manifest;
|
|
41
|
+
}
|
|
42
|
+
function targets(value) {
|
|
43
|
+
if (typeof value === "string") {
|
|
44
|
+
return [value];
|
|
45
|
+
}
|
|
46
|
+
if (Array.isArray(value)) {
|
|
47
|
+
return value.flatMap(targets);
|
|
48
|
+
}
|
|
49
|
+
if (typeof value === "object" && value !== null) {
|
|
50
|
+
return Object.values(value).flatMap(targets);
|
|
51
|
+
}
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
function manifestFields(directory) {
|
|
55
|
+
const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8"));
|
|
56
|
+
return new Map(typeof manifest === "object" && manifest !== null ? Object.entries(manifest) : []);
|
|
57
|
+
}
|
|
58
|
+
function packageEntrypoints(directory) {
|
|
59
|
+
const cached = entrypoints.get(directory);
|
|
60
|
+
if (cached !== undefined) {
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
const manifest = manifestFields(directory);
|
|
64
|
+
const fields = [
|
|
65
|
+
manifest.get("main") ?? "index.js",
|
|
66
|
+
manifest.get("module"),
|
|
67
|
+
manifest.get("source"),
|
|
68
|
+
manifest.get("browser"),
|
|
69
|
+
manifest.get("types"),
|
|
70
|
+
manifest.get("typings"),
|
|
71
|
+
];
|
|
72
|
+
const resolved = [...targets(fields), ...targets(manifest.get("exports"))].map((target) => resolve(directory, target));
|
|
73
|
+
entrypoints.set(directory, resolved);
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
function withoutExtension(path) {
|
|
77
|
+
return path.replace(/(?:\.d)?\.[cm]?[jt]sx?$/, "");
|
|
78
|
+
}
|
|
79
|
+
function isPackageEntrypoint(directory, filename) {
|
|
80
|
+
const file = withoutExtension(filename);
|
|
81
|
+
return packageEntrypoints(directory).some((target) => target.includes("*")
|
|
82
|
+
? matchesGlob(file, withoutExtension(target))
|
|
83
|
+
: withoutExtension(target) === file);
|
|
84
|
+
}
|
|
85
|
+
function isFrameworkFile(directory, filename) {
|
|
86
|
+
const path = projectPath(directory, filename).split("/").slice(0, -1);
|
|
87
|
+
const name = withoutExtension(basename(filename));
|
|
88
|
+
const directories = path[0] === "src" ? path.slice(1) : path;
|
|
89
|
+
return ((directories[0] === "app" && routeFiles.has(name)) ||
|
|
90
|
+
directories[0] === "pages" ||
|
|
91
|
+
(directories.length === 0 && rootFiles.has(name)));
|
|
92
|
+
}
|
|
93
|
+
function exportedLocalName(specifier) {
|
|
94
|
+
return specifier.local.type === "Literal" ? specifier.local.value : specifier.local.name;
|
|
95
|
+
}
|
|
96
|
+
function isForwarding(body) {
|
|
97
|
+
const imported = new Set();
|
|
98
|
+
const exported = new Set();
|
|
99
|
+
let forwards = false;
|
|
100
|
+
for (const statement of body) {
|
|
101
|
+
if (statement.type === "ExportAllDeclaration") {
|
|
102
|
+
forwards = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (statement.type === "ExportNamedDeclaration" && statement.declaration === null) {
|
|
106
|
+
if (statement.source !== null) {
|
|
107
|
+
forwards ||= statement.specifiers.length > 0;
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
for (const specifier of statement.specifiers) {
|
|
111
|
+
exported.add(exportedLocalName(specifier));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (statement.type === "ExportDefaultDeclaration" &&
|
|
117
|
+
statement.declaration.type === "Identifier") {
|
|
118
|
+
exported.add(statement.declaration.name);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (statement.type === "ImportDeclaration" && statement.specifiers.length > 0) {
|
|
122
|
+
forwards = true;
|
|
123
|
+
for (const specifier of statement.specifiers) {
|
|
124
|
+
imported.add(specifier.local.name);
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return (forwards && imported.size === exported.size && [...imported].every((name) => exported.has(name)));
|
|
131
|
+
}
|
|
132
|
+
export const noForwardingModule = defineRule({
|
|
133
|
+
meta: {
|
|
134
|
+
type: "suggestion",
|
|
135
|
+
docs: {
|
|
136
|
+
description: "Import from the authoritative module instead of a file that only re-exports.",
|
|
137
|
+
},
|
|
138
|
+
schema: [],
|
|
139
|
+
messages: {
|
|
140
|
+
forwarding: "This module only re-exports other modules. Import from the authoritative module directly, or make this file a package entrypoint if it defines a supported public interface.",
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
create(context) {
|
|
144
|
+
return {
|
|
145
|
+
Program(node) {
|
|
146
|
+
if (!isForwarding(node.body)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const directory = manifestDirectory(dirname(context.filename));
|
|
150
|
+
if (directory === null ||
|
|
151
|
+
(!isFrameworkFile(directory, context.filename) &&
|
|
152
|
+
!isPackageEntrypoint(directory, context.filename))) {
|
|
153
|
+
context.report({ node, messageId: "forwarding" });
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noGenericFileSuffix: import("@oxlint/plugins").Rule;
|