oxlint-plugin-effect 0.10.0 → 0.11.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 (33) hide show
  1. package/README.md +32 -21
  2. package/dist/presets/recommended.d.ts +11 -0
  3. package/dist/presets/recommended.js +12 -1
  4. package/dist/rules/_effect-context.js +27 -0
  5. package/dist/rules/_effect-namespaces.js +25 -0
  6. package/dist/rules/_tagged-control-flow.js +1 -1
  7. package/dist/rules/index.d.ts +12 -1
  8. package/dist/rules/index.js +23 -1
  9. package/dist/rules/no-inline-provide.d.ts +6 -0
  10. package/dist/rules/no-inline-provide.js +39 -0
  11. package/dist/rules/no-managed-runtime-in-effect.d.ts +6 -0
  12. package/dist/rules/no-managed-runtime-in-effect.js +41 -0
  13. package/dist/rules/no-nested-effect-gen.d.ts +6 -0
  14. package/dist/rules/no-nested-effect-gen.js +39 -0
  15. package/dist/rules/no-per-call-cache-construction.d.ts +6 -0
  16. package/dist/rules/no-per-call-cache-construction.js +52 -0
  17. package/dist/rules/no-run-collect-on-unbounded-stream.d.ts +6 -0
  18. package/dist/rules/no-run-collect-on-unbounded-stream.js +81 -0
  19. package/dist/rules/no-sequential-effect-all.d.ts +6 -0
  20. package/dist/rules/no-sequential-effect-all.js +61 -0
  21. package/dist/rules/no-silent-catch-all.d.ts +6 -0
  22. package/dist/rules/no-silent-catch-all.js +51 -0
  23. package/dist/rules/no-unbounded-concurrency.d.ts +6 -0
  24. package/dist/rules/no-unbounded-concurrency.js +50 -0
  25. package/dist/rules/no-unbounded-retry.d.ts +6 -0
  26. package/dist/rules/no-unbounded-retry.js +114 -0
  27. package/dist/rules/prefer-catch-tag.js +33 -6
  28. package/dist/rules/prefer-match-tags-exhaustive.js +62 -8
  29. package/dist/rules/prefer-service-of.d.ts +6 -0
  30. package/dist/rules/prefer-service-of.js +65 -0
  31. package/dist/rules/require-named-effect-fn.d.ts +6 -0
  32. package/dist/rules/require-named-effect-fn.js +37 -0
  33. package/package.json +1 -1
package/README.md CHANGED
@@ -25,27 +25,38 @@ If the project uses JSON configuration, copy the exported rule map into `rules`;
25
25
 
26
26
  ## Recommended Rules
27
27
 
