@jameswyse/oxc-config 0.2.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.
Files changed (52) hide show
  1. package/README.md +30 -186
  2. package/REFERENCE.md +105 -132
  3. package/dist/index.js +26 -0
  4. package/dist/lint.d.ts +1 -0
  5. package/dist/lint.js +124 -4
  6. package/dist/rules/electron-no-remote-module.js +3 -6
  7. package/dist/rules/explicit-api-states.js +7 -1
  8. package/dist/rules/import-boundaries.js +31 -6
  9. package/dist/rules/no-ambient-clock.js +104 -8
  10. package/dist/rules/no-derived-test-expectation.d.ts +1 -0
  11. package/dist/rules/no-derived-test-expectation.js +136 -0
  12. package/dist/rules/no-disabled-tls-verification.d.ts +1 -0
  13. package/dist/rules/no-disabled-tls-verification.js +42 -0
  14. package/dist/rules/no-env-access.js +62 -5
  15. package/dist/rules/no-fixed-test-delay.js +46 -5
  16. package/dist/rules/no-generic-object-guard.d.ts +1 -0
  17. package/dist/rules/no-generic-object-guard.js +279 -0
  18. package/dist/rules/no-get-password-form.d.ts +1 -0
  19. package/dist/rules/no-get-password-form.js +113 -0
  20. package/dist/rules/no-hardcoded-credential.d.ts +1 -0
  21. package/dist/rules/no-hardcoded-credential.js +45 -0
  22. package/dist/rules/no-import-time-effects.d.ts +1 -0
  23. package/dist/rules/no-import-time-effects.js +90 -0
  24. package/dist/rules/no-optional-test-action.d.ts +1 -0
  25. package/dist/rules/no-optional-test-action.js +45 -0
  26. package/dist/rules/no-secret-fallback.d.ts +1 -0
  27. package/dist/rules/no-secret-fallback.js +64 -0
  28. package/dist/rules/no-swallowed-errors.js +131 -16
  29. package/dist/rules/no-test-timeout-overrides.js +22 -9
  30. package/dist/rules/no-unchecked-fetch-response.d.ts +1 -0
  31. package/dist/rules/no-unchecked-fetch-response.js +91 -0
  32. package/dist/rules/no-unchecked-index-search.d.ts +1 -0
  33. package/dist/rules/no-unchecked-index-search.js +41 -0
  34. package/dist/rules/no-uncontrolled-test-clock.d.ts +1 -0
  35. package/dist/rules/no-uncontrolled-test-clock.js +112 -0
  36. package/dist/rules/no-unsettled-promise.d.ts +1 -0
  37. package/dist/rules/no-unsettled-promise.js +64 -0
  38. package/dist/rules/require-suppression-reason.d.ts +1 -0
  39. package/dist/rules/require-suppression-reason.js +32 -0
  40. package/dist/shared/index.d.ts +14 -1
  41. package/dist/shared/index.js +231 -91
  42. package/dist/vendor/anti-slop/effect/rules/no-service-constructor-imports.js +10 -2
  43. package/dist/vendor/anti-slop/rules/no-known-value-widening.js +1 -1
  44. package/dist/vendor/anti-slop/rules/no-module-mocking.js +56 -1
  45. package/dist/vendor/anti-slop/rules/no-runtime-typeof.js +30 -1
  46. package/dist/vendor/anti-slop/rules/no-shape-in-symbol-names.js +28 -1
  47. package/dist/vendor/anti-slop/rules/no-unknown-returns.js +1 -1
  48. package/dist/vendor/anti-slop/rules/no-unsafe-dictionary-type.js +2 -2
  49. package/dist/vendor/anti-slop/shared/dictionary-types.js +72 -89
  50. package/dist/vendor/anti-slop/shared/type-alias-resolution.d.ts +8 -2
  51. package/dist/vendor/anti-slop/shared/type-alias-resolution.js +2 -2
  52. package/package.json +7 -7
