@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 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.
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
- 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,5 +1,73 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
- import { propertyName, isGlobalValue, isInjectionSeam } from "../shared/index.js";
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 check(node, date) {
15
- if (isGlobalValue(sourceCode, date, "Date") && !isInjectionSeam(node)) {
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 (propertyName(node) === "now") {
22
- check(node, node.object);
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
- check(node, node.callee);
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
- check(node, node.callee);
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 { 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,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 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 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 {};
@@ -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 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
+ 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(node.name) || node.name === "expect"
97
- ? { module: "global", name: node.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 (seen.has(variable) || definition === undefined || variable.defs.length !== 1) {
251
+ if (definition === undefined || variable.defs.length !== 1) {
102
252
  return null;
103
253
  }
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;
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
- 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;
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 (node.type === "MemberExpression") {
140
- const object = apiReference(sourceCode, node.object, seen);
141
- const name = propertyName(node);
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
- 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);
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
- if (object &&
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
- return object;
172
- }
281
+ name === "extend"
282
+ ? object
283
+ : null;
173
284
  }
174
- if (node.type === "TaggedTemplateExpression" &&
175
- node.tag.type === "MemberExpression" &&
176
- propertyName(node.tag) === "each") {
177
- return apiReference(sourceCode, node.tag.object, seen);
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
- return null;
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 globalObjects = ["globalThis", "global", "window", "self"];
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
- export function isGlobalValue(sourceCode, node, name) {
212
- if (isGlobal(sourceCode, node, name)) {
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 (node?.type === "MemberExpression" &&
216
- propertyName(node) === name &&
217
- globalObjects.some((object) => isGlobal(sourceCode, node.object, object)));
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 false;
343
+ return body !== null && unwrapExpression(body) === value;
226
344
  }
227
345
  const [statement] = body.body;
228
- return (body.body.length === 1 && statement?.type === "ReturnStatement" && statement.argument === value);
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 holder = callback.parent;
232
- if (holder.type === "AssignmentPattern") {
233
- return holder.right === callback;
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.type === "Property" && holder.value === callback) ||
239
- (holder.type === "LogicalExpression" &&
240
- holder.right === callback &&
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
- return parent?.type === "CallExpression" && parent.callee === source
247
- ? isInjectionSeam(parent)
248
- : true;
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jameswyse/oxc-config",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Opinionated Oxlint and Oxfmt configs with custom rules for TypeScript and JavaScript projects",
5
5
  "keywords": [
6
6
  "config",