@jameswyse/oxc-config 0.3.0 → 0.4.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/REFERENCE.md +3 -3
- package/dist/rules/electron-no-remote-module.js +3 -6
- package/dist/rules/no-ambient-clock.js +104 -8
- package/dist/rules/no-env-access.js +62 -5
- package/dist/rules/no-uncontrolled-test-clock.js +1 -1
- package/dist/shared/index.d.ts +12 -8
- package/dist/shared/index.js +213 -143
- package/package.json +1 -1
package/REFERENCE.md
CHANGED
|
@@ -72,11 +72,11 @@ Native rules: `prefer-const`, `no-param-reassign` and `oxc/no-accumulating-sprea
|
|
|
72
72
|
|
|
73
73
|
These rules run only once the project names its owners through the [project boundary options](docs/configuration.md#enforce-project-boundaries).
|
|
74
74
|
|
|
75
|
-
- `wyse/no-env-access` (`env`): reading `process.env` or `import.meta.env`, or
|
|
75
|
+
- `wyse/no-env-access` (`env`): reading `process.env` or `import.meta.env` outside the owners, config files and scripts. That includes destructuring `env`, `globalThis.process.env`, `global.process.env`, `process["env"]` and `Reflect.get(process, "env")`, including with `"env" as const`, importing `env` from `node:process` or `process`, reading `env` from those modules' default, namespace or literal dynamic import, and re-exporting `env`, the default or the whole module from them with `export { ... } from` or `export * from`. Other `node:process` imports and re-exports, such as `argv`, type-only re-exports, and a `process` property on `window` or `self` pass. It doesn't check that the owner validates what it reads.
|
|
76
76
|
- `wyse/import-boundaries` (`boundaries`): a value import, re-export, literal dynamic import, or `require` call, including a loader made with `createRequire` from `node:module`, that matches a `deny` glob. Type-only imports and `import("…")` types pass unless the boundary sets `types: true`. It doesn't check computed imports.
|
|
77
77
|
- `wyse/server-action-guard` (`serverActionGuards`): a `"use server"` action that doesn't start by awaiting a guard, and indirect or default exports from server-action modules. Non-exported helpers and test files pass.
|
|
78
|
-
- `wyse/no-ambient-clock` (`clock`): `Date.now()` and argument-less `new Date()`, including through `globalThis` or `window`. Parameter defaults, uncalled `Date.now`, provider functions that only return the read, and test files pass.
|
|
79
|
-
- `wyse/no-ambient-random` (`random`): `Math.random()`, `crypto.randomUUID()`, and `randomUUID` or `randomInt` from `node:crypto`. The same exceptions apply. `randomBytes` and `getRandomValues` pass because they generate secrets.
|
|
78
|
+
- `wyse/no-ambient-clock` (`clock`): `Date.now()`, `Date()` and argument-less `new Date()`, including through `globalThis` or `window`. It also reports argument-less `new TZDate()` and `new TZDateMini()` and one-argument `TZDate.tz(zone)` from `@date-fns/tz`, `@date-fns/tz/date` or `@date-fns/tz/date/mini`, and calls to the date-fns helpers that read the current time: `constructNow`, the `startOf` and `endOf` helpers for today, tomorrow and yesterday, `isToday`, `isTomorrow`, `isYesterday`, `isPast`, `isFuture`, the `isThis*` helpers, `formatDistanceToNow`, `formatDistanceToNowStrict`, `isMatch`, and `isMatch` and `isMatchWithOptions` from `date-fns/fp` at the call that supplies their last curried argument. Helpers count whether they're imported by name, through a namespace, from a `date-fns/<name>` subpath or by literal dynamic import. Re-exporting one of these helpers with `export { ... } from`, or a module that contains one, such as `date-fns` or `date-fns/fp`, with `export * from`, also reports, because calls through the re-export no longer resolve to date-fns. Helpers that compare with a given date, such as `isSameDay`, partly applied `date-fns/fp` helpers, and re-exports of `TZDate`, `TZDateMini`, other date-fns helpers or types pass. Parameter defaults, uncalled `Date.now`, provider functions that only return the read, and test files pass. Parentheses, `as`, `satisfies` and `!` don't change the result, so `(Date.now as typeof Date.now)()` reports and `{ now: () => startOfToday() as Date }` passes.
|
|
79
|
+
- `wyse/no-ambient-random` (`random`): `Math.random()`, `crypto.randomUUID()`, and `randomUUID` or `randomInt` from `node:crypto`. The same exceptions and wrapper handling apply. `randomBytes` and `getRandomValues` pass because they generate secrets.
|
|
80
80
|
|
|
81
81
|
## Tests
|
|
82
82
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import { isGlobal, propertyName, variableFor } from "../shared/index.js";
|
|
2
|
+
import { isGlobal, moduleExportName, propertyName, variableFor } from "../shared/index.js";
|
|
3
3
|
function moduleName(node) {
|
|
4
4
|
return node?.type === "Literal" && typeof node.value === "string" ? node.value : null;
|
|
5
5
|
}
|
|
@@ -38,9 +38,6 @@ function isElectronModule(sourceCode, node) {
|
|
|
38
38
|
definitionNode.id.type === "Identifier" &&
|
|
39
39
|
isElectron(requiredModule(sourceCode, definitionNode.init)));
|
|
40
40
|
}
|
|
41
|
-
function specifierName(node) {
|
|
42
|
-
return node.type === "Identifier" ? node.name : node.value;
|
|
43
|
-
}
|
|
44
41
|
export const electronNoRemoteModule = defineRule({
|
|
45
42
|
meta: {
|
|
46
43
|
type: "problem",
|
|
@@ -66,7 +63,7 @@ export const electronNoRemoteModule = defineRule({
|
|
|
66
63
|
}
|
|
67
64
|
for (const specifier of node.specifiers) {
|
|
68
65
|
if (specifier.type === "ImportSpecifier" &&
|
|
69
|
-
|
|
66
|
+
moduleExportName(specifier.imported) === "remote") {
|
|
70
67
|
context.report({ node: specifier, messageId: "remote" });
|
|
71
68
|
}
|
|
72
69
|
}
|
|
@@ -76,7 +73,7 @@ export const electronNoRemoteModule = defineRule({
|
|
|
76
73
|
return;
|
|
77
74
|
}
|
|
78
75
|
for (const specifier of node.specifiers) {
|
|
79
|
-
if (
|
|
76
|
+
if (moduleExportName(specifier.local) === "remote") {
|
|
80
77
|
context.report({ node: specifier, messageId: "remote" });
|
|
81
78
|
}
|
|
82
79
|
}
|
|
@@ -1,5 +1,73 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import {
|
|
2
|
+
import { importedBinding, isGlobalValue, isInjectionSeam, moduleExportName, outermostExpression, staticPropertyName, unwrapExpression, } from "../shared/index.js";
|
|
3
|
+
const timeZoneModules = new Set(["@date-fns/tz", "@date-fns/tz/date", "@date-fns/tz/date/mini"]);
|
|
4
|
+
const timeZoneDates = new Set(["TZDate", "TZDateMini"]);
|
|
5
|
+
const clockHelpers = new Set([
|
|
6
|
+
"date-fns/constructNow",
|
|
7
|
+
"date-fns/startOfToday",
|
|
8
|
+
"date-fns/endOfToday",
|
|
9
|
+
"date-fns/startOfTomorrow",
|
|
10
|
+
"date-fns/endOfTomorrow",
|
|
11
|
+
"date-fns/startOfYesterday",
|
|
12
|
+
"date-fns/endOfYesterday",
|
|
13
|
+
"date-fns/isToday",
|
|
14
|
+
"date-fns/isTomorrow",
|
|
15
|
+
"date-fns/isYesterday",
|
|
16
|
+
"date-fns/isPast",
|
|
17
|
+
"date-fns/isFuture",
|
|
18
|
+
"date-fns/isThisSecond",
|
|
19
|
+
"date-fns/isThisMinute",
|
|
20
|
+
"date-fns/isThisHour",
|
|
21
|
+
"date-fns/isThisWeek",
|
|
22
|
+
"date-fns/isThisISOWeek",
|
|
23
|
+
"date-fns/isThisMonth",
|
|
24
|
+
"date-fns/isThisQuarter",
|
|
25
|
+
"date-fns/isThisYear",
|
|
26
|
+
"date-fns/formatDistanceToNow",
|
|
27
|
+
"date-fns/formatDistanceToNowStrict",
|
|
28
|
+
"date-fns/isMatch",
|
|
29
|
+
]);
|
|
30
|
+
const curriedClockHelpers = new Map([
|
|
31
|
+
["date-fns/fp/isMatch", 2],
|
|
32
|
+
["date-fns/fp/isMatchWithOptions", 3],
|
|
33
|
+
]);
|
|
34
|
+
const clockExports = new Set([...clockHelpers, ...curriedClockHelpers.keys()]);
|
|
35
|
+
function helperPath(source, name) {
|
|
36
|
+
return name === "default" || source.endsWith(`/${name}`) ? source : `${source}/${name}`;
|
|
37
|
+
}
|
|
38
|
+
function exportPath(sourceCode, node) {
|
|
39
|
+
const binding = importedBinding(sourceCode, node);
|
|
40
|
+
return binding === null ? null : helperPath(binding.source, binding.name);
|
|
41
|
+
}
|
|
42
|
+
function exposesClockHelper(source) {
|
|
43
|
+
return [...clockExports].some((helper) => helperPath(source, helper.slice(helper.lastIndexOf("/") + 1)) === helper);
|
|
44
|
+
}
|
|
45
|
+
function fullApplication(call, arity) {
|
|
46
|
+
if (call.arguments.some((argument) => argument.type === "SpreadElement")) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const remaining = arity - call.arguments.length;
|
|
50
|
+
if (remaining <= 0) {
|
|
51
|
+
return call;
|
|
52
|
+
}
|
|
53
|
+
const parent = outermostExpression(call).parent;
|
|
54
|
+
return parent?.type === "CallExpression" && unwrapExpression(parent.callee) === call
|
|
55
|
+
? fullApplication(parent, remaining)
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
function isTimeZoneDate(sourceCode, node) {
|
|
59
|
+
const binding = importedBinding(sourceCode, node);
|
|
60
|
+
return binding !== null && timeZoneModules.has(binding.source) && timeZoneDates.has(binding.name);
|
|
61
|
+
}
|
|
62
|
+
function isCurrentTimeZoneDate(sourceCode, node) {
|
|
63
|
+
const callee = unwrapExpression(node.callee);
|
|
64
|
+
const [zone] = node.arguments;
|
|
65
|
+
return (callee.type === "MemberExpression" &&
|
|
66
|
+
staticPropertyName(callee) === "tz" &&
|
|
67
|
+
isTimeZoneDate(sourceCode, callee.object) &&
|
|
68
|
+
node.arguments.length === 1 &&
|
|
69
|
+
zone?.type !== "SpreadElement");
|
|
70
|
+
}
|
|
3
71
|
export const noAmbientClock = defineRule({
|
|
4
72
|
meta: {
|
|
5
73
|
type: "suggestion",
|
|
@@ -11,24 +79,52 @@ export const noAmbientClock = defineRule({
|
|
|
11
79
|
},
|
|
12
80
|
create(context) {
|
|
13
81
|
const sourceCode = context.sourceCode;
|
|
14
|
-
function
|
|
15
|
-
if (
|
|
82
|
+
function report(node) {
|
|
83
|
+
if (!isInjectionSeam(node)) {
|
|
16
84
|
context.report({ node, messageId: "clock" });
|
|
17
85
|
}
|
|
18
86
|
}
|
|
19
87
|
return {
|
|
20
88
|
MemberExpression(node) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
89
|
+
if (staticPropertyName(node) === "now" && isGlobalValue(sourceCode, node.object, "Date")) {
|
|
90
|
+
report(node);
|
|
23
91
|
}
|
|
24
92
|
},
|
|
25
93
|
NewExpression(node) {
|
|
26
|
-
if (node.arguments.length === 0
|
|
27
|
-
|
|
94
|
+
if (node.arguments.length === 0 &&
|
|
95
|
+
(isGlobalValue(sourceCode, node.callee, "Date") ||
|
|
96
|
+
isTimeZoneDate(sourceCode, node.callee))) {
|
|
97
|
+
report(node);
|
|
28
98
|
}
|
|
29
99
|
},
|
|
30
100
|
CallExpression(node) {
|
|
31
|
-
|
|
101
|
+
const helper = exportPath(sourceCode, node.callee);
|
|
102
|
+
const arity = helper === null ? undefined : curriedClockHelpers.get(helper);
|
|
103
|
+
const applied = arity === undefined ? null : fullApplication(node, arity);
|
|
104
|
+
if (applied !== null) {
|
|
105
|
+
report(applied);
|
|
106
|
+
}
|
|
107
|
+
if (isGlobalValue(sourceCode, node.callee, "Date") ||
|
|
108
|
+
(helper !== null && clockHelpers.has(helper)) ||
|
|
109
|
+
isCurrentTimeZoneDate(sourceCode, node)) {
|
|
110
|
+
report(node);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
ExportNamedDeclaration(node) {
|
|
114
|
+
if (!node.source || node.exportKind === "type") {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const specifier of node.specifiers) {
|
|
118
|
+
if (specifier.exportKind !== "type" &&
|
|
119
|
+
clockExports.has(helperPath(node.source.value, moduleExportName(specifier.local)))) {
|
|
120
|
+
context.report({ node: specifier, messageId: "clock" });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
ExportAllDeclaration(node) {
|
|
125
|
+
if (node.exportKind !== "type" && exposesClockHelper(node.source.value)) {
|
|
126
|
+
context.report({ node, messageId: "clock" });
|
|
127
|
+
}
|
|
32
128
|
},
|
|
33
129
|
};
|
|
34
130
|
},
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { defineRule } from "@oxlint/plugins";
|
|
2
|
-
import {
|
|
2
|
+
import { importedBinding, isGlobalValue, isNodeGlobalValue, moduleExportName, staticPropertyName, staticString, unwrapExpression, } from "../shared/index.js";
|
|
3
|
+
const processModules = new Set(["node:process", "process"]);
|
|
4
|
+
const environmentExports = new Set(["env", "default"]);
|
|
3
5
|
function isImportMeta(node) {
|
|
4
|
-
|
|
6
|
+
const value = unwrapExpression(node);
|
|
7
|
+
return (value.type === "MetaProperty" && value.meta.name === "import" && value.property.name === "meta");
|
|
8
|
+
}
|
|
9
|
+
function isProcessModule(sourceCode, node) {
|
|
10
|
+
const binding = importedBinding(sourceCode, node);
|
|
11
|
+
return (binding !== null &&
|
|
12
|
+
processModules.has(binding.source) &&
|
|
13
|
+
(binding.name === "*" || binding.name === "default"));
|
|
5
14
|
}
|
|
6
15
|
function isEnvironmentSource(sourceCode, node) {
|
|
7
|
-
|
|
16
|
+
if (!node) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return (isNodeGlobalValue(sourceCode, node, "process") ||
|
|
20
|
+
isImportMeta(node) ||
|
|
21
|
+
isProcessModule(sourceCode, node));
|
|
22
|
+
}
|
|
23
|
+
function isReflectGet(sourceCode, node) {
|
|
24
|
+
const callee = unwrapExpression(node.callee);
|
|
25
|
+
const [target, key] = node.arguments;
|
|
26
|
+
return (callee.type === "MemberExpression" &&
|
|
27
|
+
staticPropertyName(callee) === "get" &&
|
|
28
|
+
isGlobalValue(sourceCode, callee.object, "Reflect") &&
|
|
29
|
+
target?.type !== "SpreadElement" &&
|
|
30
|
+
isEnvironmentSource(sourceCode, target) &&
|
|
31
|
+
staticString(key) === "env");
|
|
8
32
|
}
|
|
9
33
|
export const noEnvAccess = defineRule({
|
|
10
34
|
meta: {
|
|
@@ -19,14 +43,47 @@ export const noEnvAccess = defineRule({
|
|
|
19
43
|
const sourceCode = context.sourceCode;
|
|
20
44
|
return {
|
|
21
45
|
MemberExpression(node) {
|
|
22
|
-
if (
|
|
46
|
+
if (staticPropertyName(node) === "env" && isEnvironmentSource(sourceCode, node.object)) {
|
|
23
47
|
context.report({ node, messageId: "env" });
|
|
24
48
|
}
|
|
25
49
|
},
|
|
26
50
|
VariableDeclarator(node) {
|
|
27
51
|
if (node.id.type === "ObjectPattern" &&
|
|
28
52
|
isEnvironmentSource(sourceCode, node.init) &&
|
|
29
|
-
node.id.properties.some((property) => property.type === "Property" &&
|
|
53
|
+
node.id.properties.some((property) => property.type === "Property" && staticPropertyName(property) === "env")) {
|
|
54
|
+
context.report({ node, messageId: "env" });
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
CallExpression(node) {
|
|
58
|
+
if (isReflectGet(sourceCode, node)) {
|
|
59
|
+
context.report({ node, messageId: "env" });
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
ImportDeclaration(node) {
|
|
63
|
+
if (!processModules.has(node.source.value) || node.importKind === "type") {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
for (const specifier of node.specifiers) {
|
|
67
|
+
if (specifier.type === "ImportSpecifier" &&
|
|
68
|
+
specifier.importKind !== "type" &&
|
|
69
|
+
moduleExportName(specifier.imported) === "env") {
|
|
70
|
+
context.report({ node: specifier, messageId: "env" });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
ExportNamedDeclaration(node) {
|
|
75
|
+
if (!node.source || !processModules.has(node.source.value) || node.exportKind === "type") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const specifier of node.specifiers) {
|
|
79
|
+
if (specifier.exportKind !== "type" &&
|
|
80
|
+
environmentExports.has(moduleExportName(specifier.local))) {
|
|
81
|
+
context.report({ node: specifier, messageId: "env" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
ExportAllDeclaration(node) {
|
|
86
|
+
if (processModules.has(node.source.value) && node.exportKind !== "type") {
|
|
30
87
|
context.report({ node, messageId: "env" });
|
|
31
88
|
}
|
|
32
89
|
},
|
|
@@ -25,7 +25,7 @@ function fakesDate(options) {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
function isNodeMock(sourceCode, node) {
|
|
28
|
-
const binding = importedBinding(sourceCode,
|
|
28
|
+
const binding = importedBinding(sourceCode, node);
|
|
29
29
|
return nameOf(node) === "mock" || (binding?.source === "node:test" && binding.name === "mock");
|
|
30
30
|
}
|
|
31
31
|
function isLocalFunction(sourceCode, node) {
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export type ApiReference = {
|
|
|
3
3
|
readonly module: string;
|
|
4
4
|
readonly name: string;
|
|
5
5
|
};
|
|
6
|
+
export type ImportedBinding = {
|
|
7
|
+
readonly source: string;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
};
|
|
6
10
|
export type TestContext = {
|
|
7
11
|
readonly callback: FunctionNode;
|
|
8
12
|
readonly api: ApiReference;
|
|
@@ -17,19 +21,19 @@ export declare function variableFor(sourceCode: SourceCode, node: ESTree.Node):
|
|
|
17
21
|
export declare function isGlobal(sourceCode: SourceCode, identifier: ESTree.Node | null | undefined, name: string): boolean;
|
|
18
22
|
export declare function propertyName(node: ESTree.Node): string | null;
|
|
19
23
|
export declare function isPlaywright(module: string): boolean;
|
|
20
|
-
export declare function
|
|
24
|
+
export declare function unwrapExpression(node: ESTree.Node): ESTree.Node;
|
|
25
|
+
export declare function outermostExpression(node: ESTree.Node): ESTree.Node;
|
|
26
|
+
export declare function staticString(node: ESTree.Node | null | undefined): string | null;
|
|
27
|
+
export declare function staticPropertyName(node: ESTree.Node): string | null;
|
|
28
|
+
export declare function moduleExportName(node: ESTree.ModuleExportName): string;
|
|
29
|
+
export declare function importedBinding(sourceCode: SourceCode, node: ESTree.Node): ImportedBinding | null;
|
|
30
|
+
export declare function apiReference(sourceCode: SourceCode, node: ESTree.Node): ApiReference | null;
|
|
21
31
|
export declare function testContext(sourceCode: SourceCode, node: ESTree.Node): TestContext | null;
|
|
22
32
|
export declare function projectPath(cwd: string, file: string): string;
|
|
23
33
|
export declare function matchesAny(path: string, patterns?: readonly string[]): boolean;
|
|
24
34
|
export declare function isGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
|
|
35
|
+
export declare function isNodeGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
|
|
25
36
|
export declare function isInjectionSeam(source: ESTree.Node): boolean;
|
|
26
37
|
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
38
|
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;
|
|
35
39
|
export {};
|
package/dist/shared/index.js
CHANGED
|
@@ -89,94 +89,207 @@ export function propertyName(node) {
|
|
|
89
89
|
export function isPlaywright(module) {
|
|
90
90
|
return module === "@playwright/test" || module === "playwright/test";
|
|
91
91
|
}
|
|
92
|
-
export function
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
export function unwrapExpression(node) {
|
|
93
|
+
return node.type === "ParenthesizedExpression" ||
|
|
94
|
+
node.type === "TSAsExpression" ||
|
|
95
|
+
node.type === "TSSatisfiesExpression" ||
|
|
96
|
+
node.type === "TSNonNullExpression" ||
|
|
97
|
+
node.type === "TSTypeAssertion"
|
|
98
|
+
? unwrapExpression(node.expression)
|
|
99
|
+
: node;
|
|
100
|
+
}
|
|
101
|
+
export function outermostExpression(node) {
|
|
102
|
+
const parent = node.parent;
|
|
103
|
+
return parent !== null && unwrapExpression(parent) === unwrapExpression(node)
|
|
104
|
+
? outermostExpression(parent)
|
|
105
|
+
: node;
|
|
106
|
+
}
|
|
107
|
+
export function staticString(node) {
|
|
108
|
+
const value = node ? unwrapExpression(node) : null;
|
|
109
|
+
if (value?.type === "TemplateLiteral") {
|
|
110
|
+
const [quasi] = value.quasis;
|
|
111
|
+
return value.expressions.length === 0 && quasi ? (quasi.value.cooked ?? null) : null;
|
|
112
|
+
}
|
|
113
|
+
return value?.type === "Literal" && typeof value.value === "string" ? value.value : null;
|
|
114
|
+
}
|
|
115
|
+
export function staticPropertyName(node) {
|
|
116
|
+
return "computed" in node && node.computed ? staticString(memberKey(node)) : propertyName(node);
|
|
117
|
+
}
|
|
118
|
+
export function moduleExportName(node) {
|
|
119
|
+
return node.type === "Identifier" ? node.name : node.value;
|
|
120
|
+
}
|
|
121
|
+
function dynamicImportSource(node) {
|
|
122
|
+
const awaited = unwrapExpression(node);
|
|
123
|
+
const loaded = awaited.type === "AwaitExpression" ? unwrapExpression(awaited.argument) : null;
|
|
124
|
+
if (loaded?.type !== "ImportExpression") {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return staticString(loaded.source);
|
|
128
|
+
}
|
|
129
|
+
const bindingCache = new WeakMap();
|
|
130
|
+
const referenceCache = new WeakMap();
|
|
131
|
+
function memberBinding(owner, name) {
|
|
132
|
+
return (owner?.name === "*" || owner?.name === "default") && name !== null
|
|
133
|
+
? { source: owner.source, name }
|
|
134
|
+
: null;
|
|
135
|
+
}
|
|
136
|
+
function bindsName(property, name) {
|
|
137
|
+
if (property.type !== "Property") {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const target = property.value.type === "AssignmentPattern" ? property.value.left : property.value;
|
|
141
|
+
return target.type === "Identifier" && target.name === name;
|
|
142
|
+
}
|
|
143
|
+
function constAlias(definition) {
|
|
144
|
+
const declaration = definition.parent;
|
|
145
|
+
const declarator = definition.node;
|
|
146
|
+
return definition.type === "Variable" &&
|
|
147
|
+
declaration?.type === "VariableDeclaration" &&
|
|
148
|
+
declaration.kind === "const" &&
|
|
149
|
+
declarator.type === "VariableDeclarator" &&
|
|
150
|
+
declarator.init
|
|
151
|
+
? { init: declarator.init, pattern: declarator.id, name: definition.name.name }
|
|
152
|
+
: null;
|
|
153
|
+
}
|
|
154
|
+
function destructuredName(pattern, name) {
|
|
155
|
+
const property = pattern.type === "ObjectPattern"
|
|
156
|
+
? pattern.properties.find((candidate) => bindsName(candidate, name))
|
|
157
|
+
: undefined;
|
|
158
|
+
return property ? staticPropertyName(property) : null;
|
|
159
|
+
}
|
|
160
|
+
function aliasTarget(alias, resolve, member) {
|
|
161
|
+
const initial = resolve(alias.init);
|
|
162
|
+
if (alias.pattern.type === "Identifier") {
|
|
163
|
+
return initial;
|
|
164
|
+
}
|
|
165
|
+
const name = destructuredName(alias.pattern, alias.name);
|
|
166
|
+
return initial !== null && name !== null ? member(initial, name) : null;
|
|
167
|
+
}
|
|
168
|
+
function cached(cache, variable, resolve) {
|
|
169
|
+
if (!cache.has(variable)) {
|
|
170
|
+
cache.set(variable, null);
|
|
171
|
+
cache.set(variable, resolve());
|
|
172
|
+
}
|
|
173
|
+
return cache.get(variable) ?? null;
|
|
174
|
+
}
|
|
175
|
+
function importBinding(definition) {
|
|
176
|
+
const declaration = definition.parent;
|
|
177
|
+
const specifier = definition.node;
|
|
178
|
+
if (definition.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const source = declaration.source.value;
|
|
182
|
+
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
183
|
+
return { source, name: "*" };
|
|
184
|
+
}
|
|
185
|
+
if (specifier.type === "ImportDefaultSpecifier") {
|
|
186
|
+
return { source, name: "default" };
|
|
187
|
+
}
|
|
188
|
+
return specifier.type === "ImportSpecifier"
|
|
189
|
+
? {
|
|
190
|
+
source,
|
|
191
|
+
name: moduleExportName(specifier.imported),
|
|
192
|
+
}
|
|
193
|
+
: null;
|
|
194
|
+
}
|
|
195
|
+
export function importedBinding(sourceCode, node) {
|
|
196
|
+
const value = unwrapExpression(node);
|
|
197
|
+
const dynamicSource = dynamicImportSource(value);
|
|
198
|
+
if (dynamicSource !== null) {
|
|
199
|
+
return { source: dynamicSource, name: "*" };
|
|
200
|
+
}
|
|
201
|
+
if (value.type === "MemberExpression") {
|
|
202
|
+
return memberBinding(importedBinding(sourceCode, value.object), staticPropertyName(value));
|
|
203
|
+
}
|
|
204
|
+
const variable = variableFor(sourceCode, value);
|
|
205
|
+
const definition = variable?.defs[0];
|
|
206
|
+
if (variable === null || definition === undefined) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return cached(bindingCache, variable, () => {
|
|
210
|
+
const alias = constAlias(definition);
|
|
211
|
+
return alias === null
|
|
212
|
+
? importBinding(definition)
|
|
213
|
+
: aliasTarget(alias, (init) => importedBinding(sourceCode, init), memberBinding);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function bindingReference(binding) {
|
|
217
|
+
const defaultExport = defaultImports.get(binding.source);
|
|
218
|
+
if (binding.name === "default") {
|
|
219
|
+
return defaultExport ? { module: binding.source, name: defaultExport } : null;
|
|
220
|
+
}
|
|
221
|
+
return defaultExport !== undefined &&
|
|
222
|
+
testFunctions.has(defaultExport) &&
|
|
223
|
+
testModifiers.has(binding.name)
|
|
224
|
+
? { module: binding.source, name: defaultExport }
|
|
225
|
+
: { module: binding.source, name: binding.name };
|
|
226
|
+
}
|
|
227
|
+
function memberReference(object, name) {
|
|
228
|
+
if (object.name === "*") {
|
|
229
|
+
return bindingReference({ source: object.module, name });
|
|
230
|
+
}
|
|
231
|
+
if (testFunctions.has(object.name) && (testModifiers.has(name) || testFactories.has(name))) {
|
|
232
|
+
return object;
|
|
233
|
+
}
|
|
234
|
+
if (object.module === "@effect/vitest" &&
|
|
235
|
+
["test", "it"].includes(object.name) &&
|
|
236
|
+
effectModifiers.has(name)) {
|
|
237
|
+
return object;
|
|
238
|
+
}
|
|
239
|
+
return object.name === "test" && testFunctions.has(name) ? { module: object.module, name } : null;
|
|
240
|
+
}
|
|
241
|
+
export function apiReference(sourceCode, node) {
|
|
242
|
+
const value = unwrapExpression(node);
|
|
243
|
+
if (value.type === "Identifier") {
|
|
244
|
+
const variable = variableFor(sourceCode, value);
|
|
95
245
|
if (!variable?.defs.length) {
|
|
96
|
-
return testFunctions.has(
|
|
97
|
-
? { module: "global", name:
|
|
246
|
+
return testFunctions.has(value.name) || value.name === "expect"
|
|
247
|
+
? { module: "global", name: value.name }
|
|
98
248
|
: null;
|
|
99
249
|
}
|
|
100
250
|
const [definition] = variable.defs;
|
|
101
|
-
if (
|
|
251
|
+
if (definition === undefined || variable.defs.length !== 1) {
|
|
102
252
|
return null;
|
|
103
253
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const module = declaration.source.value;
|
|
109
|
-
if (specifier.type === "ImportDefaultSpecifier" ||
|
|
110
|
-
(specifier.type === "ImportSpecifier" &&
|
|
111
|
-
specifier.imported.type === "Identifier" &&
|
|
112
|
-
specifier.imported.name === "default")) {
|
|
113
|
-
const name = defaultImports.get(module);
|
|
114
|
-
return name ? { module, name } : null;
|
|
115
|
-
}
|
|
116
|
-
if (specifier.type === "ImportNamespaceSpecifier") {
|
|
117
|
-
return { module, name: "*" };
|
|
118
|
-
}
|
|
119
|
-
if (specifier.type === "ImportSpecifier") {
|
|
120
|
-
return {
|
|
121
|
-
module,
|
|
122
|
-
name: specifier.imported.type === "Identifier"
|
|
123
|
-
? specifier.imported.name
|
|
124
|
-
: specifier.imported.value,
|
|
125
|
-
};
|
|
254
|
+
return cached(referenceCache, variable, () => {
|
|
255
|
+
const alias = constAlias(definition);
|
|
256
|
+
if (alias !== null) {
|
|
257
|
+
return aliasTarget(alias, (init) => apiReference(sourceCode, init), memberReference);
|
|
126
258
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return apiReference(sourceCode, specifier.init, seen);
|
|
136
|
-
}
|
|
137
|
-
return null;
|
|
259
|
+
const binding = importBinding(definition);
|
|
260
|
+
return binding && bindingReference(binding);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (value.type === "MemberExpression") {
|
|
264
|
+
const object = apiReference(sourceCode, value.object);
|
|
265
|
+
const name = staticPropertyName(value);
|
|
266
|
+
return object && name !== null ? memberReference(object, name) : null;
|
|
138
267
|
}
|
|
139
|
-
if (
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
if (!object || !name) {
|
|
268
|
+
if (value.type === "CallExpression") {
|
|
269
|
+
const callee = unwrapExpression(value.callee);
|
|
270
|
+
if (callee.type !== "MemberExpression") {
|
|
143
271
|
return null;
|
|
144
272
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
if (testFunctions.has(object.name) && (testModifiers.has(name) || testFactories.has(name))) {
|
|
149
|
-
return object;
|
|
150
|
-
}
|
|
151
|
-
if (object.module === "@effect/vitest" &&
|
|
152
|
-
["test", "it"].includes(object.name) &&
|
|
153
|
-
effectModifiers.has(name)) {
|
|
154
|
-
return object;
|
|
155
|
-
}
|
|
156
|
-
if (object.name === "test" && testFunctions.has(name)) {
|
|
157
|
-
return { module: object.module, name };
|
|
158
|
-
}
|
|
159
|
-
return null;
|
|
160
|
-
}
|
|
161
|
-
if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
|
|
162
|
-
const name = propertyName(node.callee);
|
|
163
|
-
const object = apiReference(sourceCode, node.callee.object, seen);
|
|
273
|
+
const name = staticPropertyName(callee);
|
|
274
|
+
const object = apiReference(sourceCode, callee.object);
|
|
164
275
|
if (object && name !== null && testFunctions.has(object.name) && testFactories.has(name)) {
|
|
165
276
|
return object;
|
|
166
277
|
}
|
|
167
|
-
|
|
278
|
+
return object &&
|
|
168
279
|
["test", "it"].includes(object.name) &&
|
|
169
280
|
(isPlaywright(object.module) || ["vitest", "@effect/vitest"].includes(object.module)) &&
|
|
170
|
-
name === "extend"
|
|
171
|
-
|
|
172
|
-
|
|
281
|
+
name === "extend"
|
|
282
|
+
? object
|
|
283
|
+
: null;
|
|
173
284
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
285
|
+
if (value.type === "TaggedTemplateExpression") {
|
|
286
|
+
const tag = unwrapExpression(value.tag);
|
|
287
|
+
return tag.type === "MemberExpression" && staticPropertyName(tag) === "each"
|
|
288
|
+
? apiReference(sourceCode, tag.object)
|
|
289
|
+
: null;
|
|
178
290
|
}
|
|
179
|
-
|
|
291
|
+
const dynamicSource = dynamicImportSource(value);
|
|
292
|
+
return dynamicSource === null ? null : { module: dynamicSource, name: "*" };
|
|
180
293
|
}
|
|
181
294
|
export function testContext(sourceCode, node) {
|
|
182
295
|
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
@@ -202,50 +315,59 @@ export function projectPath(cwd, file) {
|
|
|
202
315
|
export function matchesAny(path, patterns = []) {
|
|
203
316
|
return patterns.some((pattern) => matchesGlob(path, pattern));
|
|
204
317
|
}
|
|
205
|
-
const
|
|
318
|
+
const nodeGlobalObjects = ["globalThis", "global"];
|
|
319
|
+
const globalObjects = [...nodeGlobalObjects, "window", "self"];
|
|
206
320
|
function isFunctionNode(node) {
|
|
207
321
|
return (node.type === "ArrowFunctionExpression" ||
|
|
208
322
|
node.type === "FunctionExpression" ||
|
|
209
323
|
node.type === "FunctionDeclaration");
|
|
210
324
|
}
|
|
211
|
-
|
|
212
|
-
|
|
325
|
+
function isGlobalThrough(sourceCode, node, name, objects) {
|
|
326
|
+
const value = node ? unwrapExpression(node) : null;
|
|
327
|
+
if (isGlobal(sourceCode, value, name)) {
|
|
213
328
|
return true;
|
|
214
329
|
}
|
|
215
|
-
return (
|
|
216
|
-
|
|
217
|
-
|
|
330
|
+
return (value?.type === "MemberExpression" &&
|
|
331
|
+
staticPropertyName(value) === name &&
|
|
332
|
+
objects.some((object) => isGlobal(sourceCode, unwrapExpression(value.object), object)));
|
|
333
|
+
}
|
|
334
|
+
export function isGlobalValue(sourceCode, node, name) {
|
|
335
|
+
return isGlobalThrough(sourceCode, node, name, globalObjects);
|
|
336
|
+
}
|
|
337
|
+
export function isNodeGlobalValue(sourceCode, node, name) {
|
|
338
|
+
return isGlobalThrough(sourceCode, node, name, nodeGlobalObjects);
|
|
218
339
|
}
|
|
219
340
|
function returns(callback, value) {
|
|
220
341
|
const body = callback.body;
|
|
221
|
-
if (body === value) {
|
|
222
|
-
return true;
|
|
223
|
-
}
|
|
224
342
|
if (body?.type !== "BlockStatement") {
|
|
225
|
-
return
|
|
343
|
+
return body !== null && unwrapExpression(body) === value;
|
|
226
344
|
}
|
|
227
345
|
const [statement] = body.body;
|
|
228
|
-
return (body.body.length === 1 &&
|
|
346
|
+
return (body.body.length === 1 &&
|
|
347
|
+
statement?.type === "ReturnStatement" &&
|
|
348
|
+
statement.argument !== null &&
|
|
349
|
+
unwrapExpression(statement.argument) === value);
|
|
229
350
|
}
|
|
230
351
|
function isProvider(callback, value) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
352
|
+
const provider = outermostExpression(callback);
|
|
353
|
+
const holder = provider.parent;
|
|
354
|
+
if (holder?.type === "AssignmentPattern") {
|
|
355
|
+
return holder.right === provider;
|
|
234
356
|
}
|
|
235
357
|
if (!returns(callback, value)) {
|
|
236
358
|
return false;
|
|
237
359
|
}
|
|
238
|
-
return ((holder
|
|
239
|
-
(holder
|
|
240
|
-
holder.right ===
|
|
360
|
+
return ((holder?.type === "Property" && holder.value === provider) ||
|
|
361
|
+
(holder?.type === "LogicalExpression" &&
|
|
362
|
+
holder.right === provider &&
|
|
241
363
|
["??", "||"].includes(holder.operator)));
|
|
242
364
|
}
|
|
243
365
|
export function isInjectionSeam(source) {
|
|
244
366
|
const parent = source.parent;
|
|
245
367
|
if (source.type !== "NewExpression" && source.type !== "CallExpression") {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
368
|
+
const callee = outermostExpression(source);
|
|
369
|
+
const call = callee.parent;
|
|
370
|
+
return call?.type === "CallExpression" && call.callee === callee ? isInjectionSeam(call) : true;
|
|
249
371
|
}
|
|
250
372
|
let child = source;
|
|
251
373
|
for (let ancestor = parent; ancestor; child = ancestor, ancestor = ancestor.parent) {
|
|
@@ -271,60 +393,8 @@ export function isShortCircuited(call) {
|
|
|
271
393
|
}
|
|
272
394
|
return false;
|
|
273
395
|
}
|
|
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
396
|
export function isEffectModule(sourceCode, node, module) {
|
|
312
397
|
const binding = importedBinding(sourceCode, node);
|
|
313
398
|
return ((binding?.source === "effect" && binding.name === module) ||
|
|
314
399
|
(binding?.source === `effect/${module}` && binding.name === "*"));
|
|
315
400
|
}
|
|
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
|
-
}
|