@@ -0,0 +1,112 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { importedBinding, isGlobalValue, isPlaywright, staticPropertyName, testContext, unwrapExpression, variableFor, } from "../shared/index.js";
3
+ const fakeClockMethods = new Set(["install", "setFixedTime"]);
4
+ const timerLists = new Set(["apis", "toFake", "doNotFake"]);
5
+ function nameOf(node) {
6
+ const value = unwrapExpression(node);
7
+ return value.type === "Identifier" ? value.name : staticPropertyName(value);
8
+ }
9
+ function fakesDate(options) {
10
+ const value = options === undefined ? undefined : unwrapExpression(options);
11
+ if (value?.type !== "ObjectExpression") {
12
+ return true;
13
+ }
14
+ return value.properties.every((property) => {
15
+ const name = property.type === "Property" ? staticPropertyName(property) : null;
16
+ const list = property.type === "Property" ? unwrapExpression(property.value) : null;
17
+ if (name === null || !timerLists.has(name) || list?.type !== "ArrayExpression") {
18
+ return true;
19
+ }
20
+ const values = list.elements.map((element) => element?.type === "Literal" ? element.value : undefined);
21
+ if (values.length === 0 || values.includes(undefined)) {
22
+ return true;
23
+ }
24
+ return name === "doNotFake" ? !values.includes("Date") : values.includes("Date");
25
+ });
26
+ }
27
+ function isNodeMock(sourceCode, node) {
28
+ const binding = importedBinding(sourceCode, node);
29
+ return nameOf(node) === "mock" || (binding?.source === "node:test" && binding.name === "mock");
30
+ }
31
+ function isLocalFunction(sourceCode, node) {
32
+ const definition = variableFor(sourceCode, node)?.defs[0];
33
+ return definition !== undefined && definition.type !== "ImportBinding";
34
+ }
35
+ function controlsClock(sourceCode, call) {
36
+ const callee = unwrapExpression(call.callee);
37
+ const name = isLocalFunction(sourceCode, callee) ? null : nameOf(callee);
38
+ if (name === "setSystemTime") {
39
+ return true;
40
+ }
41
+ if (name === "useFakeTimers") {
42
+ return fakesDate(call.arguments[0]);
43
+ }
44
+ if (name === null || callee.type !== "MemberExpression") {
45
+ return false;
46
+ }
47
+ const owner = unwrapExpression(callee.object);
48
+ return ((nameOf(owner) === "clock" && fakeClockMethods.has(name)) ||
49
+ (name === "enable" &&
50
+ owner.type === "MemberExpression" &&
51
+ nameOf(owner) === "timers" &&
52
+ isNodeMock(sourceCode, owner.object) &&
53
+ fakesDate(call.arguments[0])));
54
+ }
55
+ export const noUncontrolledTestClock = defineRule({
56
+ meta: {
57
+ type: "problem",
58
+ docs: { description: "Require tests that read the current time to control the clock." },
59
+ schema: [],
60
+ messages: {
61
+ clock: "This test reads the real clock, so its result depends on when it runs. Use a fixed date, or control time with fake timers.",
62
+ },
63
+ },
64
+ create(context) {
65
+ const sourceCode = context.sourceCode;
66
+ const drivesBrowser = sourceCode.ast.body.some((statement) => statement.type === "ImportDeclaration" && isPlaywright(statement.source.value));
67
+ if (drivesBrowser) {
68
+ return {};
69
+ }
70
+ const reads = [];
71
+ const controlledTests = new Set();
72
+ let controlledFile = false;
73
+ function enclosingTest(node) {
74
+ const test = testContext(sourceCode, node);
75
+ return test?.api.name === "test" || test?.api.name === "it" ? test.callback : null;
76
+ }
77
+ return {
78
+ CallExpression(node) {
79
+ if (controlsClock(sourceCode, node)) {
80
+ const test = enclosingTest(node);
81
+ if (test === null) {
82
+ controlledFile = true;
83
+ }
84
+ else {
85
+ controlledTests.add(test);
86
+ }
87
+ }
88
+ if (node.callee.type === "MemberExpression" &&
89
+ staticPropertyName(node.callee) === "now" &&
90
+ isGlobalValue(sourceCode, node.callee.object, "Date")) {
91
+ reads.push(node);
92
+ }
93
+ },
94
+ NewExpression(node) {
95
+ if (node.arguments.length === 0 && isGlobalValue(sourceCode, node.callee, "Date")) {
96
+ reads.push(node);
97
+ }
98
+ },
99
+ "Program:exit"() {
100
+ if (controlledFile) {
101
+ return;
102
+ }
103
+ for (const node of reads) {
104
+ const test = enclosingTest(node);
105
+ if (test === null || !controlledTests.has(test)) {
106
+ context.report({ node, messageId: "clock" });
107
+ }
108
+ }
109
+ },
110
+ };
111
+ },
112
+ });
@@ -0,0 +1 @@
1
+ export declare const noUnsettledPromise: import("@oxlint/plugins").Rule;
@@ -0,0 +1,64 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ import { isGlobal, isShortCircuited } from "../shared/index.js";
3
+ function isOptionallyReached(reference, executor) {
4
+ let child = reference.identifier;
5
+ for (let node = child.parent; node !== null && node !== executor; child = node, node = node.parent) {
6
+ const argument = child;
7
+ if (node.type === "CallExpression" &&
8
+ node.arguments.some((candidate) => candidate === argument) &&
9
+ isShortCircuited(node)) {
10
+ return true;
11
+ }
12
+ }
13
+ return false;
14
+ }
15
+ function doesNothing(executor) {
16
+ const body = executor.body;
17
+ if (body?.type === "BlockStatement") {
18
+ return body.body.length === 0;
19
+ }
20
+ return ((body?.type === "Identifier" && body.name === "undefined") ||
21
+ (body?.type === "UnaryExpression" &&
22
+ body.operator === "void" &&
23
+ body.argument.type === "Literal"));
24
+ }
25
+ function endsByThrowing(executor) {
26
+ return (executor.body?.type === "BlockStatement" && executor.body.body.at(-1)?.type === "ThrowStatement");
27
+ }
28
+ export const noUnsettledPromise = defineRule({
29
+ meta: {
30
+ type: "problem",
31
+ docs: { description: "Require a Promise executor that runs code to settle the promise." },
32
+ schema: [],
33
+ messages: {
34
+ unsettled: "This executor never calls `resolve` or `reject`, so the promise never settles. Return the underlying promise instead, or settle it when the work finishes.",
35
+ optional: "This executor only settles inside an optional call, so the promise never settles when the target is missing. Settle it on both paths.",
36
+ },
37
+ },
38
+ create(context) {
39
+ const sourceCode = context.sourceCode;
40
+ return {
41
+ NewExpression(node) {
42
+ const [executor] = node.arguments;
43
+ if (!isGlobal(sourceCode, node.callee, "Promise") ||
44
+ (executor?.type !== "ArrowFunctionExpression" &&
45
+ executor?.type !== "FunctionExpression") ||
46
+ doesNothing(executor) ||
47
+ endsByThrowing(executor) ||
48
+ executor.params.some((parameter) => parameter.type !== "Identifier")) {
49
+ return;
50
+ }
51
+ const references = sourceCode
52
+ .getDeclaredVariables(executor)
53
+ .filter((variable) => variable.defs.some((definition) => definition.type === "Parameter"))
54
+ .flatMap((variable) => variable.references);
55
+ if (references.length === 0) {
56
+ context.report({ node: executor, messageId: "unsettled" });
57
+ }
58
+ else if (references.every((reference) => isOptionallyReached(reference, executor))) {
59
+ context.report({ node: executor, messageId: "optional" });
60
+ }
61
+ },
62
+ };
63
+ },
64
+ });
@@ -0,0 +1 @@
1
+ export declare const requireSuppressionReason: import("@oxlint/plugins").Rule;
@@ -0,0 +1,32 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ const disableDirective = /^\s*(?:oxlint|eslint)-disable(?:-next-line|-line)?(?=\s|$)(?<rest>.*)$/su;
3
+ export const requireSuppressionReason = defineRule({
4
+ meta: {
5
+ type: "suggestion",
6
+ docs: { description: "Require each lint suppression to name its rules and give a reason." },
7
+ schema: [],
8
+ messages: {
9
+ rule: "Name the rules this directive suppresses, so it can't hide other diagnostics.",
10
+ reason: "Give this suppression a reason after `--` that says why the reported code is correct here.",
11
+ },
12
+ },
13
+ create(context) {
14
+ return {
15
+ Program() {
16
+ for (const comment of context.sourceCode.getAllComments()) {
17
+ const rest = disableDirective.exec(comment.value)?.groups?.["rest"];
18
+ if (rest === undefined) {
19
+ continue;
20
+ }
21
+ const [rules = "", reason = ""] = rest.split(/\s-{2,}(?:\s|$)/u, 2);
22
+ if (rules.trim() === "") {
23
+ context.report({ loc: comment.loc, messageId: "rule" });
24
+ }
25
+ else if (reason.trim() === "") {
26
+ context.report({ loc: comment.loc, messageId: "reason" });
27
+ }
28
+ }
29
+ },
30
+ };
31
+ },
32
+ });
@@ -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,10 +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;
37
+ export declare function isShortCircuited(call: ESTree.CallExpression): boolean;
38
+ export declare function isEffectModule(sourceCode: SourceCode, node: ESTree.Node, module: string): boolean;
26
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: "*" };
254
+ return cached(referenceCache, variable, () => {
255
+ const alias = constAlias(definition);
256
+ if (alias !== null) {
257
+ return aliasTarget(alias, (init) => apiReference(sourceCode, init), memberReference);
118
258
  }
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;
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) {
@@ -258,3 +380,21 @@ export function isInjectionSeam(source) {
258
380
  }
259
381
  return false;
260
382
  }
383
+ export function isShortCircuited(call) {
384
+ let current = call;
385
+ while (current.type === "CallExpression" || current.type === "MemberExpression") {
386
+ if (current.optional) {
387
+ return true;
388
+ }
389
+ current = current.type === "CallExpression" ? current.callee : current.object;
390
+ while (current.type === "TSNonNullExpression") {
391
+ current = current.expression;
392
+ }
393
+ }
394
+ return false;
395
+ }
396
+ export function isEffectModule(sourceCode, node, module) {
397
+ const binding = importedBinding(sourceCode, node);
398
+ return ((binding?.source === "effect" && binding.name === module) ||
399
+ (binding?.source === `effect/${module}` && binding.name === "*"));
400
+ }
@@ -11,11 +11,16 @@ function getImportedName(specifier) {
11
11
  }
12
12
  return specifier.imported.value;
13
13
  }
