@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.
- package/README.md +30 -163
- package/REFERENCE.md +105 -132
- package/dist/format.d.ts +5 -3
- package/dist/index.js +26 -0
- package/dist/lint.d.ts +3 -1
- package/dist/lint.js +153 -14
- package/dist/rules/explicit-api-states.js +7 -1
- package/dist/rules/import-boundaries.js +41 -13
- package/dist/rules/no-derived-test-expectation.d.ts +1 -0
- package/dist/rules/no-derived-test-expectation.js +136 -0
- package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
- package/dist/rules/no-disabled-tls-verification.js +42 -0
- package/dist/rules/no-fixed-test-delay.js +46 -5
- package/dist/rules/no-generic-object-guard.d.ts +1 -0
- package/dist/rules/no-generic-object-guard.js +279 -0
- package/dist/rules/no-get-password-form.d.ts +1 -0
- package/dist/rules/no-get-password-form.js +113 -0
- package/dist/rules/no-hardcoded-credential.d.ts +1 -0
- package/dist/rules/no-hardcoded-credential.js +45 -0
- package/dist/rules/no-import-time-effects.d.ts +1 -0
- package/dist/rules/no-import-time-effects.js +90 -0
- package/dist/rules/no-optional-test-action.d.ts +1 -0
- package/dist/rules/no-optional-test-action.js +45 -0
- package/dist/rules/no-secret-fallback.d.ts +1 -0
- package/dist/rules/no-secret-fallback.js +64 -0
- package/dist/rules/no-swallowed-errors.js +131 -16
- package/dist/rules/no-test-timeout-overrides.js +22 -9
- package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
- package/dist/rules/no-unchecked-fetch-response.js +91 -0
- package/dist/rules/no-unchecked-index-search.d.ts +1 -0
- package/dist/rules/no-unchecked-index-search.js +41 -0
- package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
- package/dist/rules/no-uncontrolled-test-clock.js +112 -0
- package/dist/rules/no-unsettled-promise.d.ts +1 -0
- package/dist/rules/no-unsettled-promise.js +64 -0
- package/dist/rules/require-suppression-reason.d.ts +1 -0
- package/dist/rules/require-suppression-reason.js +32 -0
- package/dist/shared/index.d.ts +9 -0
- package/dist/shared/index.js +70 -0
- package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
- package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
- package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
- package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
- package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
- package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
- package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
- package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
- package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
- package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
- package/package.json +7 -7
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { importedBinding, isGlobalValue, isPlaywright, staticPropertyName, testContext, unwrapExpression, variableFor, } from "../shared/index.js";
|
|
3
|
+
const fakeClockMethods = new Set(["install", "setFixedTime"]);
|
|
4
|
+
const timerLists = new Set(["apis", "toFake", "doNotFake"]);
|
|
5
|
+
function nameOf(node) {
|
|
6
|
+
const value = unwrapExpression(node);
|
|
7
|
+
return value.type === "Identifier" ? value.name : staticPropertyName(value);
|
|
8
|
+
}
|
|
9
|
+
function fakesDate(options) {
|
|
10
|
+
const value = options === undefined ? undefined : unwrapExpression(options);
|
|
11
|
+
if (value?.type !== "ObjectExpression") {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
return value.properties.every((property) => {
|
|
15
|
+
const name = property.type === "Property" ? staticPropertyName(property) : null;
|
|
16
|
+
const list = property.type === "Property" ? unwrapExpression(property.value) : null;
|
|
17
|
+
if (name === null || !timerLists.has(name) || list?.type !== "ArrayExpression") {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const values = list.elements.map((element) => element?.type === "Literal" ? element.value : undefined);
|
|
21
|
+
if (values.length === 0 || values.includes(undefined)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
return name === "doNotFake" ? !values.includes("Date") : values.includes("Date");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function isNodeMock(sourceCode, node) {
|
|
28
|
+
const binding = importedBinding(sourceCode, unwrapExpression(node));
|
|
29
|
+
return nameOf(node) === "mock" || (binding?.source === "node:test" && binding.name === "mock");
|
|
30
|
+
}
|
|
31
|
+
function isLocalFunction(sourceCode, node) {
|
|
32
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
33
|
+
return definition !== undefined && definition.type !== "ImportBinding";
|
|
34
|
+
}
|
|
35
|
+
function controlsClock(sourceCode, call) {
|
|
36
|
+
const callee = unwrapExpression(call.callee);
|
|
37
|
+
const name = isLocalFunction(sourceCode, callee) ? null : nameOf(callee);
|
|
38
|
+
if (name === "setSystemTime") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (name === "useFakeTimers") {
|
|
42
|
+
return fakesDate(call.arguments[0]);
|
|
43
|
+
}
|
|
44
|
+
if (name === null || callee.type !== "MemberExpression") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
const owner = unwrapExpression(callee.object);
|
|
48
|
+
return ((nameOf(owner) === "clock" && fakeClockMethods.has(name)) ||
|
|
49
|
+
(name === "enable" &&
|
|
50
|
+
owner.type === "MemberExpression" &&
|
|
51
|
+
nameOf(owner) === "timers" &&
|
|
52
|
+
isNodeMock(sourceCode, owner.object) &&
|
|
53
|
+
fakesDate(call.arguments[0])));
|
|
54
|
+
}
|
|
55
|
+
export const noUncontrolledTestClock = defineRule({
|
|
56
|
+
meta: {
|
|
57
|
+
type: "problem",
|
|
58
|
+
docs: { description: "Require tests that read the current time to control the clock." },
|
|
59
|
+
schema: [],
|
|
60
|
+
messages: {
|
|
61
|
+
clock: "This test reads the real clock, so its result depends on when it runs. Use a fixed date, or control time with fake timers.",
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
create(context) {
|
|
65
|
+
const sourceCode = context.sourceCode;
|
|
66
|
+
const drivesBrowser = sourceCode.ast.body.some((statement) => statement.type === "ImportDeclaration" && isPlaywright(statement.source.value));
|
|
67
|
+
if (drivesBrowser) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const reads = [];
|
|
71
|
+
const controlledTests = new Set();
|
|
72
|
+
let controlledFile = false;
|
|
73
|
+
function enclosingTest(node) {
|
|
74
|
+
const test = testContext(sourceCode, node);
|
|
75
|
+
return test?.api.name === "test" || test?.api.name === "it" ? test.callback : null;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
CallExpression(node) {
|
|
79
|
+
if (controlsClock(sourceCode, node)) {
|
|
80
|
+
const test = enclosingTest(node);
|
|
81
|
+
if (test === null) {
|
|
82
|
+
controlledFile = true;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
controlledTests.add(test);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.callee.type === "MemberExpression" &&
|
|
89
|
+
staticPropertyName(node.callee) === "now" &&
|
|
90
|
+
isGlobalValue(sourceCode, node.callee.object, "Date")) {
|
|
91
|
+
reads.push(node);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
NewExpression(node) {
|
|
95
|
+
if (node.arguments.length === 0 && isGlobalValue(sourceCode, node.callee, "Date")) {
|
|
96
|
+
reads.push(node);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"Program:exit"() {
|
|
100
|
+
if (controlledFile) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
for (const node of reads) {
|
|
104
|
+
const test = enclosingTest(node);
|
|
105
|
+
if (test === null || !controlledTests.has(test)) {
|
|
106
|
+
context.report({ node, messageId: "clock" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const noUnsettledPromise: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
import { isGlobal, isShortCircuited } from "../shared/index.js";
|
|
3
|
+
function isOptionallyReached(reference, executor) {
|
|
4
|
+
let child = reference.identifier;
|
|
5
|
+
for (let node = child.parent; node !== null && node !== executor; child = node, node = node.parent) {
|
|
6
|
+
const argument = child;
|
|
7
|
+
if (node.type === "CallExpression" &&
|
|
8
|
+
node.arguments.some((candidate) => candidate === argument) &&
|
|
9
|
+
isShortCircuited(node)) {
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
function doesNothing(executor) {
|
|
16
|
+
const body = executor.body;
|
|
17
|
+
if (body?.type === "BlockStatement") {
|
|
18
|
+
return body.body.length === 0;
|
|
19
|
+
}
|
|
20
|
+
return ((body?.type === "Identifier" && body.name === "undefined") ||
|
|
21
|
+
(body?.type === "UnaryExpression" &&
|
|
22
|
+
body.operator === "void" &&
|
|
23
|
+
body.argument.type === "Literal"));
|
|
24
|
+
}
|
|
25
|
+
function endsByThrowing(executor) {
|
|
26
|
+
return (executor.body?.type === "BlockStatement" && executor.body.body.at(-1)?.type === "ThrowStatement");
|
|
27
|
+
}
|
|
28
|
+
export const noUnsettledPromise = defineRule({
|
|
29
|
+
meta: {
|
|
30
|
+
type: "problem",
|
|
31
|
+
docs: { description: "Require a Promise executor that runs code to settle the promise." },
|
|
32
|
+
schema: [],
|
|
33
|
+
messages: {
|
|
34
|
+
unsettled: "This executor never calls `resolve` or `reject`, so the promise never settles. Return the underlying promise instead, or settle it when the work finishes.",
|
|
35
|
+
optional: "This executor only settles inside an optional call, so the promise never settles when the target is missing. Settle it on both paths.",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
create(context) {
|
|
39
|
+
const sourceCode = context.sourceCode;
|
|
40
|
+
return {
|
|
41
|
+
NewExpression(node) {
|
|
42
|
+
const [executor] = node.arguments;
|
|
43
|
+
if (!isGlobal(sourceCode, node.callee, "Promise") ||
|
|
44
|
+
(executor?.type !== "ArrowFunctionExpression" &&
|
|
45
|
+
executor?.type !== "FunctionExpression") ||
|
|
46
|
+
doesNothing(executor) ||
|
|
47
|
+
endsByThrowing(executor) ||
|
|
48
|
+
executor.params.some((parameter) => parameter.type !== "Identifier")) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const references = sourceCode
|
|
52
|
+
.getDeclaredVariables(executor)
|
|
53
|
+
.filter((variable) => variable.defs.some((definition) => definition.type === "Parameter"))
|
|
54
|
+
.flatMap((variable) => variable.references);
|
|
55
|
+
if (references.length === 0) {
|
|
56
|
+
context.report({ node: executor, messageId: "unsettled" });
|
|
57
|
+
}
|
|
58
|
+
else if (references.every((reference) => isOptionallyReached(reference, executor))) {
|
|
59
|
+
context.report({ node: executor, messageId: "optional" });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const requireSuppressionReason: import("@oxlint/plugins").Rule;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRule } from "@oxlint/plugins";
|
|
2
|
+
const disableDirective = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?(?=\s|$)(?<rest>.*)$/su;
|
|
3
|
+
export const requireSuppressionReason = defineRule({
|
|
4
|
+
meta: {
|
|
5
|
+
type: "suggestion",
|
|
6
|
+
docs: { description: "Require each lint suppression to name its rules and give a reason." },
|
|
7
|
+
schema: [],
|
|
8
|
+
messages: {
|
|
9
|
+
rule: "Name the rules this directive suppresses, so it can't hide other diagnostics.",
|
|
10
|
+
reason: "Give this suppression a reason after `--` that says why the reported code is correct here.",
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
create(context) {
|
|
14
|
+
return {
|
|
15
|
+
Program() {
|
|
16
|
+
for (const comment of context.sourceCode.getAllComments()) {
|
|
17
|
+
const rest = disableDirective.exec(comment.value)?.groups?.["rest"];
|
|
18
|
+
if (rest === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const [rules = "", reason = ""] = rest.split(/\s-{2,}(?:\s|$)/u, 2);
|
|
22
|
+
if (rules.trim() === "") {
|
|
23
|
+
context.report({ loc: comment.loc, messageId: "rule" });
|
|
24
|
+
}
|
|
25
|
+
else if (reason.trim() === "") {
|
|
26
|
+
context.report({ loc: comment.loc, messageId: "reason" });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
});
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -23,4 +23,13 @@ export declare function projectPath(cwd: string, file: string): string;
|
|
|
23
23
|
export declare function matchesAny(path: string, patterns?: readonly string[]): boolean;
|
|
24
24
|
export declare function isGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
|
|
25
25
|
export declare function isInjectionSeam(source: ESTree.Node): boolean;
|
|
26
|
+
export declare function isShortCircuited(call: ESTree.CallExpression): boolean;
|
|
27
|
+
export declare function staticPropertyName(node: ESTree.Node): string | null;
|
|
28
|
+
export declare function importedBinding(sourceCode: SourceCode, node: ESTree.Node): {
|
|
29
|
+
readonly source: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
} | null;
|
|
32
|
+
export declare function isEffectModule(sourceCode: SourceCode, node: ESTree.Node, module: string): boolean;
|
|
33
|
+
export declare function unwrapExpression(node: ESTree.Node): ESTree.Node;
|
|
34
|
+
export declare function outermostExpression(node: ESTree.Node): ESTree.Node;
|
|
26
35
|
export {};
|
package/dist/shared/index.js
CHANGED
|
@@ -258,3 +258,73 @@ export function isInjectionSeam(source) {
|
|
|
258
258
|
}
|
|
259
259
|
return false;
|
|
260
260
|
}
|
|
261
|
+
export function isShortCircuited(call) {
|
|
262
|
+
let current = call;
|
|
263
|
+
while (current.type === "CallExpression" || current.type === "MemberExpression") {
|
|
264
|
+
if (current.optional) {
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
current = current.type === "CallExpression" ? current.callee : current.object;
|
|
268
|
+
while (current.type === "TSNonNullExpression") {
|
|
269
|
+
current = current.expression;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
export function staticPropertyName(node) {
|
|
275
|
+
if (!("computed" in node && node.computed)) {
|
|
276
|
+
return propertyName(node);
|
|
277
|
+
}
|
|
278
|
+
const key = memberKey(node);
|
|
279
|
+
return key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
|
|
280
|
+
}
|
|
281
|
+
export function importedBinding(sourceCode, node) {
|
|
282
|
+
if (node.type === "MemberExpression") {
|
|
283
|
+
const owner = importedBinding(sourceCode, node.object);
|
|
284
|
+
const name = staticPropertyName(node);
|
|
285
|
+
return (owner?.name === "*" || owner?.name === "default") && name !== null
|
|
286
|
+
? { source: owner.source, name }
|
|
287
|
+
: null;
|
|
288
|
+
}
|
|
289
|
+
const definition = variableFor(sourceCode, node)?.defs[0];
|
|
290
|
+
const declaration = definition?.parent;
|
|
291
|
+
if (definition?.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const specifier = definition.node;
|
|
295
|
+
const source = declaration.source.value;
|
|
296
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
297
|
+
return { source, name: "*" };
|
|
298
|
+
}
|
|
299
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
300
|
+
return { source, name: "default" };
|
|
301
|
+
}
|
|
302
|
+
return specifier.type === "ImportSpecifier"
|
|
303
|
+
? {
|
|
304
|
+
source,
|
|
305
|
+
name: specifier.imported.type === "Identifier"
|
|
306
|
+
? specifier.imported.name
|
|
307
|
+
: specifier.imported.value,
|
|
308
|
+
}
|
|
309
|
+
: null;
|
|
310
|
+
}
|
|
311
|
+
export function isEffectModule(sourceCode, node, module) {
|
|
312
|
+
const binding = importedBinding(sourceCode, node);
|
|
313
|
+
return ((binding?.source === "effect" && binding.name === module) ||
|
|
314
|
+
(binding?.source === `effect/${module}` && binding.name === "*"));
|
|
315
|
+
}
|
|
316
|
+
export function unwrapExpression(node) {
|
|
317
|
+
return node.type === "ParenthesizedExpression" ||
|
|
318
|
+
node.type === "TSAsExpression" ||
|
|
319
|
+
node.type === "TSSatisfiesExpression" ||
|
|
320
|
+
node.type === "TSNonNullExpression" ||
|
|
321
|
+
node.type === "TSTypeAssertion"
|
|
322
|
+
? unwrapExpression(node.expression)
|
|
323
|
+
: node;
|
|
324
|
+
}
|
|
325
|
+
export function outermostExpression(node) {
|
|
326
|
+
const parent = node.parent;
|
|
327
|
+
return parent !== null && unwrapExpression(parent) === unwrapExpression(node)
|
|
328
|
+
? outermostExpression(parent)
|
|
329
|
+
: node;
|
|
330
|
+
}
|
|
@@ -11,11 +11,16 @@ function getImportedName(specifier) {
|
|
|
11
11
|
}
|
|
12
12
|
return specifier.imported.value;
|
|
13
13
|
}
|
|
14
|
+
function isCallOrTypeQuery(reference) {
|
|
15
|
+
const parent = reference.identifier.parent;
|
|
16
|
+
return (parent.type === "TSTypeQuery" ||
|
|
17
|
+
(parent.type === "CallExpression" && parent.callee === reference.identifier));
|
|
18
|
+
}
|
|
14
19
|
export const noServiceConstructorImports = defineRule({
|
|
15
20
|
meta: {
|
|
16
21
|
type: "problem",
|
|
17
22
|
docs: {
|
|
18
|
-
description: "Disallow project-local make<CapabilityName>
|
|
23
|
+
description: "Disallow project-local make<CapabilityName> service values imported outside test and spec files.",
|
|
19
24
|
},
|
|
20
25
|
messages: {
|
|
21
26
|
serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
|
|
@@ -33,7 +38,10 @@ export const noServiceConstructorImports = defineRule({
|
|
|
33
38
|
continue;
|
|
34
39
|
}
|
|
35
40
|
const importedName = getImportedName(specifier);
|
|
36
|
-
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)
|
|
41
|
+
if (!SERVICE_CONSTRUCTOR_NAME.test(importedName) ||
|
|
42
|
+
context.sourceCode
|
|
43
|
+
.getDeclaredVariables(specifier)
|
|
44
|
+
.every((variable) => variable.references.every(isCallOrTypeQuery))) {
|
|
37
45
|
continue;
|
|
38
46
|
}
|
|
39
47
|
context.report({
|
|
@@ -210,7 +210,7 @@ export const noKnownValueWidening = defineRule({
|
|
|
210
210
|
description: "Disallow syntactically established values from flowing into explicitly broad or anonymous target types that discard useful evidence.",
|
|
211
211
|
},
|
|
212
212
|
messages: {
|
|
213
|
-
widening: "The explicit {{target}} type on {{subject}} discards
|
|
213
|
+
widening: "The explicit {{target}} type on {{subject}} discards what the value already proves. Keep the inferred type, check it with `satisfies`, or annotate it with the named type its owner exports.",
|
|
214
214
|
},
|
|
215
215
|
},
|
|
216
216
|
createOnce(context) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { apiReference, outermostExpression, propertyName } from "../../../shared/index.js";
|
|
3
4
|
import { resolveVariable } from "../shared/scope.js";
|
|
4
5
|
const moduleMockMethods = new Set(["doMock", "mock", "unstable_mockModule"]);
|
|
5
6
|
function importedName(node) {
|
|
@@ -26,13 +27,67 @@ function isTestFrameworkObject(sourceCode, expression) {
|
|
|
26
27
|
}
|
|
27
28
|
const source = definition.parent.source.value;
|
|
28
29
|
const name = importedName(definition.node);
|
|
29
|
-
return ((source === "vitest"
|
|
30
|
+
return (((source === "vitest" || source === "@effect/vitest") && name === "vi") ||
|
|
31
|
+
(source === "@jest/globals" && name === "jest"));
|
|
30
32
|
});
|
|
31
33
|
}
|
|
34
|
+
const nodeTestModules = ["node:test", "test"];
|
|
35
|
+
const nodeTestContextMethods = new Set(["test", "before", "after", "beforeEach", "afterEach"]);
|
|
36
|
+
function callbackCalls(sourceCode, callback) {
|
|
37
|
+
const argument = outermostExpression(callback);
|
|
38
|
+
const declarator = argument.parent;
|
|
39
|
+
const holder = callback.type === "FunctionDeclaration"
|
|
40
|
+
? callback
|
|
41
|
+
: declarator?.type === "VariableDeclarator" && declarator.init === argument
|
|
42
|
+
? declarator
|
|
43
|
+
: null;
|
|
44
|
+
const uses = holder === null
|
|
45
|
+
? [argument]
|
|
46
|
+
: sourceCode
|
|
47
|
+
.getDeclaredVariables(holder)
|
|
48
|
+
.flatMap((variable) => variable.references.map((reference) => outermostExpression(reference.identifier)));
|
|
49
|
+
return uses.flatMap((use) => use.parent?.type === "CallExpression" && use.parent.arguments.some((item) => item === use)
|
|
50
|
+
? [use.parent]
|
|
51
|
+
: []);
|
|
52
|
+
}
|
|
53
|
+
function isNodeTestContext(sourceCode, node) {
|
|
54
|
+
const definition = resolveVariable(sourceCode, node)?.defs[0];
|
|
55
|
+
const callback = definition?.type === "Parameter" ? definition.node : null;
|
|
56
|
+
if (callback === null || !("params" in callback) || callback.params[0] !== definition?.name) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return callbackCalls(sourceCode, callback).some(({ callee }) => nodeTestModules.includes(apiReference(sourceCode, callee)?.module ?? "") ||
|
|
60
|
+
(callee.type === "MemberExpression" &&
|
|
61
|
+
callee.object.type === "Identifier" &&
|
|
62
|
+
nodeTestContextMethods.has(propertyName(callee) ?? "") &&
|
|
63
|
+
isNodeTestContext(sourceCode, callee.object)));
|
|
64
|
+
}
|
|
65
|
+
function isNodeTestMock(sourceCode, expression) {
|
|
66
|
+
if (expression.type === "MemberExpression") {
|
|
67
|
+
return (!expression.computed &&
|
|
68
|
+
expression.property.type === "Identifier" &&
|
|
69
|
+
expression.property.name === "mock" &&
|
|
70
|
+
expression.object.type === "Identifier" &&
|
|
71
|
+
isNodeTestContext(sourceCode, expression.object));
|
|
72
|
+
}
|
|
73
|
+
if (expression.type !== "Identifier") {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return (resolveVariable(sourceCode, expression)?.defs.some((definition) => definition.type === "ImportBinding" &&
|
|
77
|
+
definition.parent?.type === "ImportDeclaration" &&
|
|
78
|
+
nodeTestModules.includes(definition.parent.source.value) &&
|
|
79
|
+
importedName(definition.node) === "mock") ?? false);
|
|
80
|
+
}
|
|
32
81
|
function moduleMockCall(sourceCode, callee) {
|
|
33
82
|
if (!("property" in callee) || !("object" in callee) || !("computed" in callee)) {
|
|
34
83
|
return false;
|
|
35
84
|
}
|
|
85
|
+
if (!callee.computed &&
|
|
86
|
+
callee.property.type === "Identifier" &&
|
|
87
|
+
callee.property.name === "module" &&
|
|
88
|
+
isNodeTestMock(sourceCode, callee.object)) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
36
91
|
if (!isTestFrameworkObject(sourceCode, callee.object)) {
|
|
37
92
|
return false;
|
|
38
93
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { variableFor } from "../../../shared/index.js";
|
|
4
|
+
import { createTypeAliasEnvironment, resolvedTypeMatches, } from "../shared/type-alias-resolution.js";
|
|
3
5
|
function isRuntimeFunction(node) {
|
|
4
6
|
return (node.type === "ArrowFunctionExpression" ||
|
|
5
7
|
node.type === "FunctionDeclaration" ||
|
|
@@ -26,6 +28,24 @@ function isExistenceProbe(node) {
|
|
|
26
28
|
const other = parent.left === node ? parent.right : parent.left;
|
|
27
29
|
return other.type === "Literal" && other.value === "undefined";
|
|
28
30
|
}
|
|
31
|
+
function declaredType(sourceCode, node) {
|
|
32
|
+
const name = variableFor(sourceCode, node)?.defs[0]?.name;
|
|
33
|
+
return name?.type === "Identifier" ? (name.typeAnnotation?.typeAnnotation ?? null) : null;
|
|
34
|
+
}
|
|
35
|
+
function isUnion(type, environment) {
|
|
36
|
+
return resolvedTypeMatches(type, environment, (resolved, matches) => resolved.type === "TSParenthesizedType"
|
|
37
|
+
? matches(resolved.typeAnnotation)
|
|
38
|
+
: resolved.type === "TSUnionType");
|
|
39
|
+
}
|
|
40
|
+
function isOpen(type, environment) {
|
|
41
|
+
return resolvedTypeMatches(type, environment, (resolved, matches) => resolved.type === "TSUnknownKeyword" ||
|
|
42
|
+
resolved.type === "TSAnyKeyword" ||
|
|
43
|
+
(resolved.type === "TSParenthesizedType" && matches(resolved.typeAnnotation)) ||
|
|
44
|
+
(resolved.type === "TSUnionType" && resolved.types.some(matches)));
|
|
45
|
+
}
|
|
46
|
+
function isKnownUnion(type, environment) {
|
|
47
|
+
return isUnion(type, environment) && !isOpen(type, environment);
|
|
48
|
+
}
|
|
29
49
|
export const noRuntimeTypeof = defineRule({
|
|
30
50
|
meta: {
|
|
31
51
|
type: "problem",
|
|
@@ -33,7 +53,7 @@ export const noRuntimeTypeof = defineRule({
|
|
|
33
53
|
description: "Disallow runtime typeof checks; external values must be decoded into meaningful types at their I/O boundary.",
|
|
34
54
|
},
|
|
35
55
|
messages: {
|
|
36
|
-
runtimeTypeof: "A `typeof` check narrows a representation without
|
|
56
|
+
runtimeTypeof: "A `typeof` check narrows a value's representation without proving its shape. Parse input where it enters, then branch on the domain value.",
|
|
37
57
|
},
|
|
38
58
|
schema: [
|
|
39
59
|
{
|
|
@@ -45,7 +65,15 @@ export const noRuntimeTypeof = defineRule({
|
|
|
45
65
|
defaultOptions: [{ allowInTypeGuards: false }],
|
|
46
66
|
},
|
|
47
67
|
createOnce(context) {
|
|
68
|
+
let environment = null;
|
|
69
|
+
function discriminatesKnownUnion(node) {
|
|
70
|
+
const type = declaredType(context.sourceCode, node.argument);
|
|
71
|
+
return type !== null && environment !== null && isKnownUnion(type, environment);
|
|
72
|
+
}
|
|
48
73
|
return {
|
|
74
|
+
Program(program) {
|
|
75
|
+
environment = createTypeAliasEnvironment(program, context.sourceCode.visitorKeys);
|
|
76
|
+
},
|
|
49
77
|
UnaryExpression(node) {
|
|
50
78
|
const option = context.options[0];
|
|
51
79
|
const allowInTypeGuards = typeof option === "object" &&
|
|
@@ -54,6 +82,7 @@ export const noRuntimeTypeof = defineRule({
|
|
|
54
82
|
option.allowInTypeGuards === true;
|
|
55
83
|
if (node.operator === "typeof" &&
|
|
56
84
|
!isExistenceProbe(node) &&
|
|
85
|
+
!discriminatesKnownUnion(node) &&
|
|
57
86
|
(!allowInTypeGuards || !isInsideTypeGuard(node))) {
|
|
58
87
|
context.report({ node, messageId: "runtimeTypeof" });
|
|
59
88
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Adapted from dmmulroy/anti-slop
|
|
2
2
|
import { defineRule } from "@oxlint/plugins";
|
|
3
|
+
import { variableFor } from "../../../shared/index.js";
|
|
3
4
|
const FORBIDDEN_SYMBOL_NAME = "shape";
|
|
4
5
|
function containsForbiddenSymbolName(name) {
|
|
5
6
|
return name.toLowerCase().includes(FORBIDDEN_SYMBOL_NAME);
|
|
@@ -11,6 +12,30 @@ function isBorrowedMemberName(node) {
|
|
|
11
12
|
}
|
|
12
13
|
return parent.property === node && !parent.computed;
|
|
13
14
|
}
|
|
15
|
+
function importedName(specifier) {
|
|
16
|
+
return specifier.imported.type === "Identifier"
|
|
17
|
+
? specifier.imported.name
|
|
18
|
+
: specifier.imported.value;
|
|
19
|
+
}
|
|
20
|
+
function isPackageImport(specifier) {
|
|
21
|
+
return (specifier.type === "ImportSpecifier" &&
|
|
22
|
+
specifier.parent.type === "ImportDeclaration" &&
|
|
23
|
+
!/^(?:\.{1,2}\/|@\/|~\/|#)/u.test(specifier.parent.source.value));
|
|
24
|
+
}
|
|
25
|
+
function isBorrowedImportName(sourceCode, node) {
|
|
26
|
+
const parent = node.parent;
|
|
27
|
+
if (parent?.type === "ImportSpecifier" && parent.imported === node) {
|
|
28
|
+
return isPackageImport(parent);
|
|
29
|
+
}
|
|
30
|
+
const variable = variableFor(sourceCode, node);
|
|
31
|
+
const definition = variable?.defs[0];
|
|
32
|
+
const specifier = definition?.type === "ImportBinding" ? definition.node : null;
|
|
33
|
+
return (specifier !== null &&
|
|
34
|
+
isPackageImport(specifier) &&
|
|
35
|
+
importedName(specifier) === node.name &&
|
|
36
|
+
(definition?.name === node ||
|
|
37
|
+
(variable?.references.some((reference) => reference.identifier === node) ?? false)));
|
|
38
|
+
}
|
|
14
39
|
export const noForbiddenTermInSymbolNames = defineRule({
|
|
15
40
|
meta: {
|
|
16
41
|
type: "problem",
|
|
@@ -23,7 +48,9 @@ export const noForbiddenTermInSymbolNames = defineRule({
|
|
|
23
48
|
},
|
|
24
49
|
createOnce(context) {
|
|
25
50
|
const reportForbiddenSymbolName = (node) => {
|
|
26
|
-
if (!containsForbiddenSymbolName(node.name) ||
|
|
51
|
+
if (!containsForbiddenSymbolName(node.name) ||
|
|
52
|
+
isBorrowedMemberName(node) ||
|
|
53
|
+
isBorrowedImportName(context.sourceCode, node)) {
|
|
27
54
|
return;
|
|
28
55
|
}
|
|
29
56
|
context.report({ node, messageId: "forbiddenSymbolName", data: { name: node.name } });
|
|
@@ -5,7 +5,7 @@ export const noUnknownReturns = defineRule({
|
|
|
5
5
|
meta: {
|
|
6
6
|
type: "problem",
|
|
7
7
|
docs: {
|
|
8
|
-
description: "Disallow functions whose
|
|
8
|
+
description: "Disallow functions whose declared return type is unknown or Promise<unknown>.",
|
|
9
9
|
},
|
|
10
10
|
messages: {
|
|
11
11
|
unknownReturn: "This function exposes `unknown` to its caller. Parse the value at its boundary and return a named domain type.",
|
|
@@ -101,10 +101,10 @@ export const noUnsafeDictionaryType = defineRule({
|
|
|
101
101
|
meta: {
|
|
102
102
|
type: "problem",
|
|
103
103
|
docs: {
|
|
104
|
-
description: "Disallow
|
|
104
|
+
description: "Disallow dictionary types whose value type is unknown, any, object, {}, or a union or alias containing one of them.",
|
|
105
105
|
},
|
|
106
106
|
messages: {
|
|
107
|
-
unsafeDictionary: "This dictionary's {{value}} value type
|
|
107
|
+
unsafeDictionary: "This dictionary's {{value}} value type tells callers nothing about its values. Use the owner's or schema's value type, and parse external payloads before inserting them.",
|
|
108
108
|
},
|
|
109
109
|
},
|
|
110
110
|
createOnce(context) {
|