@jameswyse/oxc-config 0.3.0 → 0.5.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 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 destructuring `env` from either, outside the owners, config files and scripts. It doesn't check that the owner validates what it reads.
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.
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, literal dynamic import or `require`, including through a loader made with `createRequire` from `node:module` or `module` or a `const` copy of a loader, 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, a `require` function the file defines itself, a loader variable that's reassigned, and a `process` property on `window` or `self` pass. It doesn't check that the owner validates what it reads.
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` or `module`, whether stored, copied to another `const` or called directly as `createRequire(import.meta.url)("…")`, that matches a `deny` glob. A loader variable that's reassigned doesn't count. 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 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`. A helper reports where it's called, where it's passed or stored uncalled, such as `dates.filter(isPast)` or `{ check: isToday }`, and where it's exported by local name, such as `export { isPast }`. A `date-fns/fp` helper reports once at the end of its call chain, so the partial application in `values.filter(isMatch("yyyy"))` reports too. A `const` alias reports where it's used. Helpers count whether they're imported by name, through a namespace, from a `date-fns/<name>` subpath, by literal dynamic import or by `require`. 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`, type positions such as `typeof isPast`, type-only imports and exports, and re-exports of `TZDate`, `TZDateMini`, other date-fns helpers or types pass. Parameter defaults, such as `check = isPast`, uncalled `Date.now`, provider functions that only return the read, including through `.call`, `.apply` or `.bind`, write-only uses of a helper's name on a module object, such as `d.isPast = check`, `[d.isPast] = values`, `for (d.isPast of values)`, `delete d.isPast` and `delete d?.isPast`, 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
- specifierName(specifier.imported) === "remote") {
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 (specifierName(specifier.local) === "remote") {
76
+ if (moduleExportName(specifier.local) === "remote") {
80
77
  context.report({ node: specifier, messageId: "remote" });
81
78
  }
82
79
  }
@@ -1,6 +1,6 @@
1
1
  import { dirname, resolve as resolvePath } from "node:path";
2
2
  import { defineRule } from "@oxlint/plugins";
3
- import { importedBinding, isGlobal, matchesAny, projectPath, variableFor, } from "../shared/index.js";
3
+ import { isRequire, matchesAny, projectPath } from "../shared/index.js";
4
4
  function isStringList(value) {
5
5
  return Array.isArray(value) && value.every((item) => typeof item === "string");
6
6
  }
@@ -29,17 +29,6 @@ function parseSettings(options) {
29
29
  }
30
30
  return { root: option.root, boundaries: option.boundaries.map(parseBoundary) };
31
31
  }