14
+ function isCallOrTypeQuery(reference) {
15
+ const parent = reference.identifier.parent;
16
+ return (parent.type === "TSTypeQuery" ||
17
+ (parent.type === "CallExpression" && parent.callee === reference.identifier));
18
+ }
14
19
  export const noServiceConstructorImports = defineRule({
15
20
  meta: {
16
21
  type: "problem",
17
22
  docs: {
18
- description: "Disallow project-local make<CapabilityName> imports outside test and spec files.",
23
+ description: "Disallow project-local make<CapabilityName> service values imported outside test and spec files.",
19
24
  },
20
25
  messages: {
21
26
  serviceConstructorImport: 'Do not import Effect service constructor "{{name}}" into runtime code. Import the owning Layer, yield the contextual service, and allow its requirements to propagate to the composition root.',
@@ -33,7 +38,10 @@ export const noServiceConstructorImports = defineRule({
33
38
  continue;
34
39
  }
35
40
  const importedName = getImportedName(specifier);
36
- if (!SERVICE_CONSTRUCTOR_NAME.test(importedName)) {
41
+ if (!SERVICE_CONSTRUCTOR_NAME.test(importedName) ||
42
+ context.sourceCode
43
+ .getDeclaredVariables(specifier)
44
+ .every((variable) => variable.references.every(isCallOrTypeQuery))) {
37
45
  continue;
38
46
  }
39
47
  context.report({