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,44 @@
1
+ /**
2
+ * Ban nested `Effect.gen` calls.
3
+ *
4
+ * An `Effect.gen` inside another `Effect.gen` should be flattened.
5
+ *
6
+ * Sources: biome-effect-linting-rules/no-nested-effect-gen, language-service/nestedEffectGenYield
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
+ export const noNestedEffectGen = Rule.define({
15
+ name: "no-nested-effect-gen",
16
+ meta: Rule.meta({
17
+ type: "suggestion",
18
+ description:
19
+ "Avoid nested Effect.gen. Flatten to a single Effect.gen per method.",
20
+ }),
21
+ create: function* () {
22
+ const ctx = yield* RuleContext
23
+ const depth = yield* Ref.make(0)
24
+
25
+ return Visitor.merge(
26
+ Visitor.tracked("CallExpression", (node) => AST.isCallOf(node, "Effect", "gen"), depth),
27
+ {
28
+ CallExpression: (node) =>
29
+ Effect.flatMap(Ref.get(depth), (d) => {
30
+ if (d > 1 && AST.isCallOf(node as ESTree.CallExpression, "Effect", "gen")) {
31
+ return ctx.report(
32
+ Diagnostic.make({
33
+ node,
34
+ message:
35
+ "Nested Effect.gen detected. Flatten to a single Effect.gen.",
36
+ }),
37
+ )
38
+ }
39
+ return Effect.void
40
+ }),
41
+ },
42
+ )
43
+ },
44
+ })
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Ban nested `pipe()` calls — `pipe(pipe(...), ...)` or `.pipe(...).pipe(...)`.
3
+ *
4
+ * Flatten into a single pipeline.
5
+ *
6
+ * Sources: biome-effect-linting-rules/no-pipe-ladder, language-service/unnecessaryPipeChain
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 { pipe } from "effect/Function"
14
+
15
+ const isPipeCall = (node: ESTree.Node): boolean => {
16
+ if (node.type === "CallExpression" && "callee" in node) {
17
+ const callee = (node as unknown as Record<string, unknown>)["callee"]
18
+ if (callee != null && typeof callee === "object" && "type" in callee) {
19
+ // pipe(...)
20
+ if (callee.type === "Identifier" && "name" in callee && callee.name === "pipe") {
21
+ return true
22
+ }
23
+ // x.pipe(...)
24
+ if (
25
+ callee.type === "MemberExpression" &&
26
+ "property" in callee &&
27
+ callee.property != null &&
28
+ typeof callee.property === "object" &&
29
+ "type" in callee.property &&
30
+ callee.property.type === "Identifier" &&
31
+ "name" in callee.property &&
32
+ callee.property.name === "pipe"
33
+ ) {
34
+ return true
35
+ }
36
+ }
37
+ }
38
+ return false
39
+ }
40
+
41
+ export const noNestedPipe = Rule.define({
42
+ name: "no-nested-pipe",
43
+ meta: Rule.meta({
44
+ type: "suggestion",
45
+ description: "Avoid nested pipe() calls. Flatten into a single pipeline.",
46
+ }),
47
+ create: function* () {
48
+ const ctx = yield* RuleContext
49
+ return {
50
+ CallExpression: (node) => {
51
+ if (!isPipeCall(node)) return Effect.void
52
+
53
+ // Check if any argument is itself a pipe call
54
+ if ("arguments" in node && Array.isArray(node.arguments)) {
55
+ for (const arg of node.arguments) {
56
+ if (
57
+ arg != null &&
58
+ typeof arg === "object" &&
59
+ "type" in arg &&
60
+ isPipeCall(arg as unknown as ESTree.Node)
61
+ ) {
62
+ return ctx.report(
63
+ Diagnostic.make({
64
+ node: arg,
65
+ message:
66
+ "Nested pipe() detected. Flatten into a single pipeline.",
67
+ }),
68
+ )
69
+ }
70
+ }
71
+ }
72
+ return Effect.void
73
+ },
74
+ }
75
+ },
76
+ })
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Ban `new Error(...)`.
3
+ *
4
+ * Use Schema.TaggedErrorClass or Data.TaggedError instead.
5
+ *
6
+ * Source: language-service/globalErrorInEffectFailure, extendsNativeError
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noNewError = Rule.banNewExpr(
11
+ ["Error", "TypeError", "RangeError", "ReferenceError", "SyntaxError"],
12
+ {
13
+ message:
14
+ "Avoid native Error constructors. Use Schema.TaggedErrorClass or Data.TaggedError.",
15
+ meta: { type: "suggestion" },
16
+ },
17
+ )
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ban `new Promise(...)`.
3
+ *
4
+ * Use Effect.async, Effect.tryPromise, or Effect.promise instead.
5
+ *
6
+ * Source: language-service/newPromise
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noNewPromise = Rule.banNewExpr("Promise", {
11
+ message:
12
+ "Avoid new Promise(). Use Effect.async, Effect.tryPromise, or Effect.promise.",
13
+ meta: { type: "suggestion" },
14
+ })
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Ban Node.js builtin imports.
3
+ *
4
+ * Use Effect platform services: FileSystem, Path, HttpClient, Command.
5
+ *
6
+ * Source: language-service/nodeBuiltinImport
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 hints: Record<string, string> = {
15
+ fs: "Use FileSystem from 'effect'",
16
+ path: "Use Path from 'effect'",
17
+ http: "Use HttpClient from 'effect/unstable/http'",
18
+ https: "Use HttpClient from 'effect/unstable/http'",
19
+ child_process: "Use Command from 'effect'",
20
+ crypto: "Use a service wrapper",
21
+ os: "Use PlatformInfo from 'effect'",
22
+ net: "Use HttpServer from 'effect/unstable/http'",
23
+ dns: "Use a service wrapper",
24
+ tls: "Use a service wrapper",
25
+ dgram: "Use a service wrapper",
26
+ stream: "Use Stream from 'effect'",
27
+ zlib: "Use a service wrapper",
28
+ readline: "Use Terminal from 'effect'",
29
+ cluster: "Use a service wrapper",
30
+ worker_threads: "Use a service wrapper",
31
+ }
32
+
33
+ const nodeBuiltins = new Set(Object.keys(hints))
34
+
35
+ const resolveModule = (source: string): string | undefined => {
36
+ const name = source.startsWith("node:") ? source.slice(5) : source
37
+ const base = name.split("/")[0]
38
+ return base !== undefined && nodeBuiltins.has(base) ? base : undefined
39
+ }
40
+
41
+ export const noNodeBuiltinImport = Rule.define({
42
+ name: "no-node-builtin-import",
43
+ meta: Rule.meta({
44
+ type: "suggestion",
45
+ description:
46
+ "Avoid Node.js builtin imports. Use Effect platform services.",
47
+ }),
48
+ create: function* () {
49
+ const ctx = yield* RuleContext
50
+ return {
51
+ ImportDeclaration: (node) => {
52
+ const source = Option.getOrUndefined(
53
+ AST.narrow(node, "ImportDeclaration").pipe(Option.map(AST.importSource)),
54
+ )
55
+ if (source === undefined) return Effect.void
56
+
57
+ const mod = resolveModule(source)
58
+ if (mod === undefined) return Effect.void
59
+
60
+ const hint = hints[mod] ?? "Use an Effect service wrapper"
61
+ return ctx.report(
62
+ Diagnostic.make({
63
+ node,
64
+ message: `Avoid importing '${source}'. ${hint}.`,
65
+ }),
66
+ )
67
+ },
68
+ }
69
+ },
70
+ })
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ban `Option.as`.
3
+ *
4
+ * Use `Option.map` or `Option.match` with explicit value return.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-option-as
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noOptionAs = Rule.banMember("Option", "as", {
11
+ message: "Avoid Option.as. Use Option.map or Option.match instead.",
12
+ meta: { type: "suggestion" },
13
+ })
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Ban `Option.match(x, { onSome: (v) => v === true, onNone: () => false })`.
3
+ *
4
+ * Normalize at the schema boundary instead.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-option-boolean-normalization
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 noOptionBooleanNormalization = Rule.define({
14
+ name: "no-option-boolean-normalization",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description:
18
+ "Avoid Option.match for boolean normalization. Normalize at schema boundary.",
19
+ }),
20
+ create: function* () {
21
+ const ctx = yield* RuleContext
22
+ return {
23
+ CallExpression: (node) => {
24
+ if (!AST.isCallOf(node as ESTree.CallExpression, "Option", "match")) return Effect.void
25
+
26
+ if (!("arguments" in node) || !Array.isArray(node.arguments) || node.arguments.length < 2)
27
+ return Effect.void
28
+
29
+ const opts = node.arguments[1]
30
+ if (opts == null || typeof opts !== "object" || !("type" in opts)) return Effect.void
31
+ if (opts.type !== "ObjectExpression" || !("properties" in opts) || !Array.isArray(opts.properties))
32
+ return Effect.void
33
+
34
+ let hasTripleEqualTrue = false
35
+ let hasReturnFalse = false
36
+
37
+ for (const prop of opts.properties) {
38
+ if (prop == null || typeof prop !== "object" || !("type" in prop) || prop.type !== "Property")
39
+ continue
40
+ const key = (prop as unknown as Record<string, unknown>)["key"] as unknown as Record<string, unknown> | null
41
+ const value = (prop as unknown as Record<string, unknown>)["value"] as unknown as Record<string, unknown> | null
42
+
43
+ if (key == null || value == null) continue
44
+
45
+ // onSome: (v) => v === true
46
+ if (
47
+ key["type"] === "Identifier" && key["name"] === "onSome" &&
48
+ value["type"] === "ArrowFunctionExpression" && "body" in value
49
+ ) {
50
+ const body = value["body"] as unknown as Record<string, unknown> | null
51
+ if (
52
+ body != null &&
53
+ body["type"] === "BinaryExpression" &&
54
+ body["operator"] === "===" &&
55
+ "right" in body
56
+ ) {
57
+ const right = body["right"] as unknown as Record<string, unknown> | null
58
+ if (right != null && right["type"] === "Literal" && right["value"] === true) {
59
+ hasTripleEqualTrue = true
60
+ }
61
+ }
62
+ }
63
+
64
+ // onNone: () => false
65
+ if (
66
+ key["type"] === "Identifier" && key["name"] === "onNone" &&
67
+ value["type"] === "ArrowFunctionExpression" && "body" in value
68
+ ) {
69
+ const body = value["body"] as unknown as Record<string, unknown> | null
70
+ if (body != null && body["type"] === "Literal" && body["value"] === false) {
71
+ hasReturnFalse = true
72
+ }
73
+ }
74
+ }
75
+
76
+ if (hasTripleEqualTrue && hasReturnFalse) {
77
+ return ctx.report(
78
+ Diagnostic.make({
79
+ node,
80
+ message:
81
+ "Avoid Option.match for boolean normalization. Normalize at schema boundary instead.",
82
+ }),
83
+ )
84
+ }
85
+ return Effect.void
86
+ },
87
+ }
88
+ },
89
+ })
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Ban platform-specific global access inside Effect.gen/fn context.
3
+ *
4
+ * Catches `Bun.*`, `process.*`, `Deno.*` — any runtime-specific API
5
+ * that should be wrapped in an Effect service for testability and portability.
6
+ *
7
+ * Provides Effect alternative hints where known.
8
+ *
9
+ * Sources: language-service/processEnv, processEnvInEffect + generalized
10
+ */
11
+ import type { ESTree } from "@oxlint/plugins"
12
+ import { AST, Diagnostic, Rule, Visitor } from "../vendor/effect-oxlint/index.js"
13
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
14
+ import * as Effect from "effect/Effect"
15
+ import * as Option from "effect/Option"
16
+ import * as Ref from "effect/Ref"
17
+
18
+ import { makeEffectContextTracker } from "./_effect-context.js"
19
+
20
+ const platformObjects = new Set(["Bun", "process", "Deno"])
21
+
22
+ /**
23
+ * Map of `Object.property` → Effect alternative hint.
24
+ * Falls back to a generic message for unknown properties.
25
+ */
26
+ const alternatives: Record<string, Record<string, string>> = {
27
+ Bun: {
28
+ file: "Use FileSystem from 'effect'",
29
+ write: "Use FileSystem from 'effect'",
30
+ read: "Use FileSystem from 'effect'",
31
+ serve: "Use HttpServer from 'effect/unstable/http'",
32
+ env: "Use Config from 'effect'",
33
+ spawn: "Use Command from 'effect'",
34
+ sleep: "Use Effect.sleep",
35
+ stdin: "Use Terminal from 'effect'",
36
+ stdout: "Use Terminal from 'effect'",
37
+ stderr: "Use Terminal from 'effect'",
38
+ cwd: "Use Path from 'effect'",
39
+ which: "Use Command from 'effect'",
40
+ resolveSync: "Use Path from 'effect'",
41
+ pathToFileURL: "Use Path from 'effect'",
42
+ },
43
+ process: {
44
+ env: "Use Config from 'effect'",
45
+ exit: "Use Effect.fail or Effect.die",
46
+ cwd: "Use Path from 'effect'",
47
+ argv: "Use Args/Command from 'effect/unstable/cli'",
48
+ stdin: "Use Terminal from 'effect'",
49
+ stdout: "Use Terminal from 'effect'",
50
+ stderr: "Use Terminal from 'effect'",
51
+ chdir: "Use Path from 'effect'",
52
+ kill: "Use Command from 'effect'",
53
+ on: "Use Effect.async or Stream.async",
54
+ platform: "Use PlatformInfo from 'effect'",
55
+ arch: "Use PlatformInfo from 'effect'",
56
+ execPath: "Use Command from 'effect'",
57
+ execArgv: "Use Args/Command from 'effect/unstable/cli'",
58
+ },
59
+ Deno: {
60
+ readFile: "Use FileSystem from 'effect'",
61
+ writeFile: "Use FileSystem from 'effect'",
62
+ readTextFile: "Use FileSystem from 'effect'",
63
+ writeTextFile: "Use FileSystem from 'effect'",
64
+ open: "Use FileSystem from 'effect'",
65
+ remove: "Use FileSystem from 'effect'",
66
+ mkdir: "Use FileSystem from 'effect'",
67
+ readDir: "Use FileSystem from 'effect'",
68
+ stat: "Use FileSystem from 'effect'",
69
+ cwd: "Use Path from 'effect'",
70
+ env: "Use Config from 'effect'",
71
+ exit: "Use Effect.fail or Effect.die",
72
+ serve: "Use HttpServer from 'effect/unstable/http'",
73
+ run: "Use Command from 'effect'",
74
+ args: "Use Args/Command from 'effect/unstable/cli'",
75
+ },
76
+ }
77
+
78
+ function getHint(obj: string, prop: string): string {
79
+ const hint = alternatives[obj]?.[prop]
80
+ if (hint) return `. ${hint}`
81
+ return ". Wrap in a service for testability"
82
+ }
83
+
84
+ export const noPlatformGlobals = Rule.define({
85
+ name: "no-platform-globals",
86
+ meta: Rule.meta({
87
+ type: "suggestion",
88
+ description:
89
+ "Avoid platform globals (Bun.*, process.*, Deno.*) inside Effect context. Use Effect platform services.",
90
+ }),
91
+ create: function* () {
92
+ const ctx = yield* RuleContext
93
+ const [depth, tracker] = yield* makeEffectContextTracker
94
+
95
+ return Visitor.merge(tracker, {
96
+ MemberExpression: (node) =>
97
+ Effect.flatMap(Ref.get(depth), (d) => {
98
+ if (d <= 0) return Effect.void
99
+
100
+ const names = Option.getOrUndefined(
101
+ AST.memberNames(node as ESTree.MemberExpression),
102
+ )
103
+ if (names === undefined) return Effect.void
104
+
105
+ const [obj, prop] = names
106
+ if (!platformObjects.has(obj)) return Effect.void
107
+
108
+ return ctx.report(
109
+ Diagnostic.make({
110
+ node,
111
+ message: `Avoid ${obj}.${prop} inside Effect context${getHint(obj, prop)}.`,
112
+ }),
113
+ )
114
+ }),
115
+ })
116
+ },
117
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Ban `process.env` inside Effect.gen/fn context.
3
+ *
4
+ * Use Effect Config service instead.
5
+ *
6
+ * Source: language-service/processEnvInEffect
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 noProcessEnvInEffect = Rule.define({
18
+ name: "no-process-env-in-effect",
19
+ meta: Rule.meta({
20
+ type: "suggestion",
21
+ description: "Avoid process.env inside Effect.gen/fn. Use Config service.",
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 (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "process", "env"))) {
31
+ return ctx.report(
32
+ Diagnostic.make({ node, message: "Avoid process.env inside Effect context. Use Config service." }),
33
+ )
34
+ }
35
+ return Effect.void
36
+ }),
37
+ })
38
+ },
39
+ })
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ban `process.env` access.
3
+ *
4
+ * Use Effect Config service instead.
5
+ *
6
+ * Source: language-service/processEnv
7
+ */
8
+ import { Rule } from "../vendor/effect-oxlint/index.js"
9
+
10
+ export const noProcessEnv = Rule.banMember("process", "env", {
11
+ message: "Avoid process.env. Use Effect Config service.",
12
+ meta: { type: "suggestion" },
13
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Ban `Math.random()` inside Effect.gen/fn context.
3
+ *
4
+ * Use Random service instead.
5
+ *
6
+ * Source: language-service/globalRandomInEffect
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 noRandomInEffect = Rule.define({
18
+ name: "no-random-in-effect",
19
+ meta: Rule.meta({
20
+ type: "suggestion",
21
+ description: "Avoid Math.random() inside Effect.gen/fn. Use Random service.",
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 (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "Math", "random"))) {
31
+ return ctx.report(
32
+ Diagnostic.make({ node, message: "Avoid Math.random() inside Effect context. Use Random service." }),
33
+ )
34
+ }
35
+ return Effect.void
36
+ }),
37
+ })
38
+ },
39
+ })
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Ban explicit `return` in arrow function callbacks (info severity).
3
+ *
4
+ * Use expression-only callbacks + pipeline logic.
5
+ * Excludes Schema.filter callbacks where return is required.
6
+ *
7
+ * Source: biome-effect-linting-rules/no-return-in-arrow
8
+ */
9
+ import { 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 noReturnInArrow = Rule.define({
14
+ name: "no-return-in-arrow",
15
+ meta: Rule.meta({
16
+ type: "suggestion",
17
+ description: "Prefer expression-only arrow callbacks. Avoid explicit return in arrow functions.",
18
+ }),
19
+ create: function* () {
20
+ const ctx = yield* RuleContext
21
+ return {
22
+ ArrowFunctionExpression: (node) => {
23
+ if (
24
+ "body" in node &&
25
+ node.body != null &&
26
+ typeof node.body === "object" &&
27
+ "type" in node.body &&
28
+ node.body.type === "BlockStatement" &&
29
+ "body" in node.body &&
30
+ Array.isArray(node.body.body)
31
+ ) {
32
+ for (const stmt of node.body.body) {
33
+ if (
34
+ stmt != null &&
35
+ typeof stmt === "object" &&
36
+ "type" in stmt &&
37
+ stmt.type === "ReturnStatement"
38
+ ) {
39
+ // Check if parent is a Schema.filter call — skip those
40
+ if (
41
+ "parent" in node &&
42
+ node.parent != null &&
43
+ typeof node.parent === "object" &&
44
+ "type" in node.parent &&
45
+ node.parent.type === "CallExpression" &&
46
+ "callee" in node.parent
47
+ ) {
48
+ const callee = node.parent.callee
49
+ if (
50
+ callee != null &&
51
+ typeof callee === "object" &&
52
+ "type" in callee &&
53
+ callee.type === "MemberExpression" &&
54
+ "property" in callee &&
55
+ callee.property != null &&
56
+ typeof callee.property === "object" &&
57
+ "name" in callee.property &&
58
+ (callee.property.name === "filter" || callee.property.name === "check")
59
+ ) {
60
+ return Effect.void
61
+ }
62
+ }
63
+
64
+ return ctx.report(
65
+ Diagnostic.make({
66
+ node: stmt,
67
+ message: "Prefer expression-only arrow callbacks over explicit return.",
68
+ }),
69
+ )
70
+ }
71
+ }
72
+ }
73
+ return Effect.void
74
+ },
75
+ }
76
+ },
77
+ })
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Ban `return null` statements.
3
+ *
4
+ * Use Option.none for absence or Effect.fail for errors.
5
+ *
6
+ * Source: biome-effect-linting-rules/no-return-null
7
+ */
8
+ import { AST, Diagnostic, Rule, Visitor } from "../vendor/effect-oxlint/index.js"
9
+ import { RuleContext } from "../vendor/effect-oxlint/index.js"
10
+ import * as Effect from "effect/Effect"
11
+ import * as Option from "effect/Option"
12
+ import { pipe } from "effect/Function"
13
+
14
+ export const noReturnNull = Rule.define({
15
+ name: "no-return-null",
16
+ meta: Rule.meta({
17
+ type: "suggestion",
18
+ description:
19
+ "Avoid returning null. Use Option.none for absence or Effect.fail for errors.",
20
+ }),
21
+ create: function* () {
22
+ const ctx = yield* RuleContext
23
+ return {
24
+ ReturnStatement: (node) => {
25
+ if (
26
+ node.type === "ReturnStatement" &&
27
+ "argument" in node &&
28
+ node.argument !== null &&
29
+ node.argument !== undefined &&
30
+ "type" in node.argument &&
31
+ node.argument.type === "Literal" &&
32
+ "value" in node.argument &&
33
+ node.argument.value === null
34
+ ) {
35
+ return ctx.report(
36
+ Diagnostic.make({
37
+ node,
38
+ message:
39
+ "Avoid returning null. Use Option.none for absence.",
40
+ }),
41
+ )
42
+ }
43
+ return Effect.void
44
+ },
45
+ }
46
+ },
47
+ })