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,51 @@
1
+ /**
2
+ * Ban `Effect.succeed(someVariable)` where the argument is a plain identifier.
3
+ *
4
+ * Suggests selecting a plain value (Option/Match), then running one Effect pipeline.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-succeed-variable
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 noEffectSucceedVariable = Rule.define({
14
+ name: "no-effect-succeed-variable",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description:
18
+ "Avoid Effect.succeed with a plain variable. Select value with Option/Match, then run one Effect pipeline.",
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 === "Identifier"
37
+ ) {
38
+ return ctx.report(
39
+ Diagnostic.make({
40
+ node,
41
+ message:
42
+ "Avoid Effect.succeed(variable). Select value with Option/Match, then run one Effect pipeline.",
43
+ }),
44
+ )
45
+ }
46
+ }
47
+ return Effect.void
48
+ },
49
+ }
50
+ },
51
+ })
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Ban `Effect.succeed(undefined)` and `Effect.succeed(void 0)`.
3
+ *
4
+ * Use `Effect.void` instead.
5
+ *
6
+ * Source: language-service/effectSucceedWithVoid
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 isUndefinedLike = (node: unknown): boolean => {
14
+ if (node == null || typeof node !== "object" || !("type" in node)) return false
15
+ // undefined identifier
16
+ if (node.type === "Identifier" && "name" in node && node.name === "undefined") return true
17
+ // void 0
18
+ if (
19
+ node.type === "UnaryExpression" &&
20
+ "operator" in node &&
21
+ node.operator === "void" &&
22
+ "argument" in node &&
23
+ node.argument != null &&
24
+ typeof node.argument === "object" &&
25
+ "type" in node.argument &&
26
+ node.argument.type === "Literal" &&
27
+ "value" in node.argument &&
28
+ node.argument.value === 0
29
+ )
30
+ return true
31
+ return false
32
+ }
33
+
34
+ export const noEffectSucceedVoid = Rule.define({
35
+ name: "no-effect-succeed-void",
36
+ meta: Rule.meta({
37
+ type: "suggestion",
38
+ description:
39
+ "Avoid Effect.succeed(undefined). Use Effect.void instead.",
40
+ }),
41
+ create: function* () {
42
+ const ctx = yield* RuleContext
43
+ return {
44
+ CallExpression: (node) => {
45
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "succeed")) return Effect.void
46
+
47
+ if (
48
+ "arguments" in node &&
49
+ Array.isArray(node.arguments) &&
50
+ node.arguments.length === 1 &&
51
+ isUndefinedLike(node.arguments[0])
52
+ ) {
53
+ return ctx.report(
54
+ Diagnostic.make({
55
+ node,
56
+ message: "Use Effect.void instead of Effect.succeed(undefined).",
57
+ }),
58
+ )
59
+ }
60
+ return Effect.void
61
+ },
62
+ }
63
+ },
64
+ })
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Ban `console.*` inside `Effect.sync(...)`.
3
+ *
4
+ * Use Effect.log* instead.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-sync-console
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 consoleMethods = new Set(["log", "warn", "error", "info", "debug", "trace"])
14
+
15
+ const containsConsoleCall = (node: unknown): boolean => {
16
+ if (node == null || typeof node !== "object") return false
17
+ const n = node as Record<string, unknown>
18
+ if (
19
+ n["type"] === "CallExpression" &&
20
+ "callee" in n &&
21
+ n["callee"] != null &&
22
+ typeof n["callee"] === "object"
23
+ ) {
24
+ const callee = n["callee"] as Record<string, unknown>
25
+ if (
26
+ callee["type"] === "MemberExpression" &&
27
+ "object" in callee &&
28
+ "property" in callee
29
+ ) {
30
+ const obj = callee["object"] as Record<string, unknown> | null
31
+ const prop = callee["property"] as Record<string, unknown> | null
32
+ if (
33
+ obj != null && obj["type"] === "Identifier" && obj["name"] === "console" &&
34
+ prop != null && prop["type"] === "Identifier" && typeof prop["name"] === "string" &&
35
+ consoleMethods.has(prop["name"])
36
+ ) {
37
+ return true
38
+ }
39
+ }
40
+ }
41
+ // Recurse into body/arguments
42
+ if ("body" in n) {
43
+ if (Array.isArray(n["body"])) {
44
+ for (const child of n["body"]) {
45
+ if (containsConsoleCall(child)) return true
46
+ }
47
+ } else if (containsConsoleCall(n["body"])) return true
48
+ }
49
+ if ("argument" in n && containsConsoleCall(n["argument"])) return true
50
+ if ("arguments" in n && Array.isArray(n["arguments"])) {
51
+ for (const arg of n["arguments"]) {
52
+ if (containsConsoleCall(arg)) return true
53
+ }
54
+ }
55
+ if ("expression" in n && containsConsoleCall(n["expression"])) return true
56
+ if ("expressions" in n && Array.isArray(n["expressions"])) {
57
+ for (const expr of n["expressions"]) {
58
+ if (containsConsoleCall(expr)) return true
59
+ }
60
+ }
61
+ return false
62
+ }
63
+
64
+ export const noEffectSyncConsole = Rule.define({
65
+ name: "no-effect-sync-console",
66
+ meta: Rule.meta({
67
+ type: "suggestion",
68
+ description: "Avoid console.* inside Effect.sync. Use Effect.log* instead.",
69
+ }),
70
+ create: function* () {
71
+ const ctx = yield* RuleContext
72
+ return {
73
+ CallExpression: (node) => {
74
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "sync")) return Effect.void
75
+
76
+ if ("arguments" in node && Array.isArray(node.arguments)) {
77
+ for (const arg of node.arguments) {
78
+ if (containsConsoleCall(arg)) {
79
+ return ctx.report(
80
+ Diagnostic.make({
81
+ node,
82
+ message: "Avoid console.* inside Effect.sync. Use Effect.log, Effect.logWarning, etc.",
83
+ }),
84
+ )
85
+ }
86
+ }
87
+ }
88
+ return Effect.void
89
+ },
90
+ }
91
+ },
92
+ })
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Warn on `Effect.sync(() => someCall(...))` wrapping a plain function call.
3
+ *
4
+ * Consider whether the wrapped call truly needs to be in Effect.sync.
5
+ *
6
+ * Source: biome-effect-linting-rules/warn-effect-sync-wrapper
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 noEffectSyncWrapper = Rule.define({
14
+ name: "no-effect-sync-wrapper",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description: "Effect.sync wrapping a plain call. Consider if Effect.sync is necessary.",
18
+ }),
19
+ create: function* () {
20
+ const ctx = yield* RuleContext
21
+ return {
22
+ CallExpression: (node) => {
23
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "sync")) 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 (arg == null || typeof arg !== "object" || !("type" in arg)) return Effect.void
30
+
31
+ // () => someCall(...)
32
+ if (arg.type === "ArrowFunctionExpression" && "body" in arg) {
33
+ const body = arg.body
34
+ if (
35
+ body != null &&
36
+ typeof body === "object" &&
37
+ "type" in body &&
38
+ body.type === "CallExpression"
39
+ ) {
40
+ // Exclude console.* (handled by no-effect-sync-console)
41
+ const callee = (body as unknown as Record<string, unknown>)["callee"]
42
+ if (
43
+ callee != null &&
44
+ typeof callee === "object" &&
45
+ "type" in callee &&
46
+ callee.type === "MemberExpression" &&
47
+ "object" in callee &&
48
+ callee.object != null &&
49
+ typeof callee.object === "object" &&
50
+ "name" in callee.object &&
51
+ callee.object.name === "console"
52
+ ) {
53
+ return Effect.void
54
+ }
55
+
56
+ return ctx.report(
57
+ Diagnostic.make({
58
+ node,
59
+ message: "Effect.sync wrapping a plain function call. Consider if Effect.sync is necessary.",
60
+ }),
61
+ )
62
+ }
63
+ }
64
+ return Effect.void
65
+ },
66
+ }
67
+ },
68
+ })
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Ban `type X = Effect.Effect<...>` type aliases.
3
+ *
4
+ * Keep Effect types on service methods or inline at call site.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-type-alias
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 containsEffectType = (node: unknown): boolean => {
13
+ if (node == null || typeof node !== "object") return false
14
+ const n = node as Record<string, unknown>
15
+ // TSTypeReference with Effect.Effect
16
+ if (
17
+ n["type"] === "TSTypeReference" &&
18
+ "typeName" in n &&
19
+ n["typeName"] != null &&
20
+ typeof n["typeName"] === "object"
21
+ ) {
22
+ const typeName = n["typeName"] as Record<string, unknown>
23
+ if (
24
+ typeName["type"] === "TSQualifiedName" &&
25
+ "left" in typeName && "right" in typeName
26
+ ) {
27
+ const left = typeName["left"] as Record<string, unknown> | null
28
+ const right = typeName["right"] as Record<string, unknown> | null
29
+ if (
30
+ left != null && left["type"] === "Identifier" && left["name"] === "Effect" &&
31
+ right != null && right["type"] === "Identifier" && right["name"] === "Effect"
32
+ ) {
33
+ return true
34
+ }
35
+ }
36
+ }
37
+ // Recurse into typeAnnotation, typeParameters
38
+ if ("typeAnnotation" in n && containsEffectType(n["typeAnnotation"])) return true
39
+ if ("typeParameters" in n && n["typeParameters"] != null && typeof n["typeParameters"] === "object") {
40
+ const tp = n["typeParameters"] as Record<string, unknown>
41
+ if ("params" in tp && Array.isArray(tp["params"])) {
42
+ for (const p of tp["params"]) {
43
+ if (containsEffectType(p)) return true
44
+ }
45
+ }
46
+ }
47
+ return false
48
+ }
49
+
50
+ export const noEffectTypeAlias = Rule.define({
51
+ name: "no-effect-type-alias",
52
+ meta: Rule.meta({
53
+ type: "suggestion",
54
+ description: "Avoid Effect type aliases. Keep Effect types on service methods or inline.",
55
+ }),
56
+ create: function* () {
57
+ const ctx = yield* RuleContext
58
+ return {
59
+ TSTypeAliasDeclaration: (node) => {
60
+ if ("typeAnnotation" in node && containsEffectType(node.typeAnnotation)) {
61
+ return ctx.report(
62
+ Diagnostic.make({
63
+ node,
64
+ message: "Avoid type alias wrapping Effect.Effect. Let Effect types flow from service methods.",
65
+ }),
66
+ )
67
+ }
68
+ return Effect.void
69
+ },
70
+ }
71
+ },
72
+ })
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Ban wrapper functions that just alias an Effect.fn or pipe(Effect.fn(...)).
3
+ *
4
+ * Inline the pipeline at the call site or define a real domain function.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-effect-wrapper-alias
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 isEffectFnCall = (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"] as Record<string, unknown> | null
23
+ const prop = c["property"] as Record<string, unknown> | null
24
+ return (
25
+ obj != null && obj["type"] === "Identifier" && obj["name"] === "Effect" &&
26
+ prop != null && prop["type"] === "Identifier" && prop["name"] === "fn"
27
+ )
28
+ }
29
+
30
+ const isPipeWrappingEffectFn = (node: unknown): boolean => {
31
+ if (node == null || typeof node !== "object") return false
32
+ const n = node as Record<string, unknown>
33
+ if (n["type"] !== "CallExpression") return false
34
+ const callee = n["callee"]
35
+ if (callee == null || typeof callee !== "object") return false
36
+ const c = callee as Record<string, unknown>
37
+ if (!(c["type"] === "Identifier" && c["name"] === "pipe")) return false
38
+ const args = n["arguments"]
39
+ if (!Array.isArray(args) || args.length === 0) return false
40
+ return isEffectFnCall(args[0])
41
+ }
42
+
43
+ export const noEffectWrapperAlias = Rule.define({
44
+ name: "no-effect-wrapper-alias",
45
+ meta: Rule.meta({
46
+ type: "suggestion",
47
+ description: "Avoid wrapper functions aliasing Effect.fn. Inline or define a real domain function.",
48
+ }),
49
+ create: function* () {
50
+ const ctx = yield* RuleContext
51
+ return {
52
+ VariableDeclarator: (node) => {
53
+ if (!("init" in node) || node.init == null) return Effect.void
54
+ const init = node.init
55
+
56
+ // const x = Effect.fn(...)
57
+ if (isEffectFnCall(init)) {
58
+ return ctx.report(
59
+ Diagnostic.make({
60
+ node,
61
+ message: "Avoid aliasing Effect.fn. Inline the pipeline or define a real domain function.",
62
+ }),
63
+ )
64
+ }
65
+
66
+ // const x = pipe(Effect.fn(...), ...)
67
+ if (isPipeWrappingEffectFn(init)) {
68
+ return ctx.report(
69
+ Diagnostic.make({
70
+ node,
71
+ message: "Avoid wrapping Effect.fn in pipe. Inline the pipeline or define a real domain function.",
72
+ }),
73
+ )
74
+ }
75
+
76
+ // const x = () => Effect.fn(...) or const x = () => pipe(Effect.fn(...))
77
+ if (
78
+ typeof init === "object" &&
79
+ "type" in init &&
80
+ (init.type === "ArrowFunctionExpression" || init.type === "FunctionExpression") &&
81
+ "body" in init
82
+ ) {
83
+ const body = init.body
84
+ if (isEffectFnCall(body) || isPipeWrappingEffectFn(body)) {
85
+ return ctx.report(
86
+ Diagnostic.make({
87
+ node,
88
+ message: "Avoid wrapper function aliasing Effect.fn. Inline or define a real domain function.",
89
+ }),
90
+ )
91
+ }
92
+ }
93
+
94
+ return Effect.void
95
+ },
96
+ }
97
+ },
98
+ })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Ban `class Foo extends Error`.
3
+ *
4
+ * Use Schema.TaggedErrorClass or Data.TaggedError instead.
5
+ *
6
+ * Source: language-service/extendsNativeError
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 nativeErrors = new Set([
13
+ "Error",
14
+ "TypeError",
15
+ "RangeError",
16
+ "ReferenceError",
17
+ "SyntaxError",
18
+ "URIError",
19
+ "EvalError",
20
+ ])
21
+
22
+ export const noExtendsNativeError = Rule.define({
23
+ name: "no-extends-native-error",
24
+ meta: Rule.meta({
25
+ type: "suggestion",
26
+ description:
27
+ "Avoid extending native Error. Use Schema.TaggedErrorClass or Data.TaggedError.",
28
+ }),
29
+ create: function* () {
30
+ const ctx = yield* RuleContext
31
+ return {
32
+ ClassDeclaration: (node) => {
33
+ if (
34
+ "superClass" in node &&
35
+ node.superClass != null &&
36
+ typeof node.superClass === "object" &&
37
+ "type" in node.superClass &&
38
+ node.superClass.type === "Identifier" &&
39
+ "name" in node.superClass &&
40
+ typeof node.superClass.name === "string" &&
41
+ nativeErrors.has(node.superClass.name)
42
+ ) {
43
+ return ctx.report(
44
+ Diagnostic.make({
45
+ node,
46
+ message:
47
+ "Avoid extending native Error. Use Schema.TaggedErrorClass or Data.TaggedError.",
48
+ }),
49
+ )
50
+ }
51
+ return Effect.void
52
+ },
53
+ }
54
+ },
55
+ })
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Ban `fetch()` inside Effect.gen/fn context.
3
+ *
4
+ * Use HttpClient from 'effect/unstable/http' instead.
5
+ *
6
+ * Source: language-service/globalFetchInEffect
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 noFetchInEffect = Rule.define({
18
+ name: "no-fetch-in-effect",
19
+ meta: Rule.meta({
20
+ type: "suggestion",
21
+ description:
22
+ "Avoid fetch() inside Effect.gen/fn. Use HttpClient from 'effect/unstable/http'.",
23
+ }),
24
+ create: function* () {
25
+ const ctx = yield* RuleContext
26
+ const [depth, tracker] = yield* makeEffectContextTracker
27
+
28
+ return Visitor.merge(tracker, {
29
+ CallExpression: (node) =>
30
+ Effect.flatMap(Ref.get(depth), (d) => {
31
+ if (d > 0) {
32
+ const name = Option.getOrUndefined(AST.calleeName(node as ESTree.CallExpression))
33
+ if (name === "fetch") {
34
+ return ctx.report(
35
+ Diagnostic.make({
36
+ node,
37
+ message:
38
+ "Avoid fetch() inside Effect context. Use HttpClient from 'effect/unstable/http'.",
39
+ }),
40
+ )
41
+ }
42
+ }
43
+ return Effect.void
44
+ }),
45
+ })
46
+ },
47
+ })
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Ban nested `Effect.flatMap` calls (flatMap ladder).
3
+ *
4
+ * Build context once with `Effect.all`/`Effect.map` and run a single flatMap.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-flatmap-ladder
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
+ const isEffectFlatMap = (node: ESTree.CallExpression): boolean =>
15
+ AST.isCallOf(node, "Effect", "flatMap")
16
+
17
+ const containsEffectFlatMap = (node: unknown): boolean => {
18
+ if (node == null || typeof node !== "object") return false
19
+ const n = node as Record<string, unknown>
20
+ if (n["type"] === "CallExpression" && "callee" in n && "arguments" in n) {
21
+ if (isEffectFlatMap(node as ESTree.CallExpression)) return true
22
+ }
23
+ if ("arguments" in n && Array.isArray(n["arguments"])) {
24
+ for (const arg of n["arguments"]) {
25
+ if (containsEffectFlatMap(arg)) return true
26
+ }
27
+ }
28
+ if ("body" in n) return containsEffectFlatMap(n["body"])
29
+ return false
30
+ }
31
+
32
+ export const noFlatmapLadder = Rule.define({
33
+ name: "no-flatmap-ladder",
34
+ meta: Rule.meta({
35
+ type: "suggestion",
36
+ description:
37
+ "Avoid nested Effect.flatMap (flatMap ladder). Build context once with Effect.all and run a single flatMap.",
38
+ }),
39
+ create: function* () {
40
+ const ctx = yield* RuleContext
41
+ return {
42
+ CallExpression: (node) => {
43
+ const ce = node as ESTree.CallExpression
44
+ if (!isEffectFlatMap(ce)) return Effect.void
45
+ if (!("arguments" in ce) || !Array.isArray(ce.arguments)) return Effect.void
46
+
47
+ // Check if any argument's subtree contains another Effect.flatMap
48
+ for (const arg of ce.arguments) {
49
+ if (arg != null && typeof arg === "object" && "type" in arg) {
50
+ if (arg.type === "CallExpression" && isEffectFlatMap(arg as ESTree.CallExpression)) {
51
+ return ctx.report(
52
+ Diagnostic.make({
53
+ node,
54
+ message:
55
+ "Nested Effect.flatMap detected. Build context with Effect.all, then single flatMap.",
56
+ }),
57
+ )
58
+ }
59
+ // Check inside arrow/function body
60
+ if (
61
+ (arg.type === "ArrowFunctionExpression" || arg.type === "FunctionExpression") &&
62
+ "body" in arg &&
63
+ containsEffectFlatMap(arg.body)
64
+ ) {
65
+ return ctx.report(
66
+ Diagnostic.make({
67
+ node,
68
+ message:
69
+ "Nested Effect.flatMap detected. Build context with Effect.all, then single flatMap.",
70
+ }),
71
+ )
72
+ }
73
+ }
74
+ }
75
+ return Effect.void
76
+ },
77
+ }
78
+ },
79
+ })