oxlint-plugin-effect 0.0.0 → 0.1.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 (81) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/package.json +6 -7
  3. package/src/presets/core.ts +23 -12
  4. package/src/presets/effect-native.ts +18 -25
  5. package/src/presets/full.ts +32 -46
  6. package/src/presets/functional.ts +18 -0
  7. package/src/presets/index.ts +2 -0
  8. package/src/presets/strict.ts +77 -0
  9. package/src/rules/index.ts +17 -28
  10. package/src/rules/no-arrow-ladder.ts +1 -2
  11. package/src/rules/no-async-function.ts +1 -2
  12. package/src/rules/no-catch-all-to-map-error.ts +89 -0
  13. package/src/rules/no-effect-fn-generator.ts +1 -2
  14. package/src/rules/no-effect-gen-adapter.ts +1 -2
  15. package/src/rules/no-effect-map-flatten.ts +82 -0
  16. package/src/rules/no-effect-map-void.ts +1 -2
  17. package/src/rules/no-effect-orElse-ladder.ts +1 -2
  18. package/src/rules/no-effect-succeed-string.ts +1 -2
  19. package/src/rules/no-effect-succeed-variable.ts +1 -2
  20. package/src/rules/no-effect-succeed-void.ts +1 -2
  21. package/src/rules/no-effect-sync-console.ts +1 -2
  22. package/src/rules/no-effect-sync-wrapper.ts +1 -2
  23. package/src/rules/no-effect-type-alias.ts +1 -2
  24. package/src/rules/no-effect-wrapper-alias.ts +1 -4
  25. package/src/rules/no-extends-native-error.ts +1 -2
  26. package/src/rules/no-flatmap-ladder.ts +1 -3
  27. package/src/rules/no-fromnullable-coalesce.ts +1 -2
  28. package/src/rules/no-global-error-in-catch.ts +104 -0
  29. package/src/rules/no-global-error-in-failure.ts +83 -0
  30. package/src/rules/no-globals.ts +178 -0
  31. package/src/rules/no-iife-wrapper.ts +1 -2
  32. package/src/rules/{no-inline-runtime-provide.ts → no-inline-provide.ts} +3 -4
  33. package/src/rules/no-instanceof-schema.ts +2 -3
  34. package/src/rules/no-manual-effect-channels.ts +1 -2
  35. package/src/rules/no-match-effect-branch.ts +1 -2
  36. package/src/rules/no-match-void-branch.ts +1 -2
  37. package/src/rules/no-multiple-effect-provide.ts +75 -0
  38. package/src/rules/no-nested-effect-call.ts +1 -3
  39. package/src/rules/no-nested-effect-gen.ts +1 -2
  40. package/src/rules/no-nested-pipe.ts +1 -4
  41. package/src/rules/no-new-error.ts +46 -10
  42. package/src/rules/no-new-promise.ts +43 -7
  43. package/src/rules/no-node-builtin-import.ts +1 -3
  44. package/src/rules/no-option-boolean-normalization.ts +1 -2
  45. package/src/rules/no-positional-log-error.ts +76 -0
  46. package/src/rules/no-redundant-schema-tag-identifier.ts +62 -0
  47. package/src/rules/no-return-in-arrow.ts +1 -2
  48. package/src/rules/no-return-nullish.ts +80 -0
  49. package/src/rules/no-schema-struct-with-tag.ts +64 -0
  50. package/src/rules/no-schema-union-of-literals.ts +48 -0
  51. package/src/rules/no-string-sentinel-const.ts +1 -2
  52. package/src/rules/no-ternary.ts +1 -3
  53. package/src/rules/no-throw-statement.ts +32 -6
  54. package/src/rules/no-try-catch.ts +32 -7
  55. package/src/rules/no-unnecessary-arrow-block.ts +1 -2
  56. package/src/rules/no-unnecessary-effect-gen.ts +1 -3
  57. package/src/rules/no-unnecessary-pipe-chain.ts +47 -0
  58. package/src/rules/no-unnecessary-pipe.ts +1 -2
  59. package/src/vendor/effect-oxlint/Rule.ts +111 -173
  60. package/tests/ban-rules.test.ts +0 -167
  61. package/tests/context-rules.test.ts +75 -158
  62. package/tests/pattern-rules.test.ts +39 -50
  63. package/src/rules/no-console-in-effect.ts +0 -46
  64. package/src/rules/no-date-in-effect.ts +0 -61
  65. package/src/rules/no-dynamic-import.ts +0 -13
  66. package/src/rules/no-fetch-in-effect.ts +0 -47
  67. package/src/rules/no-global-console.ts +0 -17
  68. package/src/rules/no-global-date.ts +0 -20
  69. package/src/rules/no-global-fetch.ts +0 -14
  70. package/src/rules/no-global-random.ts +0 -13
  71. package/src/rules/no-global-timers.ts +0 -17
  72. package/src/rules/no-json-in-effect.ts +0 -45
  73. package/src/rules/no-json-parse.ts +0 -18
  74. package/src/rules/no-platform-globals.ts +0 -117
  75. package/src/rules/no-process-env-in-effect.ts +0 -39
  76. package/src/rules/no-process-env.ts +0 -13
  77. package/src/rules/no-random-in-effect.ts +0 -39
  78. package/src/rules/no-return-null.ts +0 -47
  79. package/src/rules/no-throw-in-effect-gen.ts +0 -42
  80. package/src/rules/no-timers-in-effect.ts +0 -47
  81. package/src/rules/no-try-catch-in-effect-gen.ts +0 -42
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Ban `.map(f).flatMap(g)` — use `.flatMap(x => g(f(x)))` or refactor.
3
+ *
4
+ * Detects `.pipe(Effect.map(f), Effect.flatMap(g))` chains where
5
+ * the map + flatMap can be fused.
6
+ *
7
+ * Source: language-service/effectMapFlatten
8
+ */
9
+ import type { ESTree } from "@oxlint/plugins"
10
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Option from "effect/Option"
13
+
14
+ const isEffectMap = (node: ESTree.Node): boolean => {
15
+ if (node.type !== "CallExpression") return false
16
+ return Option.isSome(AST.matchCallOf(node as ESTree.CallExpression, "Effect", "map"))
17
+ }
18
+
19
+ const isEffectFlatMap = (node: ESTree.Node): boolean => {
20
+ if (node.type !== "CallExpression") return false
21
+ return Option.isSome(AST.matchCallOf(node as ESTree.CallExpression, "Effect", "flatMap"))
22
+ }
23
+
24
+ const hasMapFlatMapSequence = (args: ReadonlyArray<ESTree.Node>): boolean => {
25
+ for (let i = 0; i < args.length - 1; i++) {
26
+ const a = args[i]; const b = args[i + 1]; if (a && b && isEffectMap(a) && isEffectFlatMap(b)) return true
27
+ }
28
+ return false
29
+ }
30
+
31
+ export const noEffectMapFlatten = Rule.define({
32
+ name: "no-effect-map-flatten",
33
+ meta: Rule.meta({
34
+ type: "suggestion",
35
+ description:
36
+ "Avoid Effect.map followed by Effect.flatMap in pipe. Fuse into a single Effect.flatMap.",
37
+ }),
38
+ create: function* () {
39
+ const ctx = yield* RuleContext
40
+ return {
41
+ CallExpression: (node) => {
42
+ const call = node as ESTree.CallExpression
43
+ const args = call.arguments
44
+
45
+ // Bare pipe(value, Effect.map(f), Effect.flatMap(g))
46
+ const calleeName = Option.getOrUndefined(AST.calleeName(call))
47
+ if (calleeName === "pipe" && hasMapFlatMapSequence(args as ReadonlyArray<ESTree.Node>)) {
48
+ return ctx.report(
49
+ Diagnostic.make({
50
+ node,
51
+ message:
52
+ "Effect.map followed by Effect.flatMap can be fused into a single Effect.flatMap.",
53
+ }),
54
+ )
55
+ }
56
+
57
+ // .pipe(Effect.map(f), Effect.flatMap(g))
58
+ const calleeNode = call.callee
59
+ if (calleeNode.type === "MemberExpression") {
60
+ const prop = (calleeNode as unknown as Record<string, unknown>)["property"]
61
+ if (
62
+ prop != null &&
63
+ typeof prop === "object" &&
64
+ "name" in prop &&
65
+ prop.name === "pipe" &&
66
+ hasMapFlatMapSequence(args as ReadonlyArray<ESTree.Node>)
67
+ ) {
68
+ return ctx.report(
69
+ Diagnostic.make({
70
+ node,
71
+ message:
72
+ "Effect.map followed by Effect.flatMap can be fused into a single Effect.flatMap.",
73
+ }),
74
+ )
75
+ }
76
+ }
77
+
78
+ return Effect.void
79
+ },
80
+ }
81
+ },
82
+ })
@@ -6,8 +6,7 @@
6
6
  * Source: language-service/effectMapVoid
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  const isVoidCallback = (arg: unknown): boolean => {
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-effect-orElse-ladder
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  const sequencingCombinators = ["flatMap", "zipRight", "as", "tap", "andThen"]
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-string-sentinel-return
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  export const noEffectSucceedString = Rule.define({
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-effect-succeed-variable
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  export const noEffectSucceedVariable = Rule.define({
@@ -6,8 +6,7 @@
6
6
  * Source: language-service/effectSucceedWithVoid
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  const isUndefinedLike = (node: unknown): boolean => {
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-effect-sync-console
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  const consoleMethods = new Set(["log", "warn", "error", "info", "debug", "trace"])
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/warn-effect-sync-wrapper
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  export const noEffectSyncWrapper = Rule.define({
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * Source: biome-effect-linting-rules/no-effect-type-alias
7
7
  */
8
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
9
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
8
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
9
  import * as Effect from "effect/Effect"
11
10
 
12
11
  const containsEffectType = (node: unknown): boolean => {
@@ -5,11 +5,8 @@
5
5
  *
6
6
  * Source: biome-effect-linting-rules/no-effect-wrapper-alias
7
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"
8
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
9
  import * as Effect from "effect/Effect"
12
- import * as Option from "effect/Option"
13
10
 
14
11
  const isEffectFnCall = (node: unknown): boolean => {
15
12
  if (node == null || typeof node !== "object") return false
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * Source: language-service/extendsNativeError
7
7
  */
8
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
9
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
8
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
9
  import * as Effect from "effect/Effect"
11
10
 
12
11
  const nativeErrors = new Set([
@@ -6,10 +6,8 @@
6
6
  * Source: biome-effect-linting-rules/no-flatmap-ladder
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
- import * as Ref from "effect/Ref"
13
11
 
14
12
  const isEffectFlatMap = (node: ESTree.CallExpression): boolean =>
15
13
  AST.isCallOf(node, "Effect", "flatMap")
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-fromnullable-nullish-coalesce
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  export const noFromNullableCoalesce = Rule.define({
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Ban `new Error()` inside Effect catch handlers.
3
+ *
4
+ * Catches: `.catchAll(e => new Error(...))`, `.catch(e => new Error(...))`
5
+ * Use Schema.TaggedErrorClass or Data.TaggedError instead.
6
+ *
7
+ * Source: language-service/globalErrorInEffectCatch
8
+ */
9
+ import type { ESTree } from "@oxlint/plugins"
10
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import * as Effect from "effect/Effect"
12
+ import * as Option from "effect/Option"
13
+
14
+ const nativeErrors = new Set([
15
+ "Error",
16
+ "TypeError",
17
+ "RangeError",
18
+ "ReferenceError",
19
+ "SyntaxError",
20
+ "URIError",
21
+ "EvalError",
22
+ ])
23
+
24
+ const catchMethods = new Set([
25
+ "catch",
26
+ "catchAll",
27
+ "catchAllCause",
28
+ "catchIf",
29
+ "catchCauseIf",
30
+ "catchTag",
31
+ "catchTags",
32
+ "catchDefect",
33
+ ])
34
+
35
+ const isNativeErrorNew = (node: ESTree.Node): boolean => {
36
+ if (node.type !== "NewExpression") return false
37
+ const callee = (node as unknown as Record<string, unknown>)["callee"] as ESTree.Node
38
+ return callee.type === "Identifier" && "name" in callee && nativeErrors.has(callee.name as string)
39
+ }
40
+
41
+ /**
42
+ * Check if a function body returns `new Error(...)`.
43
+ */
44
+ const bodyReturnsNativeError = (fn: ESTree.Node): boolean => {
45
+ const body = (fn as unknown as Record<string, unknown>)["body"] as ESTree.Node
46
+ // Arrow expression body: (e) => new Error(...)
47
+ if (body.type !== "BlockStatement") {
48
+ return isNativeErrorNew(body)
49
+ }
50
+ // Block body: (e) => { return new Error(...) }
51
+ const stmts = (body as unknown as Record<string, unknown>)["body"]
52
+ if (!Array.isArray(stmts) || stmts.length !== 1) return false
53
+ const stmt = stmts[0] as ESTree.Node
54
+ if (stmt.type !== "ReturnStatement") return false
55
+ const arg = (stmt as unknown as Record<string, unknown>)["argument"]
56
+ return arg != null && typeof arg === "object" && isNativeErrorNew(arg as ESTree.Node)
57
+ }
58
+
59
+ export const noGlobalErrorInCatch = Rule.define({
60
+ name: "no-global-error-in-catch",
61
+ meta: Rule.meta({
62
+ type: "suggestion",
63
+ description:
64
+ "Avoid native Error in Effect catch handlers. Use Schema.TaggedErrorClass or Data.TaggedError.",
65
+ }),
66
+ create: function* () {
67
+ const ctx = yield* RuleContext
68
+ return {
69
+ CallExpression: (node) => {
70
+ const call = node as ESTree.CallExpression
71
+ const names = Option.getOrUndefined(
72
+ AST.memberNames(call.callee as ESTree.MemberExpression),
73
+ )
74
+ if (names === undefined) return Effect.void
75
+ const [obj, prop] = names
76
+ if (obj !== "Effect") return Effect.void
77
+ if (!catchMethods.has(prop)) return Effect.void
78
+
79
+ // The callback is the last argument
80
+ const args = call.arguments
81
+ if (args.length === 0) return Effect.void
82
+ const callback = args[args.length - 1]
83
+ if (callback === undefined) return Effect.void
84
+ if (
85
+ callback.type !== "ArrowFunctionExpression" &&
86
+ callback.type !== "FunctionExpression"
87
+ ) {
88
+ return Effect.void
89
+ }
90
+
91
+ if (bodyReturnsNativeError(callback as ESTree.Node)) {
92
+ return ctx.report(
93
+ Diagnostic.make({
94
+ node: callback,
95
+ message:
96
+ "Avoid native Error in Effect catch handler. Use Schema.TaggedErrorClass or Data.TaggedError.",
97
+ }),
98
+ )
99
+ }
100
+ return Effect.void
101
+ },
102
+ }
103
+ },
104
+ })
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Ban `new Error()` inside `Effect.fail()`.
3
+ *
4
+ * Use Schema.TaggedErrorClass or Data.TaggedError instead.
5
+ *
6
+ * Source: language-service/globalErrorInEffectFailure
7
+ */
8
+ import type { ESTree } from "@oxlint/plugins"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Option from "effect/Option"
12
+
13
+ const nativeErrors = new Set([
14
+ "Error",
15
+ "TypeError",
16
+ "RangeError",
17
+ "ReferenceError",
18
+ "SyntaxError",
19
+ "URIError",
20
+ "EvalError",
21
+ ])
22
+
23
+ const isNativeErrorNew = (node: ESTree.Node): boolean => {
24
+ if (node.type !== "NewExpression") return false
25
+ const callee = (node as unknown as Record<string, unknown>)["callee"] as ESTree.Node
26
+ return callee.type === "Identifier" && "name" in callee && nativeErrors.has(callee.name as string)
27
+ }
28
+
29
+ export const noGlobalErrorInFailure = Rule.define({
30
+ name: "no-global-error-in-failure",
31
+ meta: Rule.meta({
32
+ type: "suggestion",
33
+ description:
34
+ "Avoid native Error in Effect.fail(). Use Schema.TaggedErrorClass or Data.TaggedError.",
35
+ }),
36
+ create: function* () {
37
+ const ctx = yield* RuleContext
38
+ return {
39
+ CallExpression: (node) => {
40
+ const call = node as ESTree.CallExpression
41
+ if (
42
+ Option.isNone(AST.matchCallOf(call, "Effect", "fail")) &&
43
+ Option.isNone(AST.matchCallOf(call, "Effect", "failSync")) &&
44
+ Option.isNone(AST.matchCallOf(call, "Effect", "failCause"))
45
+ ) {
46
+ return Effect.void
47
+ }
48
+
49
+ const args = call.arguments
50
+ if (args.length === 0) return Effect.void
51
+
52
+ // Check direct arg: Effect.fail(new Error(...))
53
+ const arg = args[args.length - 1]
54
+ if (arg === undefined) return Effect.void
55
+ if (isNativeErrorNew(arg as ESTree.Node)) {
56
+ return ctx.report(
57
+ Diagnostic.make({
58
+ node: arg,
59
+ message:
60
+ "Avoid native Error in Effect.fail(). Use Schema.TaggedErrorClass or Data.TaggedError.",
61
+ }),
62
+ )
63
+ }
64
+
65
+ // Check arrow body: Effect.failSync(() => new Error(...))
66
+ if (arg.type === "ArrowFunctionExpression") {
67
+ const body = (arg as unknown as Record<string, unknown>)["body"] as ESTree.Node
68
+ if (body.type !== "BlockStatement" && isNativeErrorNew(body)) {
69
+ return ctx.report(
70
+ Diagnostic.make({
71
+ node: arg,
72
+ message:
73
+ "Avoid native Error in Effect.failSync(). Use Schema.TaggedErrorClass or Data.TaggedError.",
74
+ }),
75
+ )
76
+ }
77
+ }
78
+
79
+ return Effect.void
80
+ },
81
+ }
82
+ },
83
+ })
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Ban global/platform APIs inside Effect.gen/fn context.
3
+ *
4
+ * Consolidates: console, fetch(), Date/new Date, Math.random/crypto,
5
+ * setTimeout/setInterval, JSON.parse/stringify, process/Bun/Deno globals
6
+ *
7
+ * Each has a specific Effect alternative hint.
8
+ *
9
+ * Sources: language-service globalConsoleInEffect, globalFetchInEffect,
10
+ * globalDateInEffect, globalRandomInEffect, globalTimersInEffect,
11
+ * preferSchemaOverJson, processEnvInEffect, cryptoRandomUUIDInEffect
12
+ */
13
+ import type { ESTree } from "@oxlint/plugins"
14
+ import { AST, Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
15
+ import * as Effect from "effect/Effect"
16
+ import * as Option from "effect/Option"
17
+ import * as Ref from "effect/Ref"
18
+
19
+ import { makeEffectContextTracker } from "./_effect-context.js"
20
+
21
+ // --- Member expression bans (obj.prop) ---
22
+
23
+ type MemberBan = readonly [obj: string, props: string | ReadonlyArray<string>, message: string]
24
+
25
+ const memberBans: ReadonlyArray<MemberBan> = [
26
+ // console
27
+ ["console", ["log", "warn", "error", "info", "debug", "trace"], "Avoid console.* inside Effect context. Use Effect.log / Effect.logWarning / Effect.logError."],
28
+ // Date
29
+ ["Date", "now", "Avoid Date.now() inside Effect context. Use Clock service from 'effect'."],
30
+ // JSON
31
+ ["JSON", ["parse", "stringify"], "Avoid JSON.parse/stringify inside Effect context. Use Schema.fromJsonString."],
32
+ // Math.random
33
+ ["Math", "random", "Avoid Math.random() inside Effect context. Use Random service from 'effect'."],
34
+ // crypto
35
+ ["crypto", ["randomUUID", "getRandomValues"], "Avoid crypto.randomUUID() inside Effect context. Use Random service from 'effect'."],
36
+ ]
37
+
38
+ // --- Platform globals (Bun.*, process.*, Deno.*) ---
39
+
40
+ const platformObjects = new Set(["Bun", "process", "Deno"])
41
+
42
+ const alternatives: Record<string, Record<string, string>> = {
43
+ Bun: {
44
+ file: "Use FileSystem from 'effect'",
45
+ write: "Use FileSystem from 'effect'",
46
+ read: "Use FileSystem from 'effect'",
47
+ serve: "Use HttpServer from 'effect/unstable/http'",
48
+ env: "Use Config from 'effect'",
49
+ spawn: "Use Command from 'effect'",
50
+ sleep: "Use Effect.sleep",
51
+ stdin: "Use Terminal from 'effect'",
52
+ stdout: "Use Terminal from 'effect'",
53
+ stderr: "Use Terminal from 'effect'",
54
+ cwd: "Use Path from 'effect'",
55
+ which: "Use Command from 'effect'",
56
+ resolveSync: "Use Path from 'effect'",
57
+ pathToFileURL: "Use Path from 'effect'",
58
+ randomUUID: "Use Random service from 'effect'",
59
+ randomUUIDv7: "Use Random service from 'effect'",
60
+ },
61
+ process: {
62
+ env: "Use Config from 'effect'",
63
+ exit: "Use Effect.fail or Effect.die",
64
+ cwd: "Use Path from 'effect'",
65
+ argv: "Use Args/Command from 'effect/unstable/cli'",
66
+ stdin: "Use Terminal from 'effect'",
67
+ stdout: "Use Terminal from 'effect'",
68
+ stderr: "Use Terminal from 'effect'",
69
+ chdir: "Use Path from 'effect'",
70
+ kill: "Use Command from 'effect'",
71
+ on: "Use Effect.async or Stream.async",
72
+ platform: "Use PlatformInfo from 'effect'",
73
+ arch: "Use PlatformInfo from 'effect'",
74
+ execPath: "Use Command from 'effect'",
75
+ execArgv: "Use Args/Command from 'effect/unstable/cli'",
76
+ },
77
+ Deno: {
78
+ readFile: "Use FileSystem from 'effect'",
79
+ writeFile: "Use FileSystem from 'effect'",
80
+ readTextFile: "Use FileSystem from 'effect'",
81
+ writeTextFile: "Use FileSystem from 'effect'",
82
+ open: "Use FileSystem from 'effect'",
83
+ remove: "Use FileSystem from 'effect'",
84
+ mkdir: "Use FileSystem from 'effect'",
85
+ readDir: "Use FileSystem from 'effect'",
86
+ stat: "Use FileSystem from 'effect'",
87
+ cwd: "Use Path from 'effect'",
88
+ env: "Use Config from 'effect'",
89
+ exit: "Use Effect.fail or Effect.die",
90
+ serve: "Use HttpServer from 'effect/unstable/http'",
91
+ run: "Use Command from 'effect'",
92
+ args: "Use Args/Command from 'effect/unstable/cli'",
93
+ },
94
+ }
95
+
96
+ function getHint(obj: string, prop: string): string {
97
+ const hint = alternatives[obj]?.[prop]
98
+ if (hint) return `. ${hint}`
99
+ return ". Wrap in a service for testability"
100
+ }
101
+
102
+ // --- Call expression bans (bare function calls) ---
103
+
104
+ const callBans: Record<string, string> = {
105
+ fetch: "Avoid fetch() inside Effect context. Use HttpClient from 'effect/unstable/http'.",
106
+ setTimeout: "Avoid setTimeout inside Effect context. Use Effect.sleep or Schedule.",
107
+ setInterval: "Avoid setInterval inside Effect context. Use Effect.sleep or Schedule.",
108
+ }
109
+
110
+ export const noGlobals = Rule.define({
111
+ name: "no-globals",
112
+ meta: Rule.meta({
113
+ type: "suggestion",
114
+ description:
115
+ "Avoid global/platform APIs inside Effect.gen/fn. Use Effect platform services instead.",
116
+ }),
117
+ create: function* () {
118
+ const ctx = yield* RuleContext
119
+ const [depth, tracker] = yield* makeEffectContextTracker
120
+
121
+ return Visitor.merge(tracker, {
122
+ // Member expressions: console.log, Date.now, JSON.parse, Math.random, process.env, Bun.*, etc.
123
+ MemberExpression: (node) =>
124
+ Effect.flatMap(Ref.get(depth), (d) => {
125
+ if (d <= 0) return Effect.void
126
+ const memberNode = node as ESTree.MemberExpression
127
+
128
+ // Check specific member bans
129
+ for (const [obj, props, message] of memberBans) {
130
+ if (Option.isSome(AST.matchMember(memberNode, obj, props))) {
131
+ return ctx.report(Diagnostic.make({ node, message }))
132
+ }
133
+ }
134
+
135
+ // Check platform globals (Bun.*, process.*, Deno.*)
136
+ const names = Option.getOrUndefined(AST.memberNames(memberNode))
137
+ if (names !== undefined) {
138
+ const [obj, prop] = names
139
+ if (platformObjects.has(obj)) {
140
+ return ctx.report(
141
+ Diagnostic.make({
142
+ node,
143
+ message: `Avoid ${obj}.${prop} inside Effect context${getHint(obj, prop)}.`,
144
+ }),
145
+ )
146
+ }
147
+ }
148
+
149
+ return Effect.void
150
+ }),
151
+
152
+ // Call expressions: fetch(), setTimeout(), setInterval()
153
+ CallExpression: (node) =>
154
+ Effect.flatMap(Ref.get(depth), (d) => {
155
+ if (d <= 0) return Effect.void
156
+ const name = Option.getOrUndefined(AST.calleeName(node as ESTree.CallExpression))
157
+ const message = name !== undefined ? callBans[name] : undefined
158
+ if (message !== undefined) {
159
+ return ctx.report(Diagnostic.make({ node, message }))
160
+ }
161
+ return Effect.void
162
+ }),
163
+
164
+ // New expressions: new Date()
165
+ NewExpression: (node) =>
166
+ Effect.flatMap(Ref.get(depth), (d) => {
167
+ if (d <= 0) return Effect.void
168
+ const callee = (node as unknown as Record<string, unknown>)["callee"] as ESTree.Node
169
+ if (callee.type === "Identifier" && "name" in callee && callee.name === "Date") {
170
+ return ctx.report(
171
+ Diagnostic.make({ node, message: "Avoid new Date() inside Effect context. Use Clock service from 'effect'." }),
172
+ )
173
+ }
174
+ return Effect.void
175
+ }),
176
+ })
177
+ },
178
+ })
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * Source: biome-effect-linting-rules/no-iife-wrapper
7
7
  */
8
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
9
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
8
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
9
  import * as Effect from "effect/Effect"
11
10
 
12
11
  export const noIifeWrapper = Rule.define({
@@ -6,15 +6,14 @@
6
6
  * Source: biome-effect-linting-rules/no-inline-runtime-provide
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
  import * as Ref from "effect/Ref"
13
12
 
14
13
  import { makeEffectContextTracker } from "./_effect-context.js"
15
14
 
16
- export const noInlineRuntimeProvide = Rule.define({
17
- name: "no-inline-runtime-provide",
15
+ export const noInlineProvide = Rule.define({
16
+ name: "no-inline-provide",
18
17
  meta: Rule.meta({
19
18
  type: "suggestion",
20
19
  description:
@@ -8,8 +8,7 @@
8
8
  * Note: Without type information, we detect common Effect Schema class names
9
9
  * used in `instanceof` checks. This is a best-effort heuristic.
10
10
  */
11
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
12
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
11
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
13
12
  import * as Effect from "effect/Effect"
14
13
 
15
14
  export const noInstanceofSchema = Rule.define({
@@ -32,7 +31,7 @@ export const noInstanceofSchema = Rule.define({
32
31
  Diagnostic.make({
33
32
  node,
34
33
  message:
35
- "Avoid instanceof. Use Schema.is(SchemaType) for Schema types, or _tag checks for tagged types.",
34
+ "Avoid instanceof. Add a static `is` field to your class: `static is = Schema.is(this)`, then use `MyClass.is(value)`. For tagged types, use `_tag` checks.",
36
35
  }),
37
36
  )
38
37
  }
@@ -5,8 +5,7 @@
5
5
  *
6
6
  * Source: biome-effect-linting-rules/no-manual-effect-channels
7
7
  */
8
- import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
9
- import { RuleContext } from "../vendor/effect-oxlint/index.js"
8
+ import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
10
9
  import * as Effect from "effect/Effect"
11
10
 
12
11
  export const noManualEffectChannels = Rule.define({
@@ -6,8 +6,7 @@
6
6
  * Source: biome-effect-linting-rules/no-match-effect-branch
7
7
  */
8
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"
9
+ import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
11
10
  import * as Effect from "effect/Effect"
12
11
 
13
12
  const sequencingMethods = new Set(["flatMap", "map", "andThen", "tap", "zipRight", "pipe"])