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,63 @@
1
+ /**
2
+ * Ban `Option.fromNullable(x ?? null)` / `Option.fromNullable(x ?? undefined)`.
3
+ *
4
+ * The nullish coalescing is redundant — pass the source directly.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-fromnullable-nullish-coalesce
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 noFromNullableCoalesce = Rule.define({
14
+ name: "no-fromnullable-coalesce",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description:
18
+ "Redundant nullish coalescing in Option.fromNullable. Pass the value directly.",
19
+ }),
20
+ create: function* () {
21
+ const ctx = yield* RuleContext
22
+ return {
23
+ CallExpression: (node) => {
24
+ // Match Option.fromNullable(...) or Option.fromNullishOr(...)
25
+ const ce = node as ESTree.CallExpression
26
+ const isFromNullable =
27
+ AST.isCallOf(ce, "Option", "fromNullable") ||
28
+ AST.isCallOf(ce, "Option", "fromNullishOr")
29
+ if (!isFromNullable) return Effect.void
30
+
31
+ if ("arguments" in node && Array.isArray(node.arguments) && node.arguments.length >= 1) {
32
+ const arg = node.arguments[0]
33
+ if (
34
+ arg != null &&
35
+ typeof arg === "object" &&
36
+ "type" in arg &&
37
+ arg.type === "LogicalExpression" &&
38
+ "operator" in arg &&
39
+ arg.operator === "??"
40
+ ) {
41
+ // Check RHS is null or undefined
42
+ if ("right" in arg && arg.right != null && typeof arg.right === "object" && "type" in arg.right) {
43
+ const rhs = arg.right
44
+ const isNullish =
45
+ (rhs.type === "Literal" && "value" in rhs && rhs.value === null) ||
46
+ (rhs.type === "Identifier" && "name" in rhs && rhs.name === "undefined")
47
+ if (isNullish) {
48
+ return ctx.report(
49
+ Diagnostic.make({
50
+ node,
51
+ message:
52
+ "Redundant ?? null/undefined in Option.fromNullable. Pass the value directly.",
53
+ }),
54
+ )
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return Effect.void
60
+ },
61
+ }
62
+ },
63
+ })
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Ban `console.log/warn/error/info/debug/trace`.
3
+ *
4
+ * Use Effect Logger service instead.
5
+ *
6
+ * Source: language-service/globalConsole
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noGlobalConsole = Rule.banMember(
11
+ "console",
12
+ ["log", "warn", "error", "info", "debug", "trace"],
13
+ {
14
+ message: "Avoid console.*. Use Effect Logger service.",
15
+ meta: { type: "suggestion" },
16
+ },
17
+ )
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Ban `Date.now()` and `new Date()`.
3
+ *
4
+ * Use Effect Clock service instead.
5
+ *
6
+ * Source: language-service/globalDate
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noGlobalDate = Rule.banMultiple(
11
+ {
12
+ newExprs: "Date",
13
+ members: [["Date", "now"]],
14
+ },
15
+ {
16
+ name: "no-global-date",
17
+ message: "Avoid Date.now() and new Date(). Use Clock service.",
18
+ meta: { type: "suggestion" },
19
+ },
20
+ )
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban bare `fetch()` calls.
3
+ *
4
+ * Use Effect HttpClient service instead.
5
+ *
6
+ * Source: language-service/globalFetch
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noGlobalFetch = Rule.banCallOf("fetch", {
11
+ message:
12
+ "Avoid global fetch. Use HttpClient from 'effect/unstable/http'.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ban `Math.random()`.
3
+ *
4
+ * Use Effect Random service instead.
5
+ *
6
+ * Source: language-service/globalRandom
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noGlobalRandom = Rule.banMember("Math", "random", {
11
+ message: "Avoid Math.random(). Use Random service.",
12
+ meta: { type: "suggestion" },
13
+ })
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Ban `setTimeout` and `setInterval`.
3
+ *
4
+ * Use Effect Schedule or Effect.sleep instead.
5
+ *
6
+ * Source: language-service/globalTimers
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noGlobalTimers = Rule.banCallOf(
11
+ ["setTimeout", "setInterval"],
12
+ {
13
+ message:
14
+ "Avoid setTimeout/setInterval. Use Effect.sleep or Schedule.",
15
+ meta: { type: "suggestion" },
16
+ },
17
+ )
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Ban `if` statements in Effect codebases.
3
+ *
4
+ * Use `Option.match`, `Either.match`, `Match.value`, or data combinators.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-if-statement
7
+ *
8
+ * Note: Very opinionated — recommended for strict functional presets only.
9
+ */
10
+ import { Rule } from "../vendor/effect-oxlint/index.js"
11
+
12
+ export const noIfStatement = Rule.banStatement("IfStatement", {
13
+ message:
14
+ "Avoid if statements. Use Option.match, Either.match, Match.value, or Effect.if.",
15
+ meta: { type: "suggestion" },
16
+ })
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Ban immediately-invoked function expressions (IIFEs).
3
+ *
4
+ * Use named const binding + flat pipeline.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-iife-wrapper
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
+ export const noIifeWrapper = Rule.define({
13
+ name: "no-iife-wrapper",
14
+ meta: Rule.meta({
15
+ type: "suggestion",
16
+ description: "Avoid IIFEs. Use named const bindings + flat pipeline.",
17
+ }),
18
+ create: function* () {
19
+ const ctx = yield* RuleContext
20
+ return {
21
+ CallExpression: (node) => {
22
+ if (!("callee" in node)) return Effect.void
23
+ const callee = node.callee
24
+ if (callee == null || typeof callee !== "object" || !("type" in callee)) return Effect.void
25
+
26
+ const isWrappedFn =
27
+ callee.type === "ArrowFunctionExpression" ||
28
+ callee.type === "FunctionExpression"
29
+
30
+ // Also catch (expr)(...) where expr is parenthesized fn
31
+ const isParenWrapped =
32
+ callee.type === "ParenthesizedExpression" &&
33
+ "expression" in callee &&
34
+ callee.expression != null &&
35
+ typeof callee.expression === "object" &&
36
+ "type" in callee.expression &&
37
+ (callee.expression.type === "ArrowFunctionExpression" ||
38
+ callee.expression.type === "FunctionExpression")
39
+
40
+ if (isWrappedFn || isParenWrapped) {
41
+ return ctx.report(
42
+ Diagnostic.make({
43
+ node,
44
+ message: "Avoid IIFEs. Use named const binding + flat pipeline.",
45
+ }),
46
+ )
47
+ }
48
+ return Effect.void
49
+ },
50
+ }
51
+ },
52
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Ban `Effect.provide(layer)` used inline inside Effect.gen/fn.
3
+ *
4
+ * Provide layers at the boundary, not deep in generators.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-inline-runtime-provide
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 Ref from "effect/Ref"
13
+
14
+ import { makeEffectContextTracker } from "./_effect-context.js"
15
+
16
+ export const noInlineRuntimeProvide = Rule.define({
17
+ name: "no-inline-runtime-provide",
18
+ meta: Rule.meta({
19
+ type: "suggestion",
20
+ description:
21
+ "Avoid Effect.provide inside Effect.gen/fn. Provide layers at the boundary.",
22
+ }),
23
+ create: function* () {
24
+ const ctx = yield* RuleContext
25
+ const [depth, tracker] = yield* makeEffectContextTracker
26
+
27
+ return Visitor.merge(tracker, {
28
+ CallExpression: (node) =>
29
+ Effect.flatMap(Ref.get(depth), (d) => {
30
+ if (d <= 0) return Effect.void
31
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "provide")) return Effect.void
32
+
33
+ return ctx.report(
34
+ Diagnostic.make({
35
+ node,
36
+ message: "Avoid Effect.provide inside Effect.gen/fn. Provide layers at the boundary.",
37
+ }),
38
+ )
39
+ }),
40
+ })
41
+ },
42
+ })
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Ban `instanceof` checks against Schema-derived classes.
3
+ *
4
+ * Use `Schema.is(SchemaType)` instead.
5
+ *
6
+ * Source: language-service/instanceOfSchema
7
+ *
8
+ * Note: Without type information, we detect common Effect Schema class names
9
+ * used in `instanceof` checks. This is a best-effort heuristic.
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 noInstanceofSchema = Rule.define({
16
+ name: "no-instanceof-schema",
17
+ meta: Rule.meta({
18
+ type: "suggestion",
19
+ description:
20
+ "Avoid instanceof with Schema types. Use Schema.is(SchemaType) instead.",
21
+ }),
22
+ create: function* () {
23
+ const ctx = yield* RuleContext
24
+ return {
25
+ BinaryExpression: (node) => {
26
+ if (
27
+ "operator" in node &&
28
+ node.operator === "instanceof"
29
+ ) {
30
+ // Report all instanceof — in Effect codebases, instanceof is generally wrong
31
+ return ctx.report(
32
+ Diagnostic.make({
33
+ node,
34
+ message:
35
+ "Avoid instanceof. Use Schema.is(SchemaType) for Schema types, or _tag checks for tagged types.",
36
+ }),
37
+ )
38
+ }
39
+ return Effect.void
40
+ },
41
+ }
42
+ },
43
+ })
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Ban `JSON.parse`/`JSON.stringify` inside Effect.gen/fn context.
3
+ *
4
+ * Use Schema.fromJsonString for type-safe JSON.
5
+ *
6
+ * Source: language-service/preferSchemaOverJson (inside Effect context)
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 noJsonInEffect = Rule.define({
18
+ name: "no-json-in-effect",
19
+ meta: Rule.meta({
20
+ type: "suggestion",
21
+ description: "Avoid JSON.parse/stringify inside Effect.gen/fn. Use Schema.fromJsonString.",
22
+ }),
23
+ create: function* () {
24
+ const ctx = yield* RuleContext
25
+ const [depth, tracker] = yield* makeEffectContextTracker
26
+
27
+ return Visitor.merge(tracker, {
28
+ MemberExpression: (node) =>
29
+ Effect.flatMap(Ref.get(depth), (d) => {
30
+ if (
31
+ d > 0 &&
32
+ Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "JSON", ["parse", "stringify"]))
33
+ ) {
34
+ return ctx.report(
35
+ Diagnostic.make({
36
+ node,
37
+ message: "Avoid JSON.parse/stringify inside Effect context. Use Schema.fromJsonString.",
38
+ }),
39
+ )
40
+ }
41
+ return Effect.void
42
+ }),
43
+ })
44
+ },
45
+ })
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ban `JSON.parse` and `JSON.stringify`.
3
+ *
4
+ * Use Schema.fromJsonString for type-safe JSON handling.
5
+ *
6
+ * Source: language-service/preferSchemaOverJson
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noJsonParse = Rule.banMember(
11
+ "JSON",
12
+ ["parse", "stringify"],
13
+ {
14
+ message:
15
+ "Avoid JSON.parse/stringify. Use Schema.fromJsonString for type-safe JSON.",
16
+ meta: { type: "suggestion" },
17
+ },
18
+ )
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ban manual `Effect.Effect<A, E, R>` type annotations.
3
+ *
4
+ * Let return types infer from the Effect/Layer you return.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-manual-effect-channels
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
+ export const noManualEffectChannels = Rule.define({
13
+ name: "no-manual-effect-channels",
14
+ meta: Rule.meta({
15
+ type: "suggestion",
16
+ description:
17
+ "Avoid manual Effect.Effect<A, E, R> annotations. Let types infer from the returned Effect.",
18
+ }),
19
+ create: function* () {
20
+ const ctx = yield* RuleContext
21
+ return {
22
+ TSTypeReference: (node) => {
23
+ if (!("typeName" in node) || node.typeName == null) return Effect.void
24
+ const typeName = node.typeName as unknown as Record<string, unknown>
25
+
26
+ // Effect.Effect<...> or Layer.Layer<...>
27
+ if (typeName["type"] === "TSQualifiedName" && "left" in typeName && "right" in typeName) {
28
+ const left = typeName["left"] as unknown as Record<string, unknown> | null
29
+ const right = typeName["right"] as unknown as Record<string, unknown> | null
30
+ if (
31
+ left != null && left["type"] === "Identifier" &&
32
+ (left["name"] === "Effect" || left["name"] === "Layer") &&
33
+ right != null && right["type"] === "Identifier" &&
34
+ (right["name"] === "Effect" || right["name"] === "Layer")
35
+ ) {
36
+ // Only flag if explicit type parameters are provided
37
+ if (
38
+ "typeParameters" in node &&
39
+ node.typeParameters != null &&
40
+ typeof node.typeParameters === "object" &&
41
+ "params" in (node.typeParameters as unknown as Record<string, unknown>) &&
42
+ Array.isArray((node.typeParameters as unknown as Record<string, unknown>)["params"]) &&
43
+ ((node.typeParameters as unknown as Record<string, unknown>)["params"] as unknown[]).length > 0
44
+ ) {
45
+ return ctx.report(
46
+ Diagnostic.make({
47
+ node,
48
+ message:
49
+ "Avoid manual Effect/Layer channel annotations. Let types infer from the returned value.",
50
+ }),
51
+ )
52
+ }
53
+ }
54
+ }
55
+ return Effect.void
56
+ },
57
+ }
58
+ },
59
+ })
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Ban Match.when/orElse branches that contain multi-step Effect sequencing.
3
+ *
4
+ * Select the value in Match, then run one Effect pipeline outside.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-match-effect-branch
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 sequencingMethods = new Set(["flatMap", "map", "andThen", "tap", "zipRight", "pipe"])
14
+
15
+ const containsEffectSequencing = (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 (callee != null && callee["type"] === "MemberExpression") {
21
+ const obj = callee["object"] as Record<string, unknown> | null
22
+ const prop = callee["property"] as Record<string, unknown> | null
23
+ if (
24
+ obj != null && obj["type"] === "Identifier" && obj["name"] === "Effect" &&
25
+ prop != null && prop["type"] === "Identifier" && typeof prop["name"] === "string" &&
26
+ sequencingMethods.has(prop["name"])
27
+ ) {
28
+ return true
29
+ }
30
+ }
31
+ if ("arguments" in n && Array.isArray(n["arguments"])) {
32
+ for (const arg of n["arguments"]) {
33
+ if (containsEffectSequencing(arg)) return true
34
+ }
35
+ }
36
+ }
37
+ if ("body" in n) {
38
+ if (Array.isArray(n["body"])) {
39
+ for (const child of n["body"]) {
40
+ if (containsEffectSequencing(child)) return true
41
+ }
42
+ } else if (containsEffectSequencing(n["body"])) return true
43
+ }
44
+ if ("argument" in n && containsEffectSequencing(n["argument"])) return true
45
+ if ("expression" in n && containsEffectSequencing(n["expression"])) return true
46
+ return false
47
+ }
48
+
49
+ export const noMatchEffectBranch = Rule.define({
50
+ name: "no-match-effect-branch",
51
+ meta: Rule.meta({
52
+ type: "suggestion",
53
+ description:
54
+ "Avoid Effect sequencing inside Match branches. Select value in Match, run Effect outside.",
55
+ }),
56
+ create: function* () {
57
+ const ctx = yield* RuleContext
58
+ return {
59
+ CallExpression: (node) => {
60
+ const ce = node as ESTree.CallExpression
61
+ const isMatchWhen = AST.isCallOf(ce, "Match", "when")
62
+ const isMatchOrElse = AST.isCallOf(ce, "Match", "orElse")
63
+
64
+ if (!isMatchWhen && !isMatchOrElse) return Effect.void
65
+ if (!("arguments" in node) || !Array.isArray(node.arguments)) return Effect.void
66
+
67
+ // Check the callback argument for Effect sequencing
68
+ const callbackIdx = isMatchWhen ? 1 : 0
69
+ const callback = node.arguments[callbackIdx]
70
+ if (callback != null && containsEffectSequencing(callback)) {
71
+ return ctx.report(
72
+ Diagnostic.make({
73
+ node,
74
+ message:
75
+ "Avoid Effect sequencing inside Match branches. Select value in Match, then run one Effect pipeline outside.",
76
+ }),
77
+ )
78
+ }
79
+ return Effect.void
80
+ },
81
+ }
82
+ },
83
+ })
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Ban `Match.when(true/false, () => Effect.void)` and `Match.orElse(() => Effect.void)`.
3
+ *
4
+ * Remove the no-op branch or restructure.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-match-void-branch
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 isEffectVoid = (node: unknown): boolean => {
14
+ if (node == null || typeof node !== "object") return false
15
+ const n = node as Record<string, unknown>
16
+ if (n["type"] !== "MemberExpression") return false
17
+ const obj = n["object"] as Record<string, unknown> | null
18
+ const prop = n["property"] as Record<string, unknown> | null
19
+ return (
20
+ obj != null && obj["type"] === "Identifier" && obj["name"] === "Effect" &&
21
+ prop != null && prop["type"] === "Identifier" && prop["name"] === "void"
22
+ )
23
+ }
24
+
25
+ const isVoidCallback = (node: unknown): boolean => {
26
+ if (node == null || typeof node !== "object") return false
27
+ const n = node as Record<string, unknown>
28
+ if (n["type"] !== "ArrowFunctionExpression" && n["type"] !== "FunctionExpression") return false
29
+ if (!("body" in n)) return false
30
+ return isEffectVoid(n["body"])
31
+ }
32
+
33
+ export const noMatchVoidBranch = Rule.define({
34
+ name: "no-match-void-branch",
35
+ meta: Rule.meta({
36
+ type: "suggestion",
37
+ description: "Avoid no-op Match branches returning Effect.void. Remove the branch or restructure.",
38
+ }),
39
+ create: function* () {
40
+ const ctx = yield* RuleContext
41
+ return {
42
+ CallExpression: (node) => {
43
+ const ce = node as ESTree.CallExpression
44
+ const isMatchWhen = AST.isCallOf(ce, "Match", "when")
45
+ const isMatchOrElse = AST.isCallOf(ce, "Match", "orElse")
46
+
47
+ if (!isMatchWhen && !isMatchOrElse) return Effect.void
48
+
49
+ if (!("arguments" in node) || !Array.isArray(node.arguments)) return Effect.void
50
+
51
+ if (isMatchOrElse && node.arguments.length >= 1 && isVoidCallback(node.arguments[0])) {
52
+ return ctx.report(
53
+ Diagnostic.make({
54
+ node,
55
+ message: "Match.orElse returning Effect.void is a no-op. Remove the branch or restructure.",
56
+ }),
57
+ )
58
+ }
59
+
60
+ if (isMatchWhen && node.arguments.length >= 2 && isVoidCallback(node.arguments[1])) {
61
+ return ctx.report(
62
+ Diagnostic.make({
63
+ node,
64
+ message: "Match.when branch returning Effect.void is a no-op. Remove the branch or restructure.",
65
+ }),
66
+ )
67
+ }
68
+
69
+ return Effect.void
70
+ },
71
+ }
72
+ },
73
+ })
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ban nested Effect.* calls — `Effect.outer(Effect.inner(...))`.
3
+ *
4
+ * Build values first, then one flat pipeline.
5
+ *
6
+ * Sources: biome-effect-linting-rules/no-nested-effect-call, no-effect-call-in-effect-arg, no-call-tower, no-effect-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
+ import * as Option from "effect/Option"
13
+
14
+ const isEffectCall = (node: unknown): boolean => {
15
+ if (node == null || typeof node !== "object") return false
16
+ const n = node as Record<string, unknown>
17
+ if (n["type"] !== "CallExpression") return false
18
+ const callee = n["callee"]
19
+ if (callee == null || typeof callee !== "object") return false
20
+ const c = callee as Record<string, unknown>
21
+ if (c["type"] !== "MemberExpression") return false
22
+ const obj = c["object"]
23
+ if (obj == null || typeof obj !== "object") return false
24
+ const o = obj as Record<string, unknown>
25
+ return o["type"] === "Identifier" && o["name"] === "Effect"
26
+ }
27
+
28
+ export const noNestedEffectCall = Rule.define({
29
+ name: "no-nested-effect-call",
30
+ meta: Rule.meta({
31
+ type: "suggestion",
32
+ description:
33
+ "Avoid nesting Effect.* calls. Build values first, then use a flat pipeline.",
34
+ }),
35
+ create: function* () {
36
+ const ctx = yield* RuleContext
37
+ return {
38
+ CallExpression: (node) => {
39
+ if (!isEffectCall(node)) return Effect.void
40
+
41
+ const ce = node as ESTree.CallExpression
42
+ if (!("arguments" in ce) || !Array.isArray(ce.arguments)) return Effect.void
43
+
44
+ for (const arg of ce.arguments) {
45
+ if (isEffectCall(arg)) {
46
+ return ctx.report(
47
+ Diagnostic.make({
48
+ node,
49
+ message:
50
+ "Nested Effect.* call detected. Build values first, then use a flat pipeline.",
51
+ }),
52
+ )
53
+ }
54
+ }
55
+ return Effect.void
56
+ },
57
+ }
58
+ },
59
+ })