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.
- package/.effect-lsp.json +60 -0
- package/.oxlintrc.json +33 -0
- package/package.json +51 -0
- package/scripts/add-rule.ts +87 -0
- package/scripts/codegen.ts +108 -0
- package/src/index.ts +9 -0
- package/src/plugin.ts +16 -0
- package/src/presets/core.ts +36 -0
- package/src/presets/effect-native.ts +41 -0
- package/src/presets/full.ts +84 -0
- package/src/presets/index.ts +3 -0
- package/src/rules/_effect-context.ts +28 -0
- package/src/rules/index.ts +83 -0
- package/src/rules/no-arrow-ladder.ts +71 -0
- package/src/rules/no-async-function.ts +48 -0
- package/src/rules/no-console-in-effect.ts +46 -0
- package/src/rules/no-date-in-effect.ts +61 -0
- package/src/rules/no-dynamic-import.ts +13 -0
- package/src/rules/no-effect-as.ts +14 -0
- package/src/rules/no-effect-async.ts +14 -0
- package/src/rules/no-effect-bind.ts +13 -0
- package/src/rules/no-effect-do.ts +14 -0
- package/src/rules/no-effect-fn-generator.ts +59 -0
- package/src/rules/no-effect-gen-adapter.ts +51 -0
- package/src/rules/no-effect-map-void.ts +81 -0
- package/src/rules/no-effect-never.ts +14 -0
- package/src/rules/no-effect-orElse-ladder.ts +80 -0
- package/src/rules/no-effect-succeed-string.ts +53 -0
- package/src/rules/no-effect-succeed-variable.ts +51 -0
- package/src/rules/no-effect-succeed-void.ts +64 -0
- package/src/rules/no-effect-sync-console.ts +92 -0
- package/src/rules/no-effect-sync-wrapper.ts +68 -0
- package/src/rules/no-effect-type-alias.ts +72 -0
- package/src/rules/no-effect-wrapper-alias.ts +98 -0
- package/src/rules/no-extends-native-error.ts +55 -0
- package/src/rules/no-fetch-in-effect.ts +47 -0
- package/src/rules/no-flatmap-ladder.ts +79 -0
- package/src/rules/no-fromnullable-coalesce.ts +63 -0
- package/src/rules/no-global-console.ts +17 -0
- package/src/rules/no-global-date.ts +20 -0
- package/src/rules/no-global-fetch.ts +14 -0
- package/src/rules/no-global-random.ts +13 -0
- package/src/rules/no-global-timers.ts +17 -0
- package/src/rules/no-if-statement.ts +16 -0
- package/src/rules/no-iife-wrapper.ts +52 -0
- package/src/rules/no-inline-runtime-provide.ts +42 -0
- package/src/rules/no-instanceof-schema.ts +43 -0
- package/src/rules/no-json-in-effect.ts +45 -0
- package/src/rules/no-json-parse.ts +18 -0
- package/src/rules/no-manual-effect-channels.ts +59 -0
- package/src/rules/no-match-effect-branch.ts +83 -0
- package/src/rules/no-match-void-branch.ts +73 -0
- package/src/rules/no-nested-effect-call.ts +59 -0
- package/src/rules/no-nested-effect-gen.ts +44 -0
- package/src/rules/no-nested-pipe.ts +76 -0
- package/src/rules/no-new-error.ts +17 -0
- package/src/rules/no-new-promise.ts +14 -0
- package/src/rules/no-node-builtin-import.ts +70 -0
- package/src/rules/no-option-as.ts +13 -0
- package/src/rules/no-option-boolean-normalization.ts +89 -0
- package/src/rules/no-platform-globals.ts +117 -0
- package/src/rules/no-process-env-in-effect.ts +39 -0
- package/src/rules/no-process-env.ts +13 -0
- package/src/rules/no-random-in-effect.ts +39 -0
- package/src/rules/no-return-in-arrow.ts +77 -0
- package/src/rules/no-return-null.ts +47 -0
- package/src/rules/no-run-in-effect.ts +18 -0
- package/src/rules/no-runtime-run-fork.ts +14 -0
- package/src/rules/no-string-sentinel-const.ts +50 -0
- package/src/rules/no-switch-statement.ts +13 -0
- package/src/rules/no-ternary.ts +32 -0
- package/src/rules/no-throw-in-effect-gen.ts +42 -0
- package/src/rules/no-throw-statement.ts +13 -0
- package/src/rules/no-timers-in-effect.ts +47 -0
- package/src/rules/no-try-catch-in-effect-gen.ts +42 -0
- package/src/rules/no-try-catch.ts +14 -0
- package/src/rules/no-unnecessary-arrow-block.ts +52 -0
- package/src/rules/no-unnecessary-effect-gen.ts +85 -0
- package/src/rules/no-unnecessary-pipe.ts +46 -0
- package/src/vendor/effect-oxlint/AST.ts +516 -0
- package/src/vendor/effect-oxlint/Comment.ts +86 -0
- package/src/vendor/effect-oxlint/Diagnostic.ts +199 -0
- package/src/vendor/effect-oxlint/Plugin.ts +71 -0
- package/src/vendor/effect-oxlint/Rule.ts +590 -0
- package/src/vendor/effect-oxlint/RuleContext.ts +144 -0
- package/src/vendor/effect-oxlint/Scope.ts +177 -0
- package/src/vendor/effect-oxlint/SourceCode.ts +377 -0
- package/src/vendor/effect-oxlint/Testing.ts +1227 -0
- package/src/vendor/effect-oxlint/Token.ts +131 -0
- package/src/vendor/effect-oxlint/Visitor.ts +287 -0
- package/src/vendor/effect-oxlint/index.ts +88 -0
- package/tests/ban-rules.test.ts +252 -0
- package/tests/context-rules.test.ts +281 -0
- package/tests/pattern-rules.test.ts +258 -0
- 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
|
+
})
|