28
- | Rule | Contract |
29
- | ---------------------------------- | ----------------------------------------------------------------------------------------- |
30
- | `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` |
31
- | `effect/noAsyncFunction` | Bans async functions and await expressions |
32
- | `effect/noTryCatch` | Bans every try/catch/finally statement |
33
- | `effect/noTestLifecycleHooks` | Bans `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`; use Effect scopes instead |
34
- | `effect/noThrowStatement` | Bans every throw statement |
35
- | `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
36
- | `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
37
- | `effect/noNullish` | Bans null and undefined; use Option or a domain enum for richer state |
38
- | `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
39
- | `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
40
- | `effect/noDynamicImports` | Allows import() only behind a named lazy-loading boundary; bans require() |
41
- | `effect/noEffectDo` | Bans Effect.Do |
42
- | `effect/noEffectBind` | Bans Effect.bind |
43
- | `effect/preferCatchTag` | Replaces manual `_tag` predicates in `Effect.catchIf` with tagged recovery |
44
- | `effect/preferEffectFn` | Requires `Effect.fn` for a generator operation that adds a span |
45
- | `effect/preferMatchTagsExhaustive` | Requires exhaustive `Match` for return-only `_tag` switches |
46
- | `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
47
- | `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
48
- | `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
28
+ | Rule | Contract |
29
+ | -------------------------------------- | ----------------------------------------------------------------------------------------- |
30
+ | `effect/noAs` | Bans TypeScript `as` assertions; use `satisfies` |
31
+ | `effect/noAsyncFunction` | Bans async functions and await expressions |
32
+ | `effect/noTryCatch` | Bans every try/catch/finally statement |
33
+ | `effect/noTestLifecycleHooks` | Bans `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`; use Effect scopes instead |
34
+ | `effect/noThrowStatement` | Bans every throw statement |
35
+ | `effect/noNewPromise` | Bans new Promise, Promise calls, and Promise static APIs |
36
+ | `effect/noNewError` | Allows native Error values only as direct arguments to Effect.die, Cause.die, or Exit.die |
37
+ | `effect/noNullish` | Bans null and undefined; use Option or a domain enum for richer state |
38
+ | `effect/noModuleMocks` | Bans Vitest and Jest module mocks and method spies; use Effect service test layers |
39
+ | `effect/noTernary` | Bans conditional expressions while allowing ordinary if statements |
40
+ | `effect/noManagedRuntimeInEffect` | Keeps ManagedRuntime construction at non-Effect host boundaries |
41
+ | `effect/noInlineProvide` | Keeps dependency provisioning at explicit composition boundaries |
42
+ | `effect/noNestedEffectGen` | Flattens directly yielded nested generators |
43
+ | `effect/noPerCallCacheConstruction` | Constructs shared caches once in their owning layer |
44
+ | `effect/noRunCollectOnUnboundedStream` | Requires termination before collecting a clearly unbounded Stream |
45
+ | `effect/noSequentialEffectAll` | Uses explicit sequencing when serial aggregation discards its result |
46
+ | `effect/noSilentCatchAll` | Keeps swallowed failures visible or recovers them truthfully |
47
+ | `effect/noUnboundedConcurrency` | Requires finite concurrency for collections that can grow |
48
+ | `effect/noUnboundedRetry` | Requires an attempt or duration bound on retry schedules |
49
+ | `effect/noDynamicImports` | Allows import() only behind a named lazy-loading boundary; bans require() |
50
+ | `effect/noEffectDo` | Bans Effect.Do |
51
+ | `effect/noEffectBind` | Bans Effect.bind |
52
+ | `effect/preferCatchTag` | Replaces manual `_tag` predicates and `catchAll` dispatch with tagged recovery |
53
+ | `effect/preferEffectFn` | Requires `Effect.fn` for a generator operation that adds a span |
54
+ | `effect/preferMatchTagsExhaustive` | Requires exhaustive `Match` for return-only `_tag` switches and if chains |
55
+ | `effect/preferPredicateIsTagged` | Replaces combined `_tag` comparisons with a named `Predicate` refinement |
56
+ | `effect/preferServiceOf` | Checks inline Layer implementations through `Service.of` |
57
+ | `effect/requireNamedEffectFn` | Requires stable names for `Effect.fn` operations |
58
+ | `effect/noGlobals` | Bans ambient capabilities with direct Effect replacements; allows `process.std*.isTTY` |
59
+ | `effect/noNodeBuiltinImport` | Bans fully replaced Node modules and replaced operations from partial modules |
49
60
 
50
61
  ## Anti-Slop Rules
51
62
 
@@ -13,19 +13,28 @@ declare const recommended: {
13
13
  readonly "effect/noEffectBind": "error";
14
14
  readonly "effect/noEffectDo": "error";
15
15
  readonly "effect/noGlobals": "error";
16
+ readonly "effect/noInlineProvide": "error";
16
17
  readonly "effect/noKnownValueWidening": "error";
18
+ readonly "effect/noManagedRuntimeInEffect": "error";
17
19
  readonly "effect/noModuleMocks": "error";
20
+ readonly "effect/noNestedEffectGen": "error";
18
21
  readonly "effect/noNewError": "error";
19
22
  readonly "effect/noNewPromise": "error";
20
23
  readonly "effect/noNodeBuiltinImport": "error";
21
24
  readonly "effect/noNullish": "error";
22
25
  readonly "effect/noObjectParameters": "error";
26
+ readonly "effect/noPerCallCacheConstruction": "error";
27
+ readonly "effect/noRunCollectOnUnboundedStream": "error";
23
28
  readonly "effect/noRuntimeTypeof": "error";
29
+ readonly "effect/noSequentialEffectAll": "error";
24
30
  readonly "effect/noShapeInSymbolNames": "error";
31
+ readonly "effect/noSilentCatchAll": "error";
25
32
  readonly "effect/noTernary": "error";
26
33
  readonly "effect/noTestLifecycleHooks": "error";
27
34
  readonly "effect/noThrowStatement": "error";
28
35
  readonly "effect/noTryCatch": "error";
36
+ readonly "effect/noUnboundedConcurrency": "error";
37
+ readonly "effect/noUnboundedRetry": "error";
29
38
  readonly "effect/noUnknownParameters": "error";
30
39
  readonly "effect/noUnknownTypeAliases": "error";
31
40
  readonly "effect/noUnsafeDictionaryType": "error";
@@ -34,6 +43,8 @@ declare const recommended: {
34
43
  readonly "effect/preferEffectFn": "error";
35
44
  readonly "effect/preferMatchTagsExhaustive": "error";
36
45
  readonly "effect/preferPredicateIsTagged": "error";
46
+ readonly "effect/preferServiceOf": "error";
47
+ readonly "effect/requireNamedEffectFn": "error";
37
48
  };
38
49
  //#endregion
39
50
  export { recommended };
@@ -13,19 +13,28 @@ const recommended = {
13
13
  "effect/noEffectBind": "error",
14
14
  "effect/noEffectDo": "error",
15
15
  "effect/noGlobals": "error",
16
+ "effect/noInlineProvide": "error",
16
17
  "effect/noKnownValueWidening": "error",
18
+ "effect/noManagedRuntimeInEffect": "error",
17
19
  "effect/noModuleMocks": "error",
20
+ "effect/noNestedEffectGen": "error",
18
21
  "effect/noNewError": "error",
19
22
  "effect/noNewPromise": "error",
20
23
  "effect/noNodeBuiltinImport": "error",
21
24
  "effect/noNullish": "error",
22
25
  "effect/noObjectParameters": "error",
26
+ "effect/noPerCallCacheConstruction": "error",
27
+ "effect/noRunCollectOnUnboundedStream": "error",
23
28
  "effect/noRuntimeTypeof": "error",
29
+ "effect/noSequentialEffectAll": "error",
24
30
  "effect/noShapeInSymbolNames": "error",
31
+ "effect/noSilentCatchAll": "error",
25
32
  "effect/noTernary": "error",
26
33
  "effect/noTestLifecycleHooks": "error",
27
34
  "effect/noThrowStatement": "error",
28
35
  "effect/noTryCatch": "error",
36
+ "effect/noUnboundedConcurrency": "error",
37
+ "effect/noUnboundedRetry": "error",
29
38
  "effect/noUnknownParameters": "error",
30
39
  "effect/noUnknownTypeAliases": "error",
31
40
  "effect/noUnsafeDictionaryType": "error",
@@ -33,7 +42,9 @@ const recommended = {
33
42
  "effect/preferCatchTag": "error",
34
43
  "effect/preferEffectFn": "error",
35
44
  "effect/preferMatchTagsExhaustive": "error",
36
- "effect/preferPredicateIsTagged": "error"
45
+ "effect/preferPredicateIsTagged": "error",
46
+ "effect/preferServiceOf": "error",
47
+ "effect/requireNamedEffectFn": "error"
37
48
  };
38
49
  //#endregion
39
50
  export { recommended };
@@ -0,0 +1,27 @@
1
+ import { isStaticCall, isStaticMember } from "./_effect-namespaces.js";
2
+ //#region src/rules/_effect-context.ts
3
+ const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
4
+ const effectProgramKind = (node, effectNamespaces) => {
5
+ const parent = node.parent;
6
+ if (parent?.type !== "CallExpression" || !parent.arguments.includes(node)) return void 0;
7
+ if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "gen")) return "gen";
8
+ if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fn")) return "fn";
9
+ if (parent.callee.type !== "Super" && isStaticMember(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
10
+ if (parent.callee.type !== "CallExpression") return void 0;
11
+ if (isStaticCall(parent.callee, effectNamespaces, "fn")) return "fn";
12
+ if (isStaticCall(parent.callee, effectNamespaces, "fnUntraced")) return "fnUntraced";
13
+ };
14
+ const enclosingEffectProgram = (node, effectNamespaces) => {
15
+ let current = node.parent;
16
+ while (current !== void 0 && current !== null) {
17
+ if (isFunction(current)) {
18
+ const kind = effectProgramKind(current, effectNamespaces);
19
+ if (kind === void 0) return void 0;
20
+ return [current, kind];
21
+ }
22
+ current = current.parent;
23
+ }
24
+ };
25
+ const isInsideEffectProgram = (node, effectNamespaces) => enclosingEffectProgram(node, effectNamespaces) !== void 0;
26
+ //#endregion
27
+ export { enclosingEffectProgram, isInsideEffectProgram };
@@ -0,0 +1,25 @@
1
+ import { findVariableUp } from "../vendor/effect-oxlint/Scope.js";
2
+ import "../vendor/effect-oxlint/index.js";
3
+ import * as Option from "effect/Option";
4
+ //#region src/rules/_effect-namespaces.ts
5
+ const importedNamespaces = (node, exportName, directModule) => {
6
+ const source = node.source.value;
7
+ if (source !== "effect" && source !== directModule) return [];
8
+ const namespaces = [];
9
+ for (const specifier of node.specifiers) {
10
+ if (specifier.type === "ImportNamespaceSpecifier" && source === directModule) {
11
+ namespaces.push(specifier.local.name);
12
+ continue;
13
+ }
14
+ if (specifier.type === "ImportSpecifier" && source === "effect" && specifier.imported.type === "Identifier" && specifier.imported.name === exportName) namespaces.push(specifier.local.name);
15
+ }
16
+ return namespaces;
17
+ };
18
+ const visibleNamespaces = (ctx, node, namespaces) => new Set([...namespaces].filter((namespace) => Option.match(findVariableUp(ctx.sourceCode.getScope(node), namespace), {
19
+ onNone: () => true,
20
+ onSome: (variable) => variable.defs.some((definition) => definition.type === "ImportBinding")
21
+ })));
22
+ const isStaticMember = (node, namespaces, property) => node.type === "MemberExpression" && !node.computed && node.object.type === "Identifier" && namespaces.has(node.object.name) && node.property.type === "Identifier" && node.property.name === property;
23
+ const isStaticCall = (node, namespaces, property) => node.type === "CallExpression" && node.callee.type !== "Super" && isStaticMember(node.callee, namespaces, property);
24
+ //#endregion
25
+ export { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces };
@@ -76,4 +76,4 @@ const isInsideCatchAllHandler = (node) => {
76
76
  };
77
77
  const taggedSwitchSubject = (node) => taggedSubject(node.discriminant);
78
78
  //#endregion
79
- export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparisonsInOr, taggedSwitchSubject };
79
+ export { effectCallbackLocation, hasOneTaggedSubject, isEffectCall, isInsideCatchAllHandler, tagComparison, tagComparisonsInOr, taggedSwitchSubject };
@@ -6,19 +6,28 @@ import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
6
6
  import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
7
7
  import { noDynamicImports } from "./no-dynamic-imports.js";
8
8
  import { noGlobals } from "./no-globals.js";
9
+ import { noInlineProvide } from "./no-inline-provide.js";
9
10
  import { noKnownValueWidening } from "./no-known-value-widening.js";
11
+ import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
10
12
  import { noModuleMocks } from "./no-module-mocks.js";
13
+ import { noNestedEffectGen } from "./no-nested-effect-gen.js";
11
14
  import { noNewError } from "./no-new-error.js";
12
15
  import { noNewPromise } from "./no-new-promise.js";
13
16
  import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
14
17
  import { noNullish } from "./no-nullish.js";
15
18
  import { noObjectParameters } from "./no-object-parameters.js";
19
+ import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
20
+ import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
16
21
  import { noRuntimeTypeof } from "./no-runtime-typeof.js";
22
+ import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
17
23
  import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
24
+ import { noSilentCatchAll } from "./no-silent-catch-all.js";
18
25
  import { noTernary } from "./no-ternary.js";
19
26
  import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
20
27
  import { noThrowStatement } from "./no-throw-statement.js";
21
28
  import { noTryCatch } from "./no-try-catch.js";
29
+ import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
30
+ import { noUnboundedRetry } from "./no-unbounded-retry.js";
22
31
  import { noUnknownParameters } from "./no-unknown-parameters.js";
23
32
  import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
24
33
  import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
@@ -27,10 +36,12 @@ import { preferCatchTag } from "./prefer-catch-tag.js";
27
36
  import { preferEffectFn } from "./prefer-effect-fn.js";
28
37
  import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
29
38
  import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
39
+ import { preferServiceOf } from "./prefer-service-of.js";
40
+ import { requireNamedEffectFn } from "./require-named-effect-fn.js";
30
41
 
31
42
  //#region src/rules/index.d.ts
32
43
  declare namespace index_d_exports {
33
- export { noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noKnownValueWidening, noModuleMocks, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noRuntimeTypeof, noShapeInSymbolNames, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged };
44
+ export { noAs, noAsyncFunction, noChainedTypeAssertions, noConditionalEmptyObjectSpread, noDynamicImports, noEffectBind, noEffectDo, noGlobals, noInlineProvide, noKnownValueWidening, noManagedRuntimeInEffect, noModuleMocks, noNestedEffectGen, noNewError, noNewPromise, noNodeBuiltinImport, noNullish, noObjectParameters, noPerCallCacheConstruction, noRunCollectOnUnboundedStream, noRuntimeTypeof, noSequentialEffectAll, noShapeInSymbolNames, noSilentCatchAll, noTernary, noTestLifecycleHooks, noThrowStatement, noTryCatch, noUnboundedConcurrency, noUnboundedRetry, noUnknownParameters, noUnknownTypeAliases, noUnsafeDictionaryType, noWidenThenAssert, preferCatchTag, preferEffectFn, preferMatchTagsExhaustive, preferPredicateIsTagged, preferServiceOf, requireNamedEffectFn };
34
45
  }
35
46
  //#endregion
36
47
  export { index_d_exports };
@@ -7,19 +7,28 @@ import { noChainedTypeAssertions } from "./no-chained-type-assertions.js";
7
7
  import { noConditionalEmptyObjectSpread } from "./no-conditional-empty-object-spread.js";
8
8
  import { noDynamicImports } from "./no-dynamic-imports.js";
9
9
  import { noGlobals } from "./no-globals.js";
10
+ import { noInlineProvide } from "./no-inline-provide.js";
10
11
  import { noKnownValueWidening } from "./no-known-value-widening.js";
12
+ import { noManagedRuntimeInEffect } from "./no-managed-runtime-in-effect.js";
11
13
  import { noModuleMocks } from "./no-module-mocks.js";
14
+ import { noNestedEffectGen } from "./no-nested-effect-gen.js";
12
15
  import { noNewError } from "./no-new-error.js";
13
16
  import { noNewPromise } from "./no-new-promise.js";
14
17
  import { noNodeBuiltinImport } from "./no-node-builtin-import.js";
15
18
  import { noNullish } from "./no-nullish.js";
16
19
  import { noObjectParameters } from "./no-object-parameters.js";
20
+ import { noPerCallCacheConstruction } from "./no-per-call-cache-construction.js";
21
+ import { noRunCollectOnUnboundedStream } from "./no-run-collect-on-unbounded-stream.js";
17
22
  import { noRuntimeTypeof } from "./no-runtime-typeof.js";
23
+ import { noSequentialEffectAll } from "./no-sequential-effect-all.js";
18
24
  import { noShapeInSymbolNames } from "./no-shape-in-symbol-names.js";
25
+ import { noSilentCatchAll } from "./no-silent-catch-all.js";
19
26
  import { noTernary } from "./no-ternary.js";
20
27
  import { noTestLifecycleHooks } from "./no-test-lifecycle-hooks.js";
21
28
  import { noThrowStatement } from "./no-throw-statement.js";
22
29
  import { noTryCatch } from "./no-try-catch.js";
30
+ import { noUnboundedConcurrency } from "./no-unbounded-concurrency.js";
31
+ import { noUnboundedRetry } from "./no-unbounded-retry.js";
23
32
  import { noUnknownParameters } from "./no-unknown-parameters.js";
24
33
  import { noUnknownTypeAliases } from "./no-unknown-type-aliases.js";
25
34
  import { noUnsafeDictionaryType } from "./no-unsafe-dictionary-type.js";
@@ -28,6 +37,8 @@ import { preferCatchTag } from "./prefer-catch-tag.js";
28
37
  import { preferEffectFn } from "./prefer-effect-fn.js";
29
38
  import { preferMatchTagsExhaustive } from "./prefer-match-tags-exhaustive.js";
30
39
  import { preferPredicateIsTagged } from "./prefer-predicate-is-tagged.js";
40
+ import { preferServiceOf } from "./prefer-service-of.js";
41
+ import { requireNamedEffectFn } from "./require-named-effect-fn.js";
31
42
  //#region src/rules/index.ts
32
43
  var rules_exports = /* @__PURE__ */ __exportAll({
33
44
  noAs: () => noAs,
@@ -38,19 +49,28 @@ var rules_exports = /* @__PURE__ */ __exportAll({
38
49
  noEffectBind: () => noEffectBind,
39
50
  noEffectDo: () => noEffectDo,
40
51
  noGlobals: () => noGlobals,
52
+ noInlineProvide: () => noInlineProvide,
41
53
  noKnownValueWidening: () => noKnownValueWidening,
54
+ noManagedRuntimeInEffect: () => noManagedRuntimeInEffect,
42
55
  noModuleMocks: () => noModuleMocks,
56
+ noNestedEffectGen: () => noNestedEffectGen,
43
57
  noNewError: () => noNewError,
44
58
  noNewPromise: () => noNewPromise,
45
59
  noNodeBuiltinImport: () => noNodeBuiltinImport,
46
60
  noNullish: () => noNullish,
47
61
  noObjectParameters: () => noObjectParameters,
62
+ noPerCallCacheConstruction: () => noPerCallCacheConstruction,
63
+ noRunCollectOnUnboundedStream: () => noRunCollectOnUnboundedStream,
48
64
  noRuntimeTypeof: () => noRuntimeTypeof,
65
+ noSequentialEffectAll: () => noSequentialEffectAll,
49
66
  noShapeInSymbolNames: () => noShapeInSymbolNames,
67
+ noSilentCatchAll: () => noSilentCatchAll,
50
68
  noTernary: () => noTernary,
51
69
  noTestLifecycleHooks: () => noTestLifecycleHooks,
52
70
  noThrowStatement: () => noThrowStatement,
53
71
  noTryCatch: () => noTryCatch,
72
+ noUnboundedConcurrency: () => noUnboundedConcurrency,
73
+ noUnboundedRetry: () => noUnboundedRetry,
54
74
  noUnknownParameters: () => noUnknownParameters,
55
75
  noUnknownTypeAliases: () => noUnknownTypeAliases,
56
76
  noUnsafeDictionaryType: () => noUnsafeDictionaryType,
@@ -58,7 +78,9 @@ var rules_exports = /* @__PURE__ */ __exportAll({
58
78
  preferCatchTag: () => preferCatchTag,
59
79
  preferEffectFn: () => preferEffectFn,
60
80
  preferMatchTagsExhaustive: () => preferMatchTagsExhaustive,
61
- preferPredicateIsTagged: () => preferPredicateIsTagged
81
+ preferPredicateIsTagged: () => preferPredicateIsTagged,
82
+ preferServiceOf: () => preferServiceOf,
83
+ requireNamedEffectFn: () => requireNamedEffectFn
62
84
  });
63
85
  //#endregion
64
86
  export { rules_exports };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-inline-provide.d.ts
4
+ declare const noInlineProvide: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noInlineProvide };
@@ -0,0 +1,39 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import { isInsideEffectProgram } from "./_effect-context.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-inline-provide.ts
9
+ /** Keep Effect provisioning at explicit composition boundaries. */
10
+ const noInlineProvide = define({
11
+ name: "no-inline-provide",
12
+ meta: meta({
13
+ type: "problem",
14
+ description: "Keep Effect provisioning outside domain Effect operations."
15
+ }),
16
+ create: function* () {
17
+ const ctx = yield* RuleContext;
18
+ const effectNamespaces = new Set(["Effect"]);
19
+ return {
20
+ ImportDeclaration: (node) => {
21
+ if (node.type !== "ImportDeclaration") return Effect.void;
22
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
23
+ return Effect.void;
24
+ },
25
+ CallExpression: (node) => {
26
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
27
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
28
+ if (!isStaticMember(node.callee, namespaces, "provide")) return Effect.void;
29
+ if (!isInsideEffectProgram(node, namespaces)) return Effect.void;
30
+ return ctx.report(make({
31
+ node,
32
+ message: "Do not provide dependencies inside an Effect operation. Provide the layer once at the application, handler, command, or test boundary."
33
+ }));
34
+ }
35
+ };
36
+ }
37
+ });
38
+ //#endregion
39
+ export { noInlineProvide };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-managed-runtime-in-effect.d.ts
4
+ declare const noManagedRuntimeInEffect: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noManagedRuntimeInEffect };
@@ -0,0 +1,41 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import { isInsideEffectProgram } from "./_effect-context.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-managed-runtime-in-effect.ts
9
+ /** Keep ManagedRuntime construction at non-Effect host boundaries. */
10
+ const noManagedRuntimeInEffect = define({
11
+ name: "no-managed-runtime-in-effect",
12
+ meta: meta({
13
+ type: "problem",
14
+ description: "Create ManagedRuntime only at a non-Effect host boundary."
15
+ }),
16
+ create: function* () {
17
+ const ctx = yield* RuleContext;
18
+ const effectNamespaces = new Set(["Effect"]);
19
+ const managedRuntimeNamespaces = new Set(["ManagedRuntime"]);
20
+ return {
21
+ ImportDeclaration: (node) => {
22
+ if (node.type !== "ImportDeclaration") return Effect.void;
23
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
24
+ for (const name of importedNamespaces(node, "ManagedRuntime", "effect/ManagedRuntime")) managedRuntimeNamespaces.add(name);
25
+ return Effect.void;
26
+ },
27
+ CallExpression: (node) => {
28
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
29
+ const runtimes = visibleNamespaces(ctx, node, managedRuntimeNamespaces);
30
+ if (!isStaticMember(node.callee, runtimes, "make")) return Effect.void;
31
+ if (!isInsideEffectProgram(node, visibleNamespaces(ctx, node, effectNamespaces))) return Effect.void;
32
+ return ctx.report(make({
33
+ node,
34
+ message: "Create ManagedRuntime only at a non-Effect host boundary. Use the current Effect environment inside Effect programs."
35
+ }));
36
+ }
37
+ };
38
+ }
39
+ });
40
+ //#endregion
41
+ export { noManagedRuntimeInEffect };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-nested-effect-gen.d.ts
4
+ declare const noNestedEffectGen: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noNestedEffectGen };
@@ -0,0 +1,39 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import { isInsideEffectProgram } from "./_effect-context.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-nested-effect-gen.ts
9
+ const isDirectYield = (node) => node.parent?.type === "YieldExpression";
10
+ const noNestedEffectGen = define({
11
+ name: "no-nested-effect-gen",
12
+ meta: meta({
13
+ type: "suggestion",
14
+ description: "Flatten directly yielded nested Effect.gen programs."
15
+ }),
16
+ create: function* () {
17
+ const ctx = yield* RuleContext;
18
+ const effectNamespaces = new Set(["Effect"]);
19
+ return {
20
+ ImportDeclaration: (node) => {
21
+ if (node.type !== "ImportDeclaration") return Effect.void;
22
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
23
+ return Effect.void;
24
+ },
25
+ CallExpression: (node) => {
26
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
27
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
28
+ if (!isStaticMember(node.callee, namespaces, "gen") || !isDirectYield(node)) return Effect.void;
29
+ if (!isInsideEffectProgram(node, namespaces)) return Effect.void;
30
+ return ctx.report(make({
31
+ node,
32
+ message: "Flatten this directly yielded Effect.gen into the owning Effect operation."
33
+ }));
34
+ }
35
+ };
36
+ }
37
+ });
38
+ //#endregion
39
+ export { noNestedEffectGen };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-per-call-cache-construction.d.ts
4
+ declare const noPerCallCacheConstruction: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noPerCallCacheConstruction };
@@ -0,0 +1,52 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import { enclosingEffectProgram } from "./_effect-context.js";
7
+ import * as Effect from "effect/Effect";
8
+ //#region src/rules/no-per-call-cache-construction.ts
9
+ const cachedEffectOperations = new Set([
10
+ "cached",
11
+ "cachedInvalidateWithTTL",
12
+ "cachedWithTTL"
13
+ ]);
14
+ const cacheConstructors = new Set(["make", "makeWith"]);
15
+ const isOperation = (node, namespaces, operations) => {
16
+ for (const operation of operations) if (isStaticMember(node, namespaces, operation)) return true;
17
+ return false;
18
+ };
19
+ const noPerCallCacheConstruction = define({
20
+ name: "no-per-call-cache-construction",
21
+ meta: meta({
22
+ type: "suggestion",
23
+ description: "Construct shared caches once in their owning layer."
24
+ }),
25
+ create: function* () {
26
+ const ctx = yield* RuleContext;
27
+ const effectNamespaces = new Set(["Effect"]);
28
+ const cacheNamespaces = new Set(["Cache"]);
29
+ return {
30
+ ImportDeclaration: (node) => {
31
+ if (node.type !== "ImportDeclaration") return Effect.void;
32
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
33
+ for (const name of importedNamespaces(node, "Cache", "effect/Cache")) cacheNamespaces.add(name);
34
+ return Effect.void;
35
+ },
36
+ CallExpression: (node) => {
37
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
38
+ const effects = visibleNamespaces(ctx, node, effectNamespaces);
39
+ const program = enclosingEffectProgram(node, effects);
40
+ if (program === void 0 || program[1] === "gen") return Effect.void;
41
+ const caches = visibleNamespaces(ctx, node, cacheNamespaces);
42
+ if (!(isOperation(node.callee, effects, cachedEffectOperations) || isOperation(node.callee, caches, cacheConstructors))) return Effect.void;
43
+ return ctx.report(make({
44
+ node,
45
+ message: "Construct this cache once in its owning layer. Creating it inside an Effect operation creates new cache state on every call."
46
+ }));
47
+ }
48
+ };
49
+ }
50
+ });
51
+ //#endregion
52
+ export { noPerCallCacheConstruction };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-run-collect-on-unbounded-stream.d.ts
4
+ declare const noRunCollectOnUnboundedStream: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noRunCollectOnUnboundedStream };
@@ -0,0 +1,81 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-run-collect-on-unbounded-stream.ts
8
+ const unboundedSources = new Set([
9
+ "fromPubSub",
10
+ "fromPubSubTake",
11
+ "fromQueue",
12
+ "repeat"
13
+ ]);
14
+ const collectOperations = new Set(["runCollect"]);
15
+ const terminatingOperations = new Set([
16
+ "take",
17
+ "takeUntil",
18
+ "takeUntilEffect",
19
+ "takeWhile",
20
+ "takeWhileEffect",
21
+ "timeout",
22
+ "timeoutOrElse"
23
+ ]);
24
+ const staticOperation = (node, streamNamespaces, operations) => {
25
+ if (node.type === "SpreadElement") return false;
26
+ const candidate = node.type === "CallExpression" && node.callee.type !== "Super" ? node.callee : node;
27
+ for (const operation of operations) if (isStaticMember(candidate, streamNamespaces, operation)) return true;
28
+ return false;
29
+ };
30
+ const unboundedRoot = (node, streamNamespaces) => {
31
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return void 0;
32
+ for (const source of unboundedSources) if (isStaticMember(node.callee, streamNamespaces, source)) return node;
33
+ if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
34
+ const root = unboundedRoot(node.callee.object, streamNamespaces);
35
+ if (root === void 0) return void 0;
36
+ for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
37
+ return root;
38
+ };
39
+ const collectedSource = (node, streamNamespaces) => {
40
+ if (node.callee.type === "Super") return void 0;
41
+ if (isStaticMember(node.callee, streamNamespaces, "runCollect")) {
42
+ const source = node.arguments[0];
43
+ if (source !== void 0 && source.type !== "SpreadElement") return source;
44
+ return;
45
+ }
46
+ if (node.callee.type !== "MemberExpression" || node.callee.computed || node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return;
47
+ for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, terminatingOperations)) return void 0;
48
+ for (const operation of node.arguments) if (staticOperation(operation, streamNamespaces, collectOperations)) return node.callee.object;
49
+ };
50
+ const noRunCollectOnUnboundedStream = define({
51
+ name: "no-run-collect-on-unbounded-stream",
52
+ meta: meta({
53
+ type: "problem",
54
+ description: "Do not collect a clearly unbounded Stream without a terminating operation."
55
+ }),
56
+ create: function* () {
57
+ const ctx = yield* RuleContext;
58
+ const streamNamespaces = new Set(["Stream"]);
59
+ return {
60
+ ImportDeclaration: (node) => {
61
+ if (node.type !== "ImportDeclaration") return Effect.void;
62
+ for (const name of importedNamespaces(node, "Stream", "effect/Stream")) streamNamespaces.add(name);
63
+ return Effect.void;
64
+ },
65
+ CallExpression: (node) => {
66
+ if (node.type !== "CallExpression") return Effect.void;
67
+ const namespaces = visibleNamespaces(ctx, node, streamNamespaces);
68
+ const source = collectedSource(node, namespaces);
69
+ if (source === void 0) return Effect.void;
70
+ const root = unboundedRoot(source, namespaces);
71
+ if (root === void 0) return Effect.void;
72
+ return ctx.report(make({
73
+ node: root,
74
+ message: "Do not collect a clearly unbounded Stream. Add a terminating operation or consume it with runForEach or runDrain."
75
+ }));
76
+ }
77
+ };
78
+ }
79
+ });
80
+ //#endregion
81
+ export { noRunCollectOnUnboundedStream };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-sequential-effect-all.d.ts
4
+ declare const noSequentialEffectAll: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noSequentialEffectAll };
@@ -0,0 +1,61 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-sequential-effect-all.ts
8
+ const propertyLiteral = (node, name) => {
9
+ if (node?.type !== "ObjectExpression") return void 0;
10
+ for (const property of node.properties) {
11
+ if (property.type !== "Property" || property.computed) continue;
12
+ if ((property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) && property.value.type === "Literal") {
13
+ const value = property.value.value;
14
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
15
+ }
16
+ }
17
+ };
18
+ const pipedToAsVoid = (node, effectNamespaces) => {
19
+ const member = node.parent;
20
+ if (member?.type !== "MemberExpression" || member.object !== node || member.computed || member.property.type !== "Identifier" || member.property.name !== "pipe") return false;
21
+ const pipe = member.parent;
22
+ if (pipe?.type !== "CallExpression") return false;
23
+ for (const operation of pipe.arguments) {
24
+ if (operation.type === "SpreadElement") continue;
25
+ if (isStaticMember(operation, effectNamespaces, "asVoid")) return true;
26
+ }
27
+ return false;
28
+ };
29
+ const noSequentialEffectAll = define({
30
+ name: "no-sequential-effect-all",
31
+ meta: meta({
32
+ type: "suggestion",
33
+ description: "Use explicit sequencing instead of a serial Effect.all whose result is discarded."
34
+ }),
35
+ create: function* () {
36
+ const ctx = yield* RuleContext;
37
+ const effectNamespaces = new Set(["Effect"]);
38
+ return {
39
+ ImportDeclaration: (node) => {
40
+ if (node.type !== "ImportDeclaration") return Effect.void;
41
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
42
+ return Effect.void;
43
+ },
44
+ CallExpression: (node) => {
45
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
46
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
47
+ if (!isStaticMember(node.callee, namespaces, "all")) return Effect.void;
48
+ if (node.arguments[0]?.type !== "ArrayExpression") return Effect.void;
49
+ const options = node.arguments[1];
50
+ if (propertyLiteral(options, "concurrency") !== 1) return Effect.void;
51
+ if (!(propertyLiteral(options, "discard") === true || pipedToAsVoid(node, namespaces))) return Effect.void;
52
+ return ctx.report(make({
53
+ node,
54
+ message: "Use Effect.andThen or one flat generator for sequential steps. Reserve Effect.all for value aggregation or real concurrency."
55
+ }));
56
+ }
57
+ };
58
+ }
59
+ });
60
+ //#endregion
61
+ export { noSequentialEffectAll };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-silent-catch-all.d.ts
4
+ declare const noSilentCatchAll: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noSilentCatchAll };
@@ -0,0 +1,51 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-silent-catch-all.ts
8
+ const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
9
+ const isEffectVoid = (node, effectNamespaces) => isStaticMember(node, effectNamespaces, "void");
10
+ const silentlyReturnsVoid = (node, effectNamespaces) => {
11
+ if (node.params.length !== 0 || node.body === null) return false;
12
+ if (node.body.type !== "BlockStatement") return isEffectVoid(node.body, effectNamespaces);
13
+ if (node.body.body.length !== 1) return false;
14
+ const statement = node.body.body[0];
15
+ return statement?.type === "ReturnStatement" && statement.argument !== null && isEffectVoid(statement.argument, effectNamespaces);
16
+ };
17
+ const handlerArgument = (node) => {
18
+ if (node.arguments.length === 1) return node.arguments[0];
19
+ return node.arguments[1];
20
+ };
21
+ const noSilentCatchAll = define({
22
+ name: "no-silent-catch-all",
23
+ meta: meta({
24
+ type: "problem",
25
+ description: "Do not silently erase every failure with Effect.void."
26
+ }),
27
+ create: function* () {
28
+ const ctx = yield* RuleContext;
29
+ const effectNamespaces = new Set(["Effect"]);
30
+ return {
31
+ ImportDeclaration: (node) => {
32
+ if (node.type !== "ImportDeclaration") return Effect.void;
33
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
34
+ return Effect.void;
35
+ },
36
+ CallExpression: (node) => {
37
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
38
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
39
+ if (!(isStaticMember(node.callee, namespaces, "catchAll") || isStaticMember(node.callee, namespaces, "catchAllCause"))) return Effect.void;
40
+ const handler = handlerArgument(node);
41
+ if (!isFunction(handler) || !silentlyReturnsVoid(handler, namespaces)) return Effect.void;
42
+ return ctx.report(make({
43
+ node: handler,
44
+ message: "Do not erase every failure with Effect.void. Recover a typed failure truthfully or record the unexpected failure before recovery."
45
+ }));
46
+ }
47
+ };
48
+ }
49
+ });
50
+ //#endregion
51
+ export { noSilentCatchAll };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-unbounded-concurrency.d.ts
4
+ declare const noUnboundedConcurrency: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noUnboundedConcurrency };
@@ -0,0 +1,50 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-unbounded-concurrency.ts
8
+ const unboundedConcurrency = (node) => {
9
+ if (node?.type !== "ObjectExpression") return void 0;
10
+ for (const property of node.properties) {
11
+ if (property.type !== "Property" || property.computed) continue;
12
+ if (!(property.key.type === "Identifier" && property.key.name === "concurrency" || property.key.type === "Literal" && property.key.value === "concurrency")) continue;
13
+ if (property.value.type === "Literal" && property.value.value === "unbounded") return property.value;
14
+ }
15
+ };
16
+ const fixedCollection = (node) => node?.type === "ArrayExpression" || node?.type === "ObjectExpression";
17
+ const noUnboundedConcurrency = define({
18
+ name: "no-unbounded-concurrency",
19
+ meta: meta({
20
+ type: "problem",
21
+ description: "Require finite concurrency for Effect operations over collections that can grow."
22
+ }),
23
+ create: function* () {
24
+ const ctx = yield* RuleContext;
25
+ const effectNamespaces = new Set(["Effect"]);
26
+ return {
27
+ ImportDeclaration: (node) => {
28
+ if (node.type !== "ImportDeclaration") return Effect.void;
29
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
30
+ return Effect.void;
31
+ },
32
+ CallExpression: (node) => {
33
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
34
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
35
+ let optionsIndex;
36
+ if (isStaticMember(node.callee, namespaces, "all")) optionsIndex = 1;
37
+ if (isStaticMember(node.callee, namespaces, "forEach")) optionsIndex = 2;
38
+ if (optionsIndex === void 0 || fixedCollection(node.arguments[0])) return Effect.void;
39
+ const unbounded = unboundedConcurrency(node.arguments[optionsIndex]);
40
+ if (unbounded === void 0) return Effect.void;
41
+ return ctx.report(make({
42
+ node: unbounded,
43
+ message: "Bound concurrency for a collection that can grow. Use a finite concurrency value."
44
+ }));
45
+ }
46
+ };
47
+ }
48
+ });
49
+ //#endregion
50
+ export { noUnboundedConcurrency };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/no-unbounded-retry.d.ts
4
+ declare const noUnboundedRetry: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { noUnboundedRetry };
@@ -0,0 +1,114 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/no-unbounded-retry.ts
8
+ const unboundedConstructors = new Set([
9
+ "exponential",
10
+ "fibonacci",
11
+ "spaced"
12
+ ]);
13
+ const boundedCombinators = new Set(["take"]);
14
+ const staticPropertyName = (node) => {
15
+ if (node.type !== "MemberExpression" || node.computed || node.property.type !== "Identifier") return;
16
+ return node.property.name;
17
+ };
18
+ const hasOwnProperty = (node, name) => {
19
+ for (const property of node.properties) {
20
+ if (property.type !== "Property" || property.computed) continue;
21
+ if (property.key.type === "Identifier" && property.key.name === name) return true;
22
+ if (property.key.type === "Literal" && property.key.value === name) return true;
23
+ }
24
+ return false;
25
+ };
26
+ const propertyValue = (node, name) => {
27
+ for (const property of node.properties) {
28
+ if (property.type !== "Property" || property.computed) continue;
29
+ if (property.key.type === "Identifier" && property.key.name === name || property.key.type === "Literal" && property.key.value === name) return property.value;
30
+ }
31
+ };
32
+ const isBoundedOperation = (node, scheduleNamespaces) => {
33
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return false;
34
+ const property = staticPropertyName(node.callee);
35
+ if (property === void 0) return false;
36
+ if (boundedCombinators.has(property) && isStaticMember(node.callee, scheduleNamespaces, property)) return true;
37
+ if (!isStaticMember(node.callee, scheduleNamespaces, "both")) return false;
38
+ const bound = node.arguments[0];
39
+ return bound !== void 0 && bound.type !== "SpreadElement" && isStaticCall(bound, scheduleNamespaces, "recurs");
40
+ };
41
+ const unboundedSchedule = (node, scheduleNamespaces) => {
42
+ if (node.type === "MemberExpression" && isStaticMember(node, scheduleNamespaces, "forever")) return node;
43
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return void 0;
44
+ const property = staticPropertyName(node.callee);
45
+ if (property !== void 0 && unboundedConstructors.has(property) && isStaticMember(node.callee, scheduleNamespaces, property)) return node;
46
+ if (property !== "pipe" || node.callee.type !== "MemberExpression") return void 0;
47
+ const root = unboundedSchedule(node.callee.object, scheduleNamespaces);
48
+ if (root === void 0) return void 0;
49
+ for (const operation of node.arguments) if (isBoundedOperation(operation, scheduleNamespaces)) return void 0;
50
+ return root;
51
+ };
52
+ const retryPolicy = (node, effectNamespaces, streamNamespaces, httpClientNamespaces) => {
53
+ if (node.callee.type === "Super") return void 0;
54
+ let policyIndex;
55
+ if (isStaticMember(node.callee, effectNamespaces, "retry")) {
56
+ if (node.arguments.length === 1) policyIndex = 0;
57
+ if (node.arguments.length >= 2) policyIndex = 1;
58
+ }
59
+ if (isStaticMember(node.callee, effectNamespaces, "retryOrElse")) {
60
+ if (node.arguments.length === 2) policyIndex = 0;
61
+ if (node.arguments.length >= 3) policyIndex = 1;
62
+ }
63
+ if (isStaticMember(node.callee, streamNamespaces, "retry")) {
64
+ if (node.arguments.length === 1) policyIndex = 0;
65
+ if (node.arguments.length >= 2) policyIndex = 1;
66
+ }
67
+ if (isStaticMember(node.callee, httpClientNamespaces, "retryTransient")) {
68
+ if (node.arguments.length === 1) policyIndex = 0;
69
+ if (node.arguments.length >= 2) policyIndex = 1;
70
+ }
71
+ if (policyIndex === void 0) return void 0;
72
+ const policy = node.arguments[policyIndex];
73
+ if (policy?.type === "SpreadElement") return void 0;
74
+ if (policy?.type !== "ObjectExpression") return policy;
75
+ if (hasOwnProperty(policy, "times")) return void 0;
76
+ return propertyValue(policy, "schedule");
77
+ };
78
+ const noUnboundedRetry = define({
79
+ name: "no-unbounded-retry",
80
+ meta: meta({
81
+ type: "problem",
82
+ description: "Require retry policies to have an explicit attempt or duration bound."
83
+ }),
84
+ create: function* () {
85
+ const ctx = yield* RuleContext;
86
+ const effectNamespaces = new Set(["Effect"]);
87
+ const httpClientNamespaces = new Set(["HttpClient"]);
88
+ const scheduleNamespaces = new Set(["Schedule"]);
89
+ const streamNamespaces = new Set(["Stream"]);
90
+ return {
91
+ ImportDeclaration: (node) => {
92
+ if (node.type !== "ImportDeclaration") return Effect.void;
93
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
94
+ for (const name of importedNamespaces(node, "HttpClient", "effect/unstable/http/HttpClient")) httpClientNamespaces.add(name);
95
+ for (const name of importedNamespaces(node, "Schedule", "effect/Schedule")) scheduleNamespaces.add(name);
96
+ for (const name of importedNamespaces(node, "Stream", "effect/Stream")) streamNamespaces.add(name);
97
+ return Effect.void;
98
+ },
99
+ CallExpression: (node) => {
100
+ if (node.type !== "CallExpression") return Effect.void;
101
+ const policy = retryPolicy(node, visibleNamespaces(ctx, node, effectNamespaces), visibleNamespaces(ctx, node, streamNamespaces), visibleNamespaces(ctx, node, httpClientNamespaces));
102
+ if (policy === void 0) return Effect.void;
103
+ const unbounded = unboundedSchedule(policy, visibleNamespaces(ctx, node, scheduleNamespaces));
104
+ if (unbounded === void 0) return Effect.void;
105
+ return ctx.report(make({
106
+ node: unbounded,
107
+ message: "Bound retry attempts or elapsed time. Add times or compose the schedule with Schedule.recurs or Schedule.take."
108
+ }));
109
+ }
110
+ };
111
+ }
112
+ });
113
+ //#endregion
114
+ export { noUnboundedRetry };
@@ -2,7 +2,7 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
- import { isEffectCall, tagComparisonsInOr } from "./_tagged-control-flow.js";
5
+ import { isEffectCall, tagComparisonsInOr, taggedSwitchSubject } from "./_tagged-control-flow.js";
6
6
  import * as Effect from "effect/Effect";
7
7
  //#region src/rules/prefer-catch-tag.ts
8
8
  const returnedExpression = (fn) => {
@@ -23,6 +23,32 @@ const hasManualTagPredicate = (argument) => {
23
23
  if (comparisons === null || comparisons.length === 0) return false;
24
24
  return comparisons.every((comparison) => comparison.subject === parameter.name);
25
25
  };
26
+ const isFunction = (argument) => argument?.type === "ArrowFunctionExpression" || argument?.type === "FunctionExpression";
27
+ const hasTagCondition = (node, parameter) => {
28
+ const comparisons = tagComparisonsInOr(node.test);
29
+ if (comparisons === null || comparisons.length === 0) return false;
30
+ return comparisons.every((comparison) => comparison.subject === parameter);
31
+ };
32
+ const hasTagSwitch = (node, parameter) => {
33
+ if (taggedSwitchSubject(node) !== parameter) return false;
34
+ return node.cases.some((switchCase) => switchCase.test?.type === "Literal" && typeof switchCase.test.value === "string");
35
+ };
36
+ const hasManualTagDispatch = (argument) => {
37
+ if (!isFunction(argument) || argument.body?.type !== "BlockStatement") return false;
38
+ const parameter = argument.params[0];
39
+ if (parameter?.type !== "Identifier") return false;
40
+ return argument.body.body.some((statement) => {
41
+ if (statement.type === "IfStatement") return hasTagCondition(statement, parameter.name);
42
+ if (statement.type === "SwitchStatement") return hasTagSwitch(statement, parameter.name);
43
+ return false;
44
+ });
45
+ };
46
+ const catchAllHandler = (node) => {
47
+ if (!isEffectCall(node, "catchAll")) return void 0;
48
+ let handlerIndex = 1;
49
+ if (node.arguments.length === 1) handlerIndex = 0;
50
+ return node.arguments[handlerIndex];
51
+ };
26
52
  const preferCatchTag = define({
27
53
  name: "prefer-catch-tag",
28
54
  meta: meta({
@@ -32,11 +58,12 @@ const preferCatchTag = define({
32
58
  create: function* () {
33
59
  const context = yield* RuleContext;
34
60
  return { CallExpression: (node) => {
35
- if (!isEffectCall(node, "catchIf")) return Effect.void;
36
- let predicateIndex = 1;
37
- if (node.arguments.length === 2) predicateIndex = 0;
38
- const predicate = node.arguments[predicateIndex];
39
- if (!hasManualTagPredicate(predicate)) return Effect.void;
61
+ if (isEffectCall(node, "catchIf")) {
62
+ let predicateIndex = 1;
63
+ if (node.arguments.length === 2) predicateIndex = 0;
64
+ const predicate = node.arguments[predicateIndex];
65
+ if (!hasManualTagPredicate(predicate)) return Effect.void;
66
+ } else if (!hasManualTagDispatch(catchAllHandler(node))) return Effect.void;
40
67
  return context.report(make({
41
68
  node,
42
69
  message: "Use Effect.catchTag for one tagged failure or Effect.catchTags for multiple tagged failures."
@@ -2,7 +2,7 @@ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
2
  import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
3
  import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
4
  import "../vendor/effect-oxlint/index.js";
5
- import { isInsideCatchAllHandler, taggedSwitchSubject } from "./_tagged-control-flow.js";
5
+ import { isInsideCatchAllHandler, tagComparison, taggedSwitchSubject } from "./_tagged-control-flow.js";
6
6
  import * as Effect from "effect/Effect";
7
7
  //#region src/rules/prefer-match-tags-exhaustive.ts
8
8
  const statementReturns = (statement) => {
@@ -25,6 +25,51 @@ const isCompleteTransformationShape = (node) => {
25
25
  }
26
26
  return true;
27
27
  };
28
+ const isElseIf = (node) => node.parent?.type === "IfStatement" && node.parent.alternate === node;
29
+ const isTerminalStatement = (node) => {
30
+ if (node.parent?.type !== "BlockStatement") return true;
31
+ return node.parent.body.at(-1) === node;
32
+ };
33
+ const isCompleteIfTransformationShape = (node) => {
34
+ if (isElseIf(node) || !isTerminalStatement(node)) return false;
35
+ let current = node;
36
+ let subject;
37
+ const tags = /* @__PURE__ */ new Set();
38
+ let branchCount = 0;
39
+ while (true) {
40
+ const comparison = tagComparison(current.test);
41
+ if (comparison === null || !statementReturns(current.consequent)) return false;
42
+ if (subject !== void 0 && comparison.subject !== subject) return false;
43
+ if (tags.has(comparison.tag)) return false;
44
+ subject = comparison.subject;
45
+ tags.add(comparison.tag);
46
+ branchCount += 1;
47
+ if (current.alternate === null) break;
48
+ if (current.alternate.type !== "IfStatement") return false;
49
+ current = current.alternate;
50
+ }
51
+ return branchCount >= 2;
52
+ };
53
+ const isCompleteSequentialIfTransformationShape = (node) => {
54
+ if (node.alternate !== null || node.parent?.type !== "BlockStatement") return false;
55
+ const statements = node.parent.body;
56
+ const index = statements.indexOf(node);
57
+ if (index < 0) return false;
58
+ const comparison = tagComparison(node.test);
59
+ if (comparison === null || !statementReturns(node.consequent)) return false;
60
+ const previous = statements[index - 1];
61
+ if (previous?.type === "IfStatement" && previous.alternate === null) {
62
+ if (tagComparison(previous.test)?.subject === comparison.subject && statementReturns(previous.consequent)) return false;
63
+ }
64
+ const tags = new Set([comparison.tag]);
65
+ for (const statement of statements.slice(index + 1)) {
66
+ if (statement.type !== "IfStatement" || statement.alternate !== null) return false;
67
+ const next = tagComparison(statement.test);
68
+ if (next === null || next.subject !== comparison.subject || tags.has(next.tag) || !statementReturns(statement.consequent)) return false;
69
+ tags.add(next.tag);
70
+ }
71
+ return tags.size >= 2;
72
+ };
28
73
  const preferMatchTagsExhaustive = define({
29
74
  name: "prefer-match-tags-exhaustive",
30
75
  meta: meta({
@@ -33,13 +78,22 @@ const preferMatchTagsExhaustive = define({
33
78
  }),
34
79
  create: function* () {
35
80
  const context = yield* RuleContext;
36
- return { SwitchStatement: (node) => {
37
- if (!isCompleteTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
38
- return context.report(make({
39
- node,
40
- message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new case."
41
- }));
42
- } };
81
+ return {
82
+ SwitchStatement: (node) => {
83
+ if (!isCompleteTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
84
+ return context.report(make({
85
+ node,
86
+ message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new case."
87
+ }));
88
+ },
89
+ IfStatement: (node) => {
90
+ if (!isCompleteIfTransformationShape(node) && !isCompleteSequentialIfTransformationShape(node) || isInsideCatchAllHandler(node)) return Effect.void;
91
+ return context.report(make({
92
+ node,
93
+ message: "Use Match.type with Match.tagsExhaustive so a new tagged variant requires a new branch."
94
+ }));
95
+ }
96
+ };
43
97
  }
44
98
  });
45
99
  //#endregion
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/prefer-service-of.d.ts
4
+ declare const preferServiceOf: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { preferServiceOf };
@@ -0,0 +1,65 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticCall, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/prefer-service-of.ts
8
+ const returnedObject = (node) => {
9
+ if (node?.type !== "ArrowFunctionExpression" && node?.type !== "FunctionExpression") return;
10
+ if (node.body === null) return void 0;
11
+ if (node.body.type === "ObjectExpression") return node.body;
12
+ if (node.body.type !== "BlockStatement") return void 0;
13
+ for (const statement of node.body.body) if (statement.type === "ReturnStatement" && statement.argument?.type === "ObjectExpression") return statement.argument;
14
+ };
15
+ const implementationObject = (node, layerNamespaces, effectNamespaces) => {
16
+ if (node.callee.type === "Super" || node.arguments.length < 2) return void 0;
17
+ const implementation = node.arguments[1];
18
+ if (implementation === void 0 || implementation.type === "SpreadElement") return void 0;
19
+ if (isStaticMember(node.callee, layerNamespaces, "succeed")) {
20
+ if (implementation.type === "ObjectExpression") return implementation;
21
+ return;
22
+ }
23
+ if (isStaticMember(node.callee, layerNamespaces, "sync")) return returnedObject(implementation);
24
+ if (!isStaticMember(node.callee, layerNamespaces, "effect")) return void 0;
25
+ if (implementation.type !== "CallExpression" || implementation.callee.type === "Super") return;
26
+ if (isStaticMember(implementation.callee, effectNamespaces, "succeed")) {
27
+ const value = implementation.arguments[0];
28
+ if (value?.type === "ObjectExpression") return value;
29
+ return;
30
+ }
31
+ if (isStaticCall(implementation, effectNamespaces, "gen")) return returnedObject(implementation.arguments[0]);
32
+ };
33
+ const preferServiceOf = define({
34
+ name: "prefer-service-of",
35
+ meta: meta({
36
+ type: "suggestion",
37
+ description: "Use Service.of to check inline Layer implementations against the service interface."
38
+ }),
39
+ create: function* () {
40
+ const ctx = yield* RuleContext;
41
+ const layerNamespaces = new Set(["Layer"]);
42
+ const effectNamespaces = new Set(["Effect"]);
43
+ return {
44
+ ImportDeclaration: (node) => {
45
+ if (node.type !== "ImportDeclaration") return Effect.void;
46
+ for (const name of importedNamespaces(node, "Layer", "effect/Layer")) layerNamespaces.add(name);
47
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
48
+ return Effect.void;
49
+ },
50
+ CallExpression: (node) => {
51
+ if (node.type !== "CallExpression") return Effect.void;
52
+ const service = node.arguments[0];
53
+ if (service?.type !== "Identifier") return Effect.void;
54
+ const implementation = implementationObject(node, visibleNamespaces(ctx, node, layerNamespaces), visibleNamespaces(ctx, node, effectNamespaces));
55
+ if (implementation === void 0) return Effect.void;
56
+ return ctx.report(make({
57
+ node: implementation,
58
+ message: `Wrap this implementation with ${service.name}.of(...) so it stays checked against the service interface.`
59
+ }));
60
+ }
61
+ };
62
+ }
63
+ });
64
+ //#endregion
65
+ export { preferServiceOf };
@@ -0,0 +1,6 @@
1
+ import * as _$_oxlint_plugins0 from "@oxlint/plugins";
2
+
3
+ //#region src/rules/require-named-effect-fn.d.ts
4
+ declare const requireNamedEffectFn: _$_oxlint_plugins0.CreateRule;
5
+ //#endregion
6
+ export { requireNamedEffectFn };
@@ -0,0 +1,37 @@
1
+ import { make } from "../vendor/effect-oxlint/Diagnostic.js";
2
+ import { RuleContext } from "../vendor/effect-oxlint/RuleContext.js";
3
+ import { define, meta } from "../vendor/effect-oxlint/Rule.js";
4
+ import "../vendor/effect-oxlint/index.js";
5
+ import { importedNamespaces, isStaticMember, visibleNamespaces } from "./_effect-namespaces.js";
6
+ import * as Effect from "effect/Effect";
7
+ //#region src/rules/require-named-effect-fn.ts
8
+ const isFunction = (node) => node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
9
+ const requireNamedEffectFn = define({
10
+ name: "require-named-effect-fn",
11
+ meta: meta({
12
+ type: "suggestion",
13
+ description: "Require a stable operation name for Effect.fn."
14
+ }),
15
+ create: function* () {
16
+ const ctx = yield* RuleContext;
17
+ const effectNamespaces = new Set(["Effect"]);
18
+ return {
19
+ ImportDeclaration: (node) => {
20
+ if (node.type !== "ImportDeclaration") return Effect.void;
21
+ for (const name of importedNamespaces(node, "Effect", "effect/Effect")) effectNamespaces.add(name);
22
+ return Effect.void;
23
+ },
24
+ CallExpression: (node) => {
25
+ if (node.type !== "CallExpression" || node.callee.type === "Super") return Effect.void;
26
+ const namespaces = visibleNamespaces(ctx, node, effectNamespaces);
27
+ if (!isStaticMember(node.callee, namespaces, "fn") || !isFunction(node.arguments[0])) return Effect.void;
28
+ return ctx.report(make({
29
+ node,
30
+ message: "Name this operation with Effect.fn(\"Domain.operation\") for stable tracing and diagnostics."
31
+ }));
32
+ }
33
+ };
34
+ }
35
+ });
36
+ //#endregion
37
+ export { requireNamedEffectFn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oxlint-plugin-effect",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "oxlint plugin with Effect-specific lint rules",
5
5
  "license": "MIT",
6
6
  "repository": {