oxlint-plugin-effect 0.0.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 (95) hide show
  1. package/.effect-lsp.json +60 -0
  2. package/.oxlintrc.json +33 -0
  3. package/package.json +51 -0
  4. package/scripts/add-rule.ts +87 -0
  5. package/scripts/codegen.ts +108 -0
  6. package/src/index.ts +9 -0
  7. package/src/plugin.ts +16 -0
  8. package/src/presets/core.ts +36 -0
  9. package/src/presets/effect-native.ts +41 -0
  10. package/src/presets/full.ts +84 -0
  11. package/src/presets/index.ts +3 -0
  12. package/src/rules/_effect-context.ts +28 -0
  13. package/src/rules/index.ts +83 -0
  14. package/src/rules/no-arrow-ladder.ts +71 -0
  15. package/src/rules/no-async-function.ts +48 -0
  16. package/src/rules/no-console-in-effect.ts +46 -0
  17. package/src/rules/no-date-in-effect.ts +61 -0
  18. package/src/rules/no-dynamic-import.ts +13 -0
  19. package/src/rules/no-effect-as.ts +14 -0
  20. package/src/rules/no-effect-async.ts +14 -0
  21. package/src/rules/no-effect-bind.ts +13 -0
  22. package/src/rules/no-effect-do.ts +14 -0
  23. package/src/rules/no-effect-fn-generator.ts +59 -0
  24. package/src/rules/no-effect-gen-adapter.ts +51 -0
  25. package/src/rules/no-effect-map-void.ts +81 -0
  26. package/src/rules/no-effect-never.ts +14 -0
  27. package/src/rules/no-effect-orElse-ladder.ts +80 -0
  28. package/src/rules/no-effect-succeed-string.ts +53 -0
  29. package/src/rules/no-effect-succeed-variable.ts +51 -0
  30. package/src/rules/no-effect-succeed-void.ts +64 -0
  31. package/src/rules/no-effect-sync-console.ts +92 -0
  32. package/src/rules/no-effect-sync-wrapper.ts +68 -0
  33. package/src/rules/no-effect-type-alias.ts +72 -0
  34. package/src/rules/no-effect-wrapper-alias.ts +98 -0
  35. package/src/rules/no-extends-native-error.ts +55 -0
  36. package/src/rules/no-fetch-in-effect.ts +47 -0
  37. package/src/rules/no-flatmap-ladder.ts +79 -0
  38. package/src/rules/no-fromnullable-coalesce.ts +63 -0
  39. package/src/rules/no-global-console.ts +17 -0
  40. package/src/rules/no-global-date.ts +20 -0
  41. package/src/rules/no-global-fetch.ts +14 -0
  42. package/src/rules/no-global-random.ts +13 -0
  43. package/src/rules/no-global-timers.ts +17 -0
  44. package/src/rules/no-if-statement.ts +16 -0
  45. package/src/rules/no-iife-wrapper.ts +52 -0
  46. package/src/rules/no-inline-runtime-provide.ts +42 -0
  47. package/src/rules/no-instanceof-schema.ts +43 -0
  48. package/src/rules/no-json-in-effect.ts +45 -0
  49. package/src/rules/no-json-parse.ts +18 -0
  50. package/src/rules/no-manual-effect-channels.ts +59 -0
  51. package/src/rules/no-match-effect-branch.ts +83 -0
  52. package/src/rules/no-match-void-branch.ts +73 -0
  53. package/src/rules/no-nested-effect-call.ts +59 -0
  54. package/src/rules/no-nested-effect-gen.ts +44 -0
  55. package/src/rules/no-nested-pipe.ts +76 -0
  56. package/src/rules/no-new-error.ts +17 -0
  57. package/src/rules/no-new-promise.ts +14 -0
  58. package/src/rules/no-node-builtin-import.ts +70 -0
  59. package/src/rules/no-option-as.ts +13 -0
  60. package/src/rules/no-option-boolean-normalization.ts +89 -0
  61. package/src/rules/no-platform-globals.ts +117 -0
  62. package/src/rules/no-process-env-in-effect.ts +39 -0
  63. package/src/rules/no-process-env.ts +13 -0
  64. package/src/rules/no-random-in-effect.ts +39 -0
  65. package/src/rules/no-return-in-arrow.ts +77 -0
  66. package/src/rules/no-return-null.ts +47 -0
  67. package/src/rules/no-run-in-effect.ts +18 -0
  68. package/src/rules/no-runtime-run-fork.ts +14 -0
  69. package/src/rules/no-string-sentinel-const.ts +50 -0
  70. package/src/rules/no-switch-statement.ts +13 -0
  71. package/src/rules/no-ternary.ts +32 -0
  72. package/src/rules/no-throw-in-effect-gen.ts +42 -0
  73. package/src/rules/no-throw-statement.ts +13 -0
  74. package/src/rules/no-timers-in-effect.ts +47 -0
  75. package/src/rules/no-try-catch-in-effect-gen.ts +42 -0
  76. package/src/rules/no-try-catch.ts +14 -0
  77. package/src/rules/no-unnecessary-arrow-block.ts +52 -0
  78. package/src/rules/no-unnecessary-effect-gen.ts +85 -0
  79. package/src/rules/no-unnecessary-pipe.ts +46 -0
  80. package/src/vendor/effect-oxlint/AST.ts +516 -0
  81. package/src/vendor/effect-oxlint/Comment.ts +86 -0
  82. package/src/vendor/effect-oxlint/Diagnostic.ts +199 -0
  83. package/src/vendor/effect-oxlint/Plugin.ts +71 -0
  84. package/src/vendor/effect-oxlint/Rule.ts +590 -0
  85. package/src/vendor/effect-oxlint/RuleContext.ts +144 -0
  86. package/src/vendor/effect-oxlint/Scope.ts +177 -0
  87. package/src/vendor/effect-oxlint/SourceCode.ts +377 -0
  88. package/src/vendor/effect-oxlint/Testing.ts +1227 -0
  89. package/src/vendor/effect-oxlint/Token.ts +131 -0
  90. package/src/vendor/effect-oxlint/Visitor.ts +287 -0
  91. package/src/vendor/effect-oxlint/index.ts +88 -0
  92. package/tests/ban-rules.test.ts +252 -0
  93. package/tests/context-rules.test.ts +281 -0
  94. package/tests/pattern-rules.test.ts +258 -0
  95. package/tsconfig.json +15 -0
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Ban nested IIFE chains — `((x) => ((y) => ...)(arg2))(arg1)`.
3
+ *
4
+ * Use named const bindings + flat pipeline.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-arrow-ladder
7
+ */
8
+ import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
9
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
10
+ import * as Effect from "effect/Effect"
11
+
12
+ const isIife = (node: unknown): boolean => {
13
+ if (node == null || typeof node !== "object") return false
14
+ const n = node as unknown as Record<string, unknown>
15
+ if (n["type"] !== "CallExpression") return false
16
+ const callee = n["callee"]
17
+ if (callee == null || typeof callee !== "object") return false
18
+ const c = callee as unknown as Record<string, unknown>
19
+ return c["type"] === "ArrowFunctionExpression" || c["type"] === "FunctionExpression"
20
+ }
21
+
22
+ const containsIife = (node: unknown): boolean => {
23
+ if (node == null || typeof node !== "object") return false
24
+ const n = node as unknown as Record<string, unknown>
25
+ if (isIife(n)) return true
26
+ if ("body" in n) {
27
+ if (Array.isArray(n["body"])) {
28
+ for (const child of n["body"]) {
29
+ if (containsIife(child)) return true
30
+ }
31
+ } else if (containsIife(n["body"])) return true
32
+ }
33
+ if ("argument" in n && containsIife(n["argument"])) return true
34
+ if ("arguments" in n && Array.isArray(n["arguments"])) {
35
+ for (const arg of n["arguments"]) {
36
+ if (containsIife(arg)) return true
37
+ }
38
+ }
39
+ if ("expression" in n && containsIife(n["expression"])) return true
40
+ return false
41
+ }
42
+
43
+ export const noArrowLadder = Rule.define({
44
+ name: "no-arrow-ladder",
45
+ meta: Rule.meta({
46
+ type: "suggestion",
47
+ description: "Avoid nested IIFE chains. Use named const bindings + flat pipeline.",
48
+ }),
49
+ create: function* () {
50
+ const ctx = yield* RuleContext
51
+ return {
52
+ CallExpression: (node) => {
53
+ if (!isIife(node)) return Effect.void
54
+
55
+ // Check if the body of the IIFE itself contains another IIFE
56
+ const callee = (node as unknown as Record<string, unknown>)["callee"]
57
+ if (callee == null || typeof callee !== "object") return Effect.void
58
+ const c = callee as unknown as Record<string, unknown>
59
+ if ("body" in c && containsIife(c["body"])) {
60
+ return ctx.report(
61
+ Diagnostic.make({
62
+ node,
63
+ message: "Nested IIFE chain detected. Use named const bindings + flat pipeline.",
64
+ }),
65
+ )
66
+ }
67
+ return Effect.void
68
+ },
69
+ }
70
+ },
71
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Ban `async` functions.
3
+ *
4
+ * Use Effect.promise, Effect.tryPromise, or Effect.gen instead.
5
+ *
6
+ * Source: language-service/asyncFunction
7
+ *
8
+ * Note: This is an effectNative rule — off by default.
9
+ * Only enable in codebases fully committed to Effect.
10
+ */
11
+ import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
12
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
13
+ import * as Effect from "effect/Effect"
14
+
15
+ export const noAsyncFunction = Rule.define({
16
+ name: "no-async-function",
17
+ meta: Rule.meta({
18
+ type: "suggestion",
19
+ description:
20
+ "Avoid async functions. Use Effect.promise or Effect.tryPromise.",
21
+ }),
22
+ create: function* () {
23
+ const ctx = yield* RuleContext
24
+ const report = (node: Parameters<typeof Diagnostic.make>[0]["node"]) =>
25
+ ctx.report(
26
+ Diagnostic.make({
27
+ node,
28
+ message:
29
+ "Avoid async functions. Use Effect.promise, Effect.tryPromise, or Effect.gen.",
30
+ }),
31
+ )
32
+
33
+ return {
34
+ FunctionDeclaration: (node) => {
35
+ if ("async" in node && node.async === true) return report(node)
36
+ return Effect.void
37
+ },
38
+ FunctionExpression: (node) => {
39
+ if ("async" in node && node.async === true) return report(node)
40
+ return Effect.void
41
+ },
42
+ ArrowFunctionExpression: (node) => {
43
+ if ("async" in node && node.async === true) return report(node)
44
+ return Effect.void
45
+ },
46
+ }
47
+ },
48
+ })
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Ban `console.*` calls inside Effect.gen/fn context.
3
+ *
4
+ * Use Effect Logger service instead.
5
+ *
6
+ * Source: language-service/globalConsoleInEffect
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule, Visitor } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Option from "effect/Option"
13
+ import * as Ref from "effect/Ref"
14
+
15
+ import { makeEffectContextTracker } from "./_effect-context.js"
16
+
17
+ const consoleMethods = ["log", "warn", "error", "info", "debug", "trace"]
18
+
19
+ export const noConsoleInEffect = Rule.define({
20
+ name: "no-console-in-effect",
21
+ meta: Rule.meta({
22
+ type: "suggestion",
23
+ description:
24
+ "Avoid console.* inside Effect.gen/fn. Use Effect Logger service.",
25
+ }),
26
+ create: function* () {
27
+ const ctx = yield* RuleContext
28
+ const [depth, tracker] = yield* makeEffectContextTracker
29
+
30
+ return Visitor.merge(tracker, {
31
+ MemberExpression: (node) =>
32
+ Effect.flatMap(Ref.get(depth), (d) => {
33
+ if (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "console", consoleMethods))) {
34
+ return ctx.report(
35
+ Diagnostic.make({
36
+ node,
37
+ message:
38
+ "Avoid console.* inside Effect context. Use Effect Logger service.",
39
+ }),
40
+ )
41
+ }
42
+ return Effect.void
43
+ }),
44
+ })
45
+ },
46
+ })
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Ban `Date.now()` and `new Date()` inside Effect.gen/fn context.
3
+ *
4
+ * Use Clock service instead.
5
+ *
6
+ * Source: language-service/globalDateInEffect
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule, Visitor } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Option from "effect/Option"
13
+ import * as Ref from "effect/Ref"
14
+
15
+ import { makeEffectContextTracker } from "./_effect-context.js"
16
+
17
+ export const noDateInEffect = Rule.define({
18
+ name: "no-date-in-effect",
19
+ meta: Rule.meta({
20
+ type: "suggestion",
21
+ description:
22
+ "Avoid Date.now() and new Date() inside Effect.gen/fn. Use Clock service.",
23
+ }),
24
+ create: function* () {
25
+ const ctx = yield* RuleContext
26
+ const [depth, tracker] = yield* makeEffectContextTracker
27
+
28
+ return Visitor.merge(
29
+ tracker,
30
+ {
31
+ MemberExpression: (node) =>
32
+ Effect.flatMap(Ref.get(depth), (d) => {
33
+ if (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "Date", "now"))) {
34
+ return ctx.report(
35
+ Diagnostic.make({ node, message: "Avoid Date.now() inside Effect context. Use Clock service." }),
36
+ )
37
+ }
38
+ return Effect.void
39
+ }),
40
+ NewExpression: (node) =>
41
+ Effect.flatMap(Ref.get(depth), (d) => {
42
+ if (
43
+ d > 0 &&
44
+ "callee" in node &&
45
+ node.callee != null &&
46
+ typeof node.callee === "object" &&
47
+ "type" in node.callee &&
48
+ node.callee.type === "Identifier" &&
49
+ "name" in node.callee &&
50
+ node.callee.name === "Date"
51
+ ) {
52
+ return ctx.report(
53
+ Diagnostic.make({ node, message: "Avoid new Date() inside Effect context. Use Clock service." }),
54
+ )
55
+ }
56
+ return Effect.void
57
+ }),
58
+ },
59
+ )
60
+ },
61
+ })
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ban dynamic `import()` expressions.
3
+ *
4
+ * Use static module imports.
5
+ *
6
+ * Source: biome-effect-linting-rules/prevent-dynamic-imports
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noDynamicImport = Rule.banStatement("ImportExpression", {
11
+ message: "Avoid dynamic import(). Use static module imports.",
12
+ meta: { type: "problem" },
13
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban `Effect.as`.
3
+ *
4
+ * Use `Effect.map` for value mapping or `Effect.asVoid` after explicit steps.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-as
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noEffectAs = Rule.banMember("Effect", "as", {
11
+ message:
12
+ "Avoid Effect.as. Use Effect.map for value mapping or Effect.asVoid.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban `Effect.async`.
3
+ *
4
+ * Use `Stream` or structured Effect lifecycles (acquire/use/release).
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-async
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noEffectAsync = Rule.banMember("Effect", "async", {
11
+ message:
12
+ "Avoid Effect.async. Use Stream or structured Effect lifecycles.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ban `Effect.bind`.
3
+ *
4
+ * Use flat pipe or direct top-level `Effect.gen` with yields.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-bind
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noEffectBind = Rule.banMember("Effect", "bind", {
11
+ message: "Avoid Effect.bind. Use flat pipe or Effect.gen with yields.",
12
+ meta: { type: "suggestion" },
13
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban `Effect.Do` — the builder/do-notation entry point.
3
+ *
4
+ * Use flat pipe-based flow or `Effect.gen` instead.
5
+ *
6
+ * Sources: biome-effect-linting-rules/no-effect-do, language-service/effectDoNotation
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noEffectDo = Rule.banMember("Effect", "Do", {
11
+ message:
12
+ "Avoid Effect.Do builder notation. Use flat pipe-based flow or Effect.gen.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ban `Effect.fn(function* () { ... })` called with a generator argument.
3
+ *
4
+ * Effect.fn should wrap a plain function or arrow that builds a pipeline,
5
+ * not a generator (use Effect.gen for generators).
6
+ *
7
+ * Source: biome-effect-linting-rules/no-effect-fn-generator
8
+ *
9
+ * Note: This rule may be too aggressive. In Effect v4, `Effect.fn("name")(function*() {...})`
10
+ * is actually the idiomatic pattern. This rule targets `Effect.fn(function*() {...})` without
11
+ * a name span, which is the discouraged pattern from the biome rules.
12
+ */
13
+ import type { ESTree } from "@oxlint/plugins"
14
+ import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
15
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
16
+ import * as Effect from "effect/Effect"
17
+
18
+ export const noEffectFnGenerator = Rule.define({
19
+ name: "no-effect-fn-generator",
20
+ meta: Rule.meta({
21
+ type: "suggestion",
22
+ description:
23
+ "Effect.fn should not directly wrap a generator. Use Effect.fn('name')(function*() {...}) with a span name.",
24
+ }),
25
+ create: function* () {
26
+ const ctx = yield* RuleContext
27
+ return {
28
+ CallExpression: (node) => {
29
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "fn")) return Effect.void
30
+
31
+ // Effect.fn(function*() {...}) — single arg that's a generator
32
+ if (
33
+ "arguments" in node &&
34
+ Array.isArray(node.arguments) &&
35
+ node.arguments.length === 1
36
+ ) {
37
+ const arg = node.arguments[0]
38
+ if (
39
+ arg != null &&
40
+ typeof arg === "object" &&
41
+ "type" in arg &&
42
+ arg.type === "FunctionExpression" &&
43
+ "generator" in arg &&
44
+ arg.generator === true
45
+ ) {
46
+ return ctx.report(
47
+ Diagnostic.make({
48
+ node,
49
+ message:
50
+ "Use Effect.fn('spanName')(function*() {...}) — provide a trace span name.",
51
+ }),
52
+ )
53
+ }
54
+ }
55
+ return Effect.void
56
+ },
57
+ }
58
+ },
59
+ })
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Ban deprecated adapter parameter in `Effect.gen(function*(adapter) {...})`.
3
+ *
4
+ * The adapter parameter is deprecated — just use `yield*` directly.
5
+ *
6
+ * Source: language-service/effectGenUsesAdapter
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+
13
+ export const noEffectGenAdapter = Rule.define({
14
+ name: "no-effect-gen-adapter",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description: "Deprecated adapter parameter in Effect.gen. Use `yield*` directly.",
18
+ }),
19
+ create: function* () {
20
+ const ctx = yield* RuleContext
21
+ return {
22
+ CallExpression: (node) => {
23
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "gen")) return Effect.void
24
+
25
+ if (!("arguments" in node) || !Array.isArray(node.arguments) || node.arguments.length < 1)
26
+ return Effect.void
27
+
28
+ const arg = node.arguments[0]
29
+ if (
30
+ arg != null &&
31
+ typeof arg === "object" &&
32
+ "type" in arg &&
33
+ arg.type === "FunctionExpression" &&
34
+ "generator" in arg &&
35
+ arg.generator === true &&
36
+ "params" in arg &&
37
+ Array.isArray(arg.params) &&
38
+ arg.params.length > 0
39
+ ) {
40
+ return ctx.report(
41
+ Diagnostic.make({
42
+ node,
43
+ message: "Deprecated adapter parameter in Effect.gen. Remove the parameter and use `yield*` directly.",
44
+ }),
45
+ )
46
+ }
47
+ return Effect.void
48
+ },
49
+ }
50
+ },
51
+ })
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Ban `Effect.map(() => undefined)` / `Effect.map(() => void 0)` / `Effect.map(() => {})`.
3
+ *
4
+ * Use `Effect.asVoid` instead.
5
+ *
6
+ * Source: language-service/effectMapVoid
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+
13
+ const isVoidCallback = (arg: unknown): boolean => {
14
+ if (arg == null || typeof arg !== "object" || !("type" in arg)) return false
15
+
16
+ // Arrow function: () => undefined, () => void 0, () => {}
17
+ if (arg.type === "ArrowFunctionExpression" && "body" in arg) {
18
+ const body = arg.body
19
+ if (body == null || typeof body !== "object" || !("type" in body)) return false
20
+
21
+ // () => undefined
22
+ if (body.type === "Identifier" && "name" in body && body.name === "undefined") return true
23
+
24
+ // () => void 0
25
+ if (
26
+ body.type === "UnaryExpression" &&
27
+ "operator" in body &&
28
+ body.operator === "void"
29
+ )
30
+ return true
31
+
32
+ // () => {} (empty block)
33
+ if (
34
+ body.type === "BlockStatement" &&
35
+ "body" in body &&
36
+ Array.isArray(body.body) &&
37
+ body.body.length === 0
38
+ )
39
+ return true
40
+ }
41
+
42
+ return false
43
+ }
44
+
45
+ export const noEffectMapVoid = Rule.define({
46
+ name: "no-effect-map-void",
47
+ meta: Rule.meta({
48
+ type: "suggestion",
49
+ description:
50
+ "Avoid Effect.map(() => undefined). Use Effect.asVoid instead.",
51
+ }),
52
+ create: function* () {
53
+ const ctx = yield* RuleContext
54
+ return {
55
+ CallExpression: (node) => {
56
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "map")) return Effect.void
57
+
58
+ if (
59
+ "arguments" in node &&
60
+ Array.isArray(node.arguments) &&
61
+ node.arguments.length >= 1
62
+ ) {
63
+ // Effect.map(effect, fn) or as pipe arg Effect.map(fn)
64
+ const fn =
65
+ node.arguments.length === 2
66
+ ? node.arguments[1]
67
+ : node.arguments[0]
68
+ if (isVoidCallback(fn)) {
69
+ return ctx.report(
70
+ Diagnostic.make({
71
+ node,
72
+ message: "Use Effect.asVoid instead of Effect.map(() => undefined).",
73
+ }),
74
+ )
75
+ }
76
+ }
77
+ return Effect.void
78
+ },
79
+ }
80
+ },
81
+ })
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban `Effect.never`.
3
+ *
4
+ * Use `Stream` or explicit acquire/release lifecycles instead.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-never
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noEffectNever = Rule.banMember("Effect", "never", {
11
+ message:
12
+ "Avoid Effect.never. Use Stream or explicit acquire/release lifecycles.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Ban `Effect.orElse` wrapping chains with sequencing combinators.
3
+ *
4
+ * Move error handling to a single terminal decision after the pipeline.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-orElse-ladder
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+
13
+ const sequencingCombinators = ["flatMap", "zipRight", "as", "tap", "andThen"]
14
+
15
+ const containsSequencing = (node: unknown): boolean => {
16
+ if (node == null || typeof node !== "object") return false
17
+ const n = node as Record<string, unknown>
18
+ if (n["type"] === "CallExpression" && "callee" in n) {
19
+ const callee = n["callee"] as Record<string, unknown> | null
20
+ if (
21
+ callee != null &&
22
+ callee["type"] === "MemberExpression" &&
23
+ "object" in callee &&
24
+ "property" in callee
25
+ ) {
26
+ const obj = callee["object"] as Record<string, unknown> | null
27
+ const prop = callee["property"] as Record<string, unknown> | null
28
+ if (
29
+ obj != null &&
30
+ obj["type"] === "Identifier" &&
31
+ obj["name"] === "Effect" &&
32
+ prop != null &&
33
+ prop["type"] === "Identifier" &&
34
+ typeof prop["name"] === "string" &&
35
+ sequencingCombinators.includes(prop["name"])
36
+ ) {
37
+ return true
38
+ }
39
+ }
40
+ if ("arguments" in n && Array.isArray(n["arguments"])) {
41
+ for (const arg of n["arguments"]) {
42
+ if (containsSequencing(arg)) return true
43
+ }
44
+ }
45
+ }
46
+ if ("body" in n) return containsSequencing(n["body"])
47
+ return false
48
+ }
49
+
50
+ export const noEffectOrElseLadder = Rule.define({
51
+ name: "no-effect-orElse-ladder",
52
+ meta: Rule.meta({
53
+ type: "suggestion",
54
+ description:
55
+ "Avoid Effect.orElse wrapping sequencing chains. Handle errors at a single terminal decision.",
56
+ }),
57
+ create: function* () {
58
+ const ctx = yield* RuleContext
59
+ return {
60
+ CallExpression: (node) => {
61
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "orElse")) return Effect.void
62
+
63
+ if ("arguments" in node && Array.isArray(node.arguments)) {
64
+ for (const arg of node.arguments) {
65
+ if (containsSequencing(arg)) {
66
+ return ctx.report(
67
+ Diagnostic.make({
68
+ node,
69
+ message:
70
+ "Effect.orElse wrapping sequencing chain. Move error handling to a terminal decision after the pipeline.",
71
+ }),
72
+ )
73
+ }
74
+ }
75
+ }
76
+ return Effect.void
77
+ },
78
+ }
79
+ },
80
+ })
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Ban `Effect.succeed("...")` with string literal arguments.
3
+ *
4
+ * Return domain values (Option/Either/tagged unions) instead.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-string-sentinel-return
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
10
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+
13
+ export const noEffectSucceedString = Rule.define({
14
+ name: "no-effect-succeed-string",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description:
18
+ "Avoid Effect.succeed with string literals. Return domain values (tagged unions, Option, Either).",
19
+ }),
20
+ create: function* () {
21
+ const ctx = yield* RuleContext
22
+ return {
23
+ CallExpression: (node) => {
24
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "succeed")) return Effect.void
25
+
26
+ if (
27
+ "arguments" in node &&
28
+ Array.isArray(node.arguments) &&
29
+ node.arguments.length === 1
30
+ ) {
31
+ const arg = node.arguments[0]
32
+ if (
33
+ arg != null &&
34
+ typeof arg === "object" &&
35
+ "type" in arg &&
36
+ arg.type === "Literal" &&
37
+ "value" in arg &&
38
+ typeof arg.value === "string"
39
+ ) {
40
+ return ctx.report(
41
+ Diagnostic.make({
42
+ node,
43
+ message:
44
+ "Avoid Effect.succeed with string literals. Use tagged unions, Option, or Either.",
45
+ }),
46
+ )
47
+ }
48
+ }
49
+ return Effect.void
50
+ },
51
+ }
52
+ },
53
+ })