32
- const nodeModuleSources = new Set(["module", "node:module"]);
33
- function isRequire(sourceCode, callee) {
34
- if (isGlobal(sourceCode, callee, "require")) {
35
- return true;
36
- }
37
- const definition = variableFor(sourceCode, callee)?.defs[0];
38
- const init = definition?.node.type === "VariableDeclarator" ? definition.node.init : null;
39
- const factory = init?.type === "CallExpression" ? init.callee : null;
40
- const binding = factory === null ? null : importedBinding(sourceCode, factory);
41
- return (binding !== null && nodeModuleSources.has(binding.source) && binding.name === "createRequire");
42
- }
43
32
  export const importBoundaries = defineRule({
44
33
  meta: {
45
34
  type: "problem",
@@ -1,5 +1,127 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { propertyName, isGlobalValue, isInjectionSeam } from "../shared/index.js";
2
+ import { importedBinding, isGlobalValue, isInjectedValue, isInjectionSeam, moduleExportName, outermostExpression, staticPropertyName, unwrapExpression, variableFor, } 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
+ "date-fns/fp/isMatch",
30
+ "date-fns/fp/isMatchWithOptions",
31
+ ]);
32
+ function helperPath(source, name) {
33
+ return name === "default" || source.endsWith(`/${name}`) ? source : `${source}/${name}`;
34
+ }
35
+ function isClockHelper(sourceCode, node) {
36
+ const binding = importedBinding(sourceCode, node);
37
+ return binding !== null && clockHelpers.has(helperPath(binding.source, binding.name));
38
+ }
39
+ function exposesClockHelper(source) {
40
+ return [...clockHelpers].some((helper) => helperPath(source, helper.slice(helper.lastIndexOf("/") + 1)) === helper);
41
+ }
42
+ function isTypeImport(definition) {
43
+ const declaration = definition.parent;
44
+ const specifier = definition.node;
45
+ return (declaration?.type === "ImportDeclaration" &&
46
+ (declaration.importKind === "type" ||
47
+ (specifier.type === "ImportSpecifier" && specifier.importKind === "type")));
48
+ }
49
+ function isTypePosition(node) {
50
+ const parent = node.parent;
51
+ const declaration = parent?.parent;
52
+ return (parent?.type === "TSTypeQuery" ||
53
+ parent?.type === "TSQualifiedName" ||
54
+ (parent?.type === "ExportSpecifier" &&
55
+ (parent.exportKind === "type" ||
56
+ (declaration?.type === "ExportNamedDeclaration" && declaration.exportKind === "type"))));
57
+ }
58
+ function isValueReference(sourceCode, node) {
59
+ const variable = variableFor(sourceCode, node);
60
+ const definition = variable?.defs[0];
61
+ return (variable !== null &&
62
+ definition !== undefined &&
63
+ !isTypeImport(definition) &&
64
+ !isTypePosition(node) &&
65
+ variable.references.some((reference) => reference.identifier === node && reference.isRead()));
66
+ }
67
+ const functionMethods = new Set(["call", "apply", "bind"]);
68
+ function helperUse(node) {
69
+ const value = outermostExpression(node);
70
+ const parent = value.parent;
71
+ if (parent?.type === "CallExpression" && parent.callee === value) {
72
+ return helperUse(parent);
73
+ }
74
+ if (parent?.type === "MemberExpression" &&
75
+ parent.object === value &&
76
+ functionMethods.has(staticPropertyName(parent) ?? "")) {
77
+ const use = helperUse(parent);
78
+ return use === parent ? node : use;
79
+ }
80
+ return node;
81
+ }
82
+ function isWriteTarget(node) {
83
+ const expression = outermostExpression(node);
84
+ const target = expression.parent?.type === "ChainExpression"
85
+ ? outermostExpression(expression.parent)
86
+ : expression;
87
+ const parent = target.parent;
88
+ return ((parent?.type === "AssignmentExpression" &&
89
+ parent.left === target &&
90
+ parent.operator === "=") ||
91
+ ((parent?.type === "ForOfStatement" || parent?.type === "ForInStatement") &&
92
+ parent.left === target) ||
93
+ (parent?.type === "UnaryExpression" && parent.operator === "delete") ||
94
+ (parent?.type === "AssignmentPattern" && parent.left === target) ||
95
+ (parent?.type === "Property" &&
96
+ parent.value === target &&
97
+ parent.parent.type === "ObjectPattern") ||
98
+ parent?.type === "ArrayPattern" ||
99
+ parent?.type === "RestElement");
100
+ }
101
+ function isFollowedAlias(node) {
102
+ const value = outermostExpression(node);
103
+ const declarator = value.parent;
104
+ const declaration = declarator?.parent;
105
+ return (declarator?.type === "VariableDeclarator" &&
106
+ declarator.init === value &&
107
+ declarator.id.type === "Identifier" &&
108
+ declaration?.type === "VariableDeclaration" &&
109
+ declaration.kind === "const" &&
110
+ declaration.parent.type !== "ExportNamedDeclaration");
111
+ }
112
+ function isTimeZoneDate(sourceCode, node) {
113
+ const binding = importedBinding(sourceCode, node);
114
+ return binding !== null && timeZoneModules.has(binding.source) && timeZoneDates.has(binding.name);
115
+ }
116
+ function isCurrentTimeZoneDate(sourceCode, node) {
117
+ const callee = unwrapExpression(node.callee);
118
+ const [zone] = node.arguments;
119
+ return (callee.type === "MemberExpression" &&
120
+ staticPropertyName(callee) === "tz" &&
121
+ isTimeZoneDate(sourceCode, callee.object) &&
122
+ node.arguments.length === 1 &&
123
+ zone?.type !== "SpreadElement");
124
+ }
3
125
  export const noAmbientClock = defineRule({
4
126
  meta: {
5
127
  type: "suggestion",
@@ -11,24 +133,59 @@ export const noAmbientClock = defineRule({
11
133
  },
12
134
  create(context) {
13
135
  const sourceCode = context.sourceCode;
14
- function check(node, date) {
15
- if (isGlobalValue(sourceCode, date, "Date") && !isInjectionSeam(node)) {
136
+ function report(node) {
137
+ if (!isInjectionSeam(node)) {
16
138
  context.report({ node, messageId: "clock" });
17
139
  }
18
140
  }
141
+ function reportHelper(reference) {
142
+ const use = helperUse(reference);
143
+ if ((use !== reference || !isFollowedAlias(reference)) && !isInjectedValue(use)) {
144
+ context.report({ node: use, messageId: "clock" });
145
+ }
146
+ }
19
147
  return {
148
+ Identifier(node) {
149
+ if (isClockHelper(sourceCode, node) && isValueReference(sourceCode, node)) {
150
+ reportHelper(node);
151
+ }
152
+ },
20
153
  MemberExpression(node) {
21
- if (propertyName(node) === "now") {
22
- check(node, node.object);
154
+ if (staticPropertyName(node) === "now" && isGlobalValue(sourceCode, node.object, "Date")) {
155
+ report(node);
156
+ }
157
+ if (isClockHelper(sourceCode, node) && !isWriteTarget(node)) {
158
+ reportHelper(node);
23
159
  }
24
160
  },
25
161
  NewExpression(node) {
26
- if (node.arguments.length === 0) {
27
- check(node, node.callee);
162
+ if (node.arguments.length === 0 &&
163
+ (isGlobalValue(sourceCode, node.callee, "Date") ||
164
+ isTimeZoneDate(sourceCode, node.callee))) {
165
+ report(node);
28
166
  }
29
167
  },
30
168
  CallExpression(node) {
31
- check(node, node.callee);
169
+ if (isGlobalValue(sourceCode, node.callee, "Date") ||
170
+ isCurrentTimeZoneDate(sourceCode, node)) {
171
+ report(node);
172
+ }
173
+ },
174
+ ExportNamedDeclaration(node) {
175
+ if (!node.source || node.exportKind === "type") {
176
+ return;
177
+ }
178
+ for (const specifier of node.specifiers) {
179
+ if (specifier.exportKind !== "type" &&
180
+ clockHelpers.has(helperPath(node.source.value, moduleExportName(specifier.local)))) {
181
+ context.report({ node: specifier, messageId: "clock" });
182
+ }
183
+ }
184
+ },
185
+ ExportAllDeclaration(node) {
186
+ if (node.exportKind !== "type" && exposesClockHelper(node.source.value)) {
187
+ context.report({ node, messageId: "clock" });
188
+ }
32
189
  },
33
190
  };
34
191
  },
@@ -1,10 +1,34 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { isGlobal, propertyName } from "../shared/index.js";
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
- return (node?.type === "MetaProperty" && node.meta.name === "import" && node.property.name === "meta");
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
- return isGlobal(sourceCode, node, "process") || isImportMeta(node);
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 (propertyName(node) === "env" && isEnvironmentSource(sourceCode, node.object)) {
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" && propertyName(property) === "env")) {
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, unwrapExpression(node));
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) {
@@ -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,21 @@ 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 apiReference(sourceCode: SourceCode, node: ESTree.Node, seen?: Set<Variable>): ApiReference | null;
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 isRequire(sourceCode: SourceCode, node: ESTree.Node): boolean;
31
+ export declare function apiReference(sourceCode: SourceCode, node: ESTree.Node): ApiReference | null;
21
32
  export declare function testContext(sourceCode: SourceCode, node: ESTree.Node): TestContext | null;
22
33
  export declare function projectPath(cwd: string, file: string): string;
23
34
  export declare function matchesAny(path: string, patterns?: readonly string[]): boolean;
24
35
  export declare function isGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
36
+ export declare function isNodeGlobalValue(sourceCode: SourceCode, node: ESTree.Node | null | undefined, name: string): boolean;
37
+ export declare function isInjectedValue(source: ESTree.Node): boolean;
25
38
  export declare function isInjectionSeam(source: ESTree.Node): boolean;
26
39
  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
40
  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
41
  export {};
@@ -89,92 +89,246 @@ export function propertyName(node) {
89
89
  export function isPlaywright(module) {
90
90
  return module === "@playwright/test" || module === "playwright/test";
91
91
  }
92
- export function apiReference(sourceCode, node, seen = new Set()) {
93
- if (node.type === "Identifier") {
94
- const variable = variableFor(sourceCode, node);
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
+ const nodeModuleSources = new Set(["module", "node:module"]);
176
+ const loaderCache = new WeakMap();
177
+ function unchangedInit(variable) {
178
+ const [definition, ...otherDefinitions] = variable.defs;
179
+ const declarator = definition?.node;
180
+ return otherDefinitions.length === 0 &&
181
+ definition?.type === "Variable" &&
182
+ declarator?.type === "VariableDeclarator" &&
183
+ declarator.id === definition.name &&
184
+ variable.references.every((reference) => reference.init || !reference.isWrite())
185
+ ? declarator.init
186
+ : null;
187
+ }
188
+ function isRequireFactory(node, resolve) {
189
+ const binding = node.type === "CallExpression" ? resolve(node.callee) : null;
190
+ return (binding !== null && nodeModuleSources.has(binding.source) && binding.name === "createRequire");
191
+ }
192
+ function isLoader(sourceCode, node, resolve) {
193
+ const callee = unwrapExpression(node);
194
+ if (isGlobal(sourceCode, callee, "require") || isRequireFactory(callee, resolve)) {
195
+ return true;
196
+ }
197
+ const variable = variableFor(sourceCode, callee);
198
+ return (variable !== null &&
199
+ cached(loaderCache, variable, () => {
200
+ const init = unchangedInit(variable);
201
+ return init !== null && isLoader(sourceCode, init, resolve);
202
+ }) === true);
203
+ }
204
+ function loadedSource(sourceCode, node, resolve) {
205
+ const required = node.type === "CallExpression" &&
206
+ node.arguments.length === 1 &&
207
+ isLoader(sourceCode, node.callee, resolve);
208
+ return required ? staticString(node.arguments[0]) : dynamicImportSource(node);
209
+ }
210
+ function importBinding(definition) {
211
+ const declaration = definition.parent;
212
+ const specifier = definition.node;
213
+ if (definition.type !== "ImportBinding" || declaration?.type !== "ImportDeclaration") {
214
+ return null;
215
+ }
216
+ const source = declaration.source.value;
217
+ if (specifier.type === "ImportNamespaceSpecifier") {
218
+ return { source, name: "*" };
219
+ }
220
+ if (specifier.type === "ImportDefaultSpecifier") {
221
+ return { source, name: "default" };
222
+ }
223
+ return specifier.type === "ImportSpecifier"
224
+ ? {
225
+ source,
226
+ name: moduleExportName(specifier.imported),
227
+ }
228
+ : null;
229
+ }
230
+ export function importedBinding(sourceCode, node) {
231
+ const value = unwrapExpression(node);
232
+ const source = loadedSource(sourceCode, value, (callee) => importedBinding(sourceCode, callee));
233
+ if (source !== null) {
234
+ return { source, name: "*" };
235
+ }
236
+ if (value.type === "MemberExpression") {
237
+ return memberBinding(importedBinding(sourceCode, value.object), staticPropertyName(value));
238
+ }
239
+ const variable = variableFor(sourceCode, value);
240
+ const definition = variable?.defs[0];
241
+ if (variable === null || definition === undefined) {
242
+ return null;
243
+ }
244
+ return cached(bindingCache, variable, () => {
245
+ const alias = constAlias(definition);
246
+ return alias === null
247
+ ? importBinding(definition)
248
+ : aliasTarget(alias, (init) => importedBinding(sourceCode, init), memberBinding);
249
+ });
250
+ }
251
+ export function isRequire(sourceCode, node) {
252
+ return isLoader(sourceCode, node, (callee) => importedBinding(sourceCode, callee));
253
+ }
254
+ function bindingReference(binding) {
255
+ const defaultExport = defaultImports.get(binding.source);
256
+ if (binding.name === "default") {
257
+ return defaultExport ? { module: binding.source, name: defaultExport } : null;
258
+ }
259
+ return defaultExport !== undefined &&
260
+ testFunctions.has(defaultExport) &&
261
+ testModifiers.has(binding.name)
262
+ ? { module: binding.source, name: defaultExport }
263
+ : { module: binding.source, name: binding.name };
264
+ }
265
+ function memberReference(object, name) {
266
+ if (object.name === "*") {
267
+ return bindingReference({ source: object.module, name });
268
+ }
269
+ if (testFunctions.has(object.name) && (testModifiers.has(name) || testFactories.has(name))) {
270
+ return object;
271
+ }
272
+ if (object.module === "@effect/vitest" &&
273
+ ["test", "it"].includes(object.name) &&
274
+ effectModifiers.has(name)) {
275
+ return object;
276
+ }
277
+ return object.name === "test" && testFunctions.has(name) ? { module: object.module, name } : null;
278
+ }
279
+ export function apiReference(sourceCode, node) {
280
+ const value = unwrapExpression(node);
281
+ const source = loadedSource(sourceCode, value, (callee) => importedBinding(sourceCode, callee));
282
+ if (source !== null) {
283
+ return { module: source, name: "*" };
284
+ }
285
+ if (value.type === "Identifier") {
286
+ const variable = variableFor(sourceCode, value);
95
287
  if (!variable?.defs.length) {
96
- return testFunctions.has(node.name) || node.name === "expect"
97
- ? { module: "global", name: node.name }
288
+ return testFunctions.has(value.name) || value.name === "expect"
289
+ ? { module: "global", name: value.name }
98
290
  : null;
99
291
  }
100
292
  const [definition] = variable.defs;
101
- if (seen.has(variable) || definition === undefined || variable.defs.length !== 1) {
293
+ if (definition === undefined || variable.defs.length !== 1) {
102
294
  return null;
103
295
  }
104
- seen.add(variable);
105
- const specifier = definition.node;
106
- const declaration = definition.parent;
107
- if (definition.type === "ImportBinding" && declaration?.type === "ImportDeclaration") {
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;
296
+ return cached(referenceCache, variable, () => {
297
+ const alias = constAlias(definition);
298
+ if (alias !== null) {
299
+ return aliasTarget(alias, (init) => apiReference(sourceCode, init), memberReference);
115
300
  }
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
- };
126
- }
127
- return null;
128
- }
129
- if (definition.type === "Variable" &&
130
- declaration?.type === "VariableDeclaration" &&
131
- declaration.kind === "const" &&
132
- specifier.type === "VariableDeclarator" &&
133
- specifier.id.type === "Identifier" &&
134
- specifier.init) {
135
- return apiReference(sourceCode, specifier.init, seen);
136
- }
137
- return null;
301
+ const binding = importBinding(definition);
302
+ return binding && bindingReference(binding);
303
+ });
304
+ }
305
+ if (value.type === "MemberExpression") {
306
+ const object = apiReference(sourceCode, value.object);
307
+ const name = staticPropertyName(value);
308
+ return object && name !== null ? memberReference(object, name) : null;
138
309
  }
139
- if (node.type === "MemberExpression") {
140
- const object = apiReference(sourceCode, node.object, seen);
141
- const name = propertyName(node);
142
- if (!object || !name) {
310
+ if (value.type === "CallExpression") {
311
+ const callee = unwrapExpression(value.callee);
312
+ if (callee.type !== "MemberExpression") {
143
313
  return null;
144
314
  }
145
- if (object.name === "*") {
146
- return { module: object.module, name };
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);
315
+ const name = staticPropertyName(callee);
316
+ const object = apiReference(sourceCode, callee.object);
164
317
  if (object && name !== null && testFunctions.has(object.name) && testFactories.has(name)) {
165
318
  return object;
166
319
  }
167
- if (object &&
320
+ return object &&
168
321
  ["test", "it"].includes(object.name) &&
169
322
  (isPlaywright(object.module) || ["vitest", "@effect/vitest"].includes(object.module)) &&
170
- name === "extend") {
171
- return object;
172
- }
323
+ name === "extend"
324
+ ? object
325
+ : null;
173
326
  }
174
- if (node.type === "TaggedTemplateExpression" &&
175
- node.tag.type === "MemberExpression" &&
176
- propertyName(node.tag) === "each") {
177
- return apiReference(sourceCode, node.tag.object, seen);
327
+ if (value.type === "TaggedTemplateExpression") {
328
+ const tag = unwrapExpression(value.tag);
329
+ return tag.type === "MemberExpression" && staticPropertyName(tag) === "each"
330
+ ? apiReference(sourceCode, tag.object)
331
+ : null;
178
332
  }
179
333
  return null;
180
334
  }
@@ -202,53 +356,56 @@ export function projectPath(cwd, file) {
202
356
  export function matchesAny(path, patterns = []) {
203
357
  return patterns.some((pattern) => matchesGlob(path, pattern));
204
358
  }
205
- const globalObjects = ["globalThis", "global", "window", "self"];
359
+ const nodeGlobalObjects = ["globalThis", "global"];
360
+ const globalObjects = [...nodeGlobalObjects, "window", "self"];
206
361
  function isFunctionNode(node) {
207
362
  return (node.type === "ArrowFunctionExpression" ||
208
363
  node.type === "FunctionExpression" ||
209
364
  node.type === "FunctionDeclaration");
210
365
  }
211
- export function isGlobalValue(sourceCode, node, name) {
212
- if (isGlobal(sourceCode, node, name)) {
366
+ function isGlobalThrough(sourceCode, node, name, objects) {
367
+ const value = node ? unwrapExpression(node) : null;
368
+ if (isGlobal(sourceCode, value, name)) {
213
369
  return true;
214
370
  }
215
- return (node?.type === "MemberExpression" &&
216
- propertyName(node) === name &&
217
- globalObjects.some((object) => isGlobal(sourceCode, node.object, object)));
371
+ return (value?.type === "MemberExpression" &&
372
+ staticPropertyName(value) === name &&
373
+ objects.some((object) => isGlobal(sourceCode, unwrapExpression(value.object), object)));
374
+ }
375
+ export function isGlobalValue(sourceCode, node, name) {
376
+ return isGlobalThrough(sourceCode, node, name, globalObjects);
377
+ }
378
+ export function isNodeGlobalValue(sourceCode, node, name) {
379
+ return isGlobalThrough(sourceCode, node, name, nodeGlobalObjects);
218
380
  }
219
381
  function returns(callback, value) {
220
382
  const body = callback.body;
221
- if (body === value) {
222
- return true;
223
- }
224
383
  if (body?.type !== "BlockStatement") {
225
- return false;
384
+ return body !== null && unwrapExpression(body) === value;
226
385
  }
227
386
  const [statement] = body.body;
228
- return (body.body.length === 1 && statement?.type === "ReturnStatement" && statement.argument === value);
387
+ return (body.body.length === 1 &&
388
+ statement?.type === "ReturnStatement" &&
389
+ statement.argument !== null &&
390
+ unwrapExpression(statement.argument) === value);
229
391
  }
230
392
  function isProvider(callback, value) {
231
- const holder = callback.parent;
232
- if (holder.type === "AssignmentPattern") {
233
- return holder.right === callback;
393
+ const provider = outermostExpression(callback);
394
+ const holder = provider.parent;
395
+ if (holder?.type === "AssignmentPattern") {
396
+ return holder.right === provider;
234
397
  }
235
398
  if (!returns(callback, value)) {
236
399
  return false;
237
400
  }
238
- return ((holder.type === "Property" && holder.value === callback) ||
239
- (holder.type === "LogicalExpression" &&
240
- holder.right === callback &&
401
+ return ((holder?.type === "Property" && holder.value === provider) ||
402
+ (holder?.type === "LogicalExpression" &&
403
+ holder.right === provider &&
241
404
  ["??", "||"].includes(holder.operator)));
242
405
  }
243
- export function isInjectionSeam(source) {
244
- const parent = source.parent;
245
- if (source.type !== "NewExpression" && source.type !== "CallExpression") {
246
- return parent?.type === "CallExpression" && parent.callee === source
247
- ? isInjectionSeam(parent)
248
- : true;
249
- }
406
+ export function isInjectedValue(source) {
250
407
  let child = source;
251
- for (let ancestor = parent; ancestor; child = ancestor, ancestor = ancestor.parent) {
408
+ for (let ancestor = source.parent; ancestor; child = ancestor, ancestor = ancestor.parent) {
252
409
  if (ancestor.type === "AssignmentPattern") {
253
410
  return ancestor.right === child;
254
411
  }
@@ -258,6 +415,14 @@ export function isInjectionSeam(source) {
258
415
  }
259
416
  return false;
260
417
  }
418
+ export function isInjectionSeam(source) {
419
+ if (source.type !== "NewExpression" && source.type !== "CallExpression") {
420
+ const callee = outermostExpression(source);
421
+ const call = callee.parent;
422
+ return call?.type === "CallExpression" && call.callee === callee ? isInjectionSeam(call) : true;
423
+ }
424
+ return isInjectedValue(source);
425
+ }
261
426
  export function isShortCircuited(call) {
262
427
  let current = call;
263
428
  while (current.type === "CallExpression" || current.type === "MemberExpression") {
@@ -271,60 +436,8 @@ export function isShortCircuited(call) {
271
436
  }
272
437
  return false;
273
438
  }
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
439
  export function isEffectModule(sourceCode, node, module) {
312
440
  const binding = importedBinding(sourceCode, node);
313
441
  return ((binding?.source === "effect" && binding.name === module) ||
314
442
  (binding?.source === `effect/${module}` && binding.name === "*"));
315
443
  }
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jameswyse/oxc-config",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Opinionated Oxlint and Oxfmt configs with custom rules for TypeScript and JavaScript projects",
5
5
  "keywords": [
6
6
  "config",