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,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban nested IIFE chains — `((x) => ((y) => ...)(arg2))(arg1)`.
|
|
3
|
+
*
|
|
4
|
+
* Use named const bindings + flat pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-arrow-ladder
|
|
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 isIife = (node: unknown): boolean => {
|
|
13
|
+
if (node == null || typeof node !== "object") return false
|
|
14
|
+
const n = node as unknown as Record<string, unknown>
|
|
15
|
+
if (n["type"] !== "CallExpression") return false
|
|
16
|
+
const callee = n["callee"]
|
|
17
|
+
if (callee == null || typeof callee !== "object") return false
|
|
18
|
+
const c = callee as unknown as Record<string, unknown>
|
|
19
|
+
return c["type"] === "ArrowFunctionExpression" || c["type"] === "FunctionExpression"
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const containsIife = (node: unknown): boolean => {
|
|
23
|
+
if (node == null || typeof node !== "object") return false
|
|
24
|
+
const n = node as unknown as Record<string, unknown>
|
|
25
|
+
if (isIife(n)) return true
|
|
26
|
+
if ("body" in n) {
|
|
27
|
+
if (Array.isArray(n["body"])) {
|
|
28
|
+
for (const child of n["body"]) {
|
|
29
|
+
if (containsIife(child)) return true
|
|
30
|
+
}
|
|
31
|
+
} else if (containsIife(n["body"])) return true
|
|
32
|
+
}
|
|
33
|
+
if ("argument" in n && containsIife(n["argument"])) return true
|
|
34
|
+
if ("arguments" in n && Array.isArray(n["arguments"])) {
|
|
35
|
+
for (const arg of n["arguments"]) {
|
|
36
|
+
if (containsIife(arg)) return true
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if ("expression" in n && containsIife(n["expression"])) return true
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const noArrowLadder = Rule.define({
|
|
44
|
+
name: "no-arrow-ladder",
|
|
45
|
+
meta: Rule.meta({
|
|
46
|
+
type: "suggestion",
|
|
47
|
+
description: "Avoid nested IIFE chains. Use named const bindings + flat pipeline.",
|
|
48
|
+
}),
|
|
49
|
+
create: function* () {
|
|
50
|
+
const ctx = yield* RuleContext
|
|
51
|
+
return {
|
|
52
|
+
CallExpression: (node) => {
|
|
53
|
+
if (!isIife(node)) return Effect.void
|
|
54
|
+
|
|
55
|
+
// Check if the body of the IIFE itself contains another IIFE
|
|
56
|
+
const callee = (node as unknown as Record<string, unknown>)["callee"]
|
|
57
|
+
if (callee == null || typeof callee !== "object") return Effect.void
|
|
58
|
+
const c = callee as unknown as Record<string, unknown>
|
|
59
|
+
if ("body" in c && containsIife(c["body"])) {
|
|
60
|
+
return ctx.report(
|
|
61
|
+
Diagnostic.make({
|
|
62
|
+
node,
|
|
63
|
+
message: "Nested IIFE chain detected. Use named const bindings + flat pipeline.",
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
return Effect.void
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `async` functions.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect.promise, Effect.tryPromise, or Effect.gen instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/asyncFunction
|
|
7
|
+
*
|
|
8
|
+
* Note: This is an effectNative rule — off by default.
|
|
9
|
+
* Only enable in codebases fully committed to Effect.
|
|
10
|
+
*/
|
|
11
|
+
import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
12
|
+
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
13
|
+
import * as Effect from "effect/Effect"
|
|
14
|
+
|
|
15
|
+
export const noAsyncFunction = Rule.define({
|
|
16
|
+
name: "no-async-function",
|
|
17
|
+
meta: Rule.meta({
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
description:
|
|
20
|
+
"Avoid async functions. Use Effect.promise or Effect.tryPromise.",
|
|
21
|
+
}),
|
|
22
|
+
create: function* () {
|
|
23
|
+
const ctx = yield* RuleContext
|
|
24
|
+
const report = (node: Parameters<typeof Diagnostic.make>[0]["node"]) =>
|
|
25
|
+
ctx.report(
|
|
26
|
+
Diagnostic.make({
|
|
27
|
+
node,
|
|
28
|
+
message:
|
|
29
|
+
"Avoid async functions. Use Effect.promise, Effect.tryPromise, or Effect.gen.",
|
|
30
|
+
}),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
FunctionDeclaration: (node) => {
|
|
35
|
+
if ("async" in node && node.async === true) return report(node)
|
|
36
|
+
return Effect.void
|
|
37
|
+
},
|
|
38
|
+
FunctionExpression: (node) => {
|
|
39
|
+
if ("async" in node && node.async === true) return report(node)
|
|
40
|
+
return Effect.void
|
|
41
|
+
},
|
|
42
|
+
ArrowFunctionExpression: (node) => {
|
|
43
|
+
if ("async" in node && node.async === true) return report(node)
|
|
44
|
+
return Effect.void
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `console.*` calls inside Effect.gen/fn context.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect Logger service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalConsoleInEffect
|
|
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
|
+
const consoleMethods = ["log", "warn", "error", "info", "debug", "trace"]
|
|
18
|
+
|
|
19
|
+
export const noConsoleInEffect = Rule.define({
|
|
20
|
+
name: "no-console-in-effect",
|
|
21
|
+
meta: Rule.meta({
|
|
22
|
+
type: "suggestion",
|
|
23
|
+
description:
|
|
24
|
+
"Avoid console.* inside Effect.gen/fn. Use Effect Logger service.",
|
|
25
|
+
}),
|
|
26
|
+
create: function* () {
|
|
27
|
+
const ctx = yield* RuleContext
|
|
28
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
29
|
+
|
|
30
|
+
return Visitor.merge(tracker, {
|
|
31
|
+
MemberExpression: (node) =>
|
|
32
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
33
|
+
if (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "console", consoleMethods))) {
|
|
34
|
+
return ctx.report(
|
|
35
|
+
Diagnostic.make({
|
|
36
|
+
node,
|
|
37
|
+
message:
|
|
38
|
+
"Avoid console.* inside Effect context. Use Effect Logger service.",
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
return Effect.void
|
|
43
|
+
}),
|
|
44
|
+
})
|
|
45
|
+
},
|
|
46
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Date.now()` and `new Date()` inside Effect.gen/fn context.
|
|
3
|
+
*
|
|
4
|
+
* Use Clock service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalDateInEffect
|
|
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 noDateInEffect = Rule.define({
|
|
18
|
+
name: "no-date-in-effect",
|
|
19
|
+
meta: Rule.meta({
|
|
20
|
+
type: "suggestion",
|
|
21
|
+
description:
|
|
22
|
+
"Avoid Date.now() and new Date() inside Effect.gen/fn. Use Clock service.",
|
|
23
|
+
}),
|
|
24
|
+
create: function* () {
|
|
25
|
+
const ctx = yield* RuleContext
|
|
26
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
27
|
+
|
|
28
|
+
return Visitor.merge(
|
|
29
|
+
tracker,
|
|
30
|
+
{
|
|
31
|
+
MemberExpression: (node) =>
|
|
32
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
33
|
+
if (d > 0 && Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "Date", "now"))) {
|
|
34
|
+
return ctx.report(
|
|
35
|
+
Diagnostic.make({ node, message: "Avoid Date.now() inside Effect context. Use Clock service." }),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
return Effect.void
|
|
39
|
+
}),
|
|
40
|
+
NewExpression: (node) =>
|
|
41
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
42
|
+
if (
|
|
43
|
+
d > 0 &&
|
|
44
|
+
"callee" in node &&
|
|
45
|
+
node.callee != null &&
|
|
46
|
+
typeof node.callee === "object" &&
|
|
47
|
+
"type" in node.callee &&
|
|
48
|
+
node.callee.type === "Identifier" &&
|
|
49
|
+
"name" in node.callee &&
|
|
50
|
+
node.callee.name === "Date"
|
|
51
|
+
) {
|
|
52
|
+
return ctx.report(
|
|
53
|
+
Diagnostic.make({ node, message: "Avoid new Date() inside Effect context. Use Clock service." }),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
return Effect.void
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
},
|
|
61
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban dynamic `import()` expressions.
|
|
3
|
+
*
|
|
4
|
+
* Use static module imports.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/prevent-dynamic-imports
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noDynamicImport = Rule.banStatement("ImportExpression", {
|
|
11
|
+
message: "Avoid dynamic import(). Use static module imports.",
|
|
12
|
+
meta: { type: "problem" },
|
|
13
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.as`.
|
|
3
|
+
*
|
|
4
|
+
* Use `Effect.map` for value mapping or `Effect.asVoid` after explicit steps.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-effect-as
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noEffectAs = Rule.banMember("Effect", "as", {
|
|
11
|
+
message:
|
|
12
|
+
"Avoid Effect.as. Use Effect.map for value mapping or Effect.asVoid.",
|
|
13
|
+
meta: { type: "suggestion" },
|
|
14
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.async`.
|
|
3
|
+
*
|
|
4
|
+
* Use `Stream` or structured Effect lifecycles (acquire/use/release).
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-effect-async
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noEffectAsync = Rule.banMember("Effect", "async", {
|
|
11
|
+
message:
|
|
12
|
+
"Avoid Effect.async. Use Stream or structured Effect lifecycles.",
|
|
13
|
+
meta: { type: "suggestion" },
|
|
14
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.bind`.
|
|
3
|
+
*
|
|
4
|
+
* Use flat pipe or direct top-level `Effect.gen` with yields.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-effect-bind
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noEffectBind = Rule.banMember("Effect", "bind", {
|
|
11
|
+
message: "Avoid Effect.bind. Use flat pipe or Effect.gen with yields.",
|
|
12
|
+
meta: { type: "suggestion" },
|
|
13
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.Do` — the builder/do-notation entry point.
|
|
3
|
+
*
|
|
4
|
+
* Use flat pipe-based flow or `Effect.gen` instead.
|
|
5
|
+
*
|
|
6
|
+
* Sources: biome-effect-linting-rules/no-effect-do, language-service/effectDoNotation
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noEffectDo = Rule.banMember("Effect", "Do", {
|
|
11
|
+
message:
|
|
12
|
+
"Avoid Effect.Do builder notation. Use flat pipe-based flow or Effect.gen.",
|
|
13
|
+
meta: { type: "suggestion" },
|
|
14
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.fn(function* () { ... })` called with a generator argument.
|
|
3
|
+
*
|
|
4
|
+
* Effect.fn should wrap a plain function or arrow that builds a pipeline,
|
|
5
|
+
* not a generator (use Effect.gen for generators).
|
|
6
|
+
*
|
|
7
|
+
* Source: biome-effect-linting-rules/no-effect-fn-generator
|
|
8
|
+
*
|
|
9
|
+
* Note: This rule may be too aggressive. In Effect v4, `Effect.fn("name")(function*() {...})`
|
|
10
|
+
* is actually the idiomatic pattern. This rule targets `Effect.fn(function*() {...})` without
|
|
11
|
+
* a name span, which is the discouraged pattern from the biome rules.
|
|
12
|
+
*/
|
|
13
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
14
|
+
import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
15
|
+
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
16
|
+
import * as Effect from "effect/Effect"
|
|
17
|
+
|
|
18
|
+
export const noEffectFnGenerator = Rule.define({
|
|
19
|
+
name: "no-effect-fn-generator",
|
|
20
|
+
meta: Rule.meta({
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
description:
|
|
23
|
+
"Effect.fn should not directly wrap a generator. Use Effect.fn('name')(function*() {...}) with a span name.",
|
|
24
|
+
}),
|
|
25
|
+
create: function* () {
|
|
26
|
+
const ctx = yield* RuleContext
|
|
27
|
+
return {
|
|
28
|
+
CallExpression: (node) => {
|
|
29
|
+
if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "fn")) return Effect.void
|
|
30
|
+
|
|
31
|
+
// Effect.fn(function*() {...}) — single arg that's a generator
|
|
32
|
+
if (
|
|
33
|
+
"arguments" in node &&
|
|
34
|
+
Array.isArray(node.arguments) &&
|
|
35
|
+
node.arguments.length === 1
|
|
36
|
+
) {
|
|
37
|
+
const arg = node.arguments[0]
|
|
38
|
+
if (
|
|
39
|
+
arg != null &&
|
|
40
|
+
typeof arg === "object" &&
|
|
41
|
+
"type" in arg &&
|
|
42
|
+
arg.type === "FunctionExpression" &&
|
|
43
|
+
"generator" in arg &&
|
|
44
|
+
arg.generator === true
|
|
45
|
+
) {
|
|
46
|
+
return ctx.report(
|
|
47
|
+
Diagnostic.make({
|
|
48
|
+
node,
|
|
49
|
+
message:
|
|
50
|
+
"Use Effect.fn('spanName')(function*() {...}) — provide a trace span name.",
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Effect.void
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban deprecated adapter parameter in `Effect.gen(function*(adapter) {...})`.
|
|
3
|
+
*
|
|
4
|
+
* The adapter parameter is deprecated — just use `yield*` directly.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/effectGenUsesAdapter
|
|
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 noEffectGenAdapter = Rule.define({
|
|
14
|
+
name: "no-effect-gen-adapter",
|
|
15
|
+
meta: Rule.meta({
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
description: "Deprecated adapter parameter in Effect.gen. Use `yield*` directly.",
|
|
18
|
+
}),
|
|
19
|
+
create: function* () {
|
|
20
|
+
const ctx = yield* RuleContext
|
|
21
|
+
return {
|
|
22
|
+
CallExpression: (node) => {
|
|
23
|
+
if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "gen")) 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 (
|
|
30
|
+
arg != null &&
|
|
31
|
+
typeof arg === "object" &&
|
|
32
|
+
"type" in arg &&
|
|
33
|
+
arg.type === "FunctionExpression" &&
|
|
34
|
+
"generator" in arg &&
|
|
35
|
+
arg.generator === true &&
|
|
36
|
+
"params" in arg &&
|
|
37
|
+
Array.isArray(arg.params) &&
|
|
38
|
+
arg.params.length > 0
|
|
39
|
+
) {
|
|
40
|
+
return ctx.report(
|
|
41
|
+
Diagnostic.make({
|
|
42
|
+
node,
|
|
43
|
+
message: "Deprecated adapter parameter in Effect.gen. Remove the parameter and use `yield*` directly.",
|
|
44
|
+
}),
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
return Effect.void
|
|
48
|
+
},
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.map(() => undefined)` / `Effect.map(() => void 0)` / `Effect.map(() => {})`.
|
|
3
|
+
*
|
|
4
|
+
* Use `Effect.asVoid` instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/effectMapVoid
|
|
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 isVoidCallback = (arg: unknown): boolean => {
|
|
14
|
+
if (arg == null || typeof arg !== "object" || !("type" in arg)) return false
|
|
15
|
+
|
|
16
|
+
// Arrow function: () => undefined, () => void 0, () => {}
|
|
17
|
+
if (arg.type === "ArrowFunctionExpression" && "body" in arg) {
|
|
18
|
+
const body = arg.body
|
|
19
|
+
if (body == null || typeof body !== "object" || !("type" in body)) return false
|
|
20
|
+
|
|
21
|
+
// () => undefined
|
|
22
|
+
if (body.type === "Identifier" && "name" in body && body.name === "undefined") return true
|
|
23
|
+
|
|
24
|
+
// () => void 0
|
|
25
|
+
if (
|
|
26
|
+
body.type === "UnaryExpression" &&
|
|
27
|
+
"operator" in body &&
|
|
28
|
+
body.operator === "void"
|
|
29
|
+
)
|
|
30
|
+
return true
|
|
31
|
+
|
|
32
|
+
// () => {} (empty block)
|
|
33
|
+
if (
|
|
34
|
+
body.type === "BlockStatement" &&
|
|
35
|
+
"body" in body &&
|
|
36
|
+
Array.isArray(body.body) &&
|
|
37
|
+
body.body.length === 0
|
|
38
|
+
)
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const noEffectMapVoid = Rule.define({
|
|
46
|
+
name: "no-effect-map-void",
|
|
47
|
+
meta: Rule.meta({
|
|
48
|
+
type: "suggestion",
|
|
49
|
+
description:
|
|
50
|
+
"Avoid Effect.map(() => undefined). Use Effect.asVoid instead.",
|
|
51
|
+
}),
|
|
52
|
+
create: function* () {
|
|
53
|
+
const ctx = yield* RuleContext
|
|
54
|
+
return {
|
|
55
|
+
CallExpression: (node) => {
|
|
56
|
+
if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "map")) return Effect.void
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
"arguments" in node &&
|
|
60
|
+
Array.isArray(node.arguments) &&
|
|
61
|
+
node.arguments.length >= 1
|
|
62
|
+
) {
|
|
63
|
+
// Effect.map(effect, fn) or as pipe arg Effect.map(fn)
|
|
64
|
+
const fn =
|
|
65
|
+
node.arguments.length === 2
|
|
66
|
+
? node.arguments[1]
|
|
67
|
+
: node.arguments[0]
|
|
68
|
+
if (isVoidCallback(fn)) {
|
|
69
|
+
return ctx.report(
|
|
70
|
+
Diagnostic.make({
|
|
71
|
+
node,
|
|
72
|
+
message: "Use Effect.asVoid instead of Effect.map(() => undefined).",
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return Effect.void
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.never`.
|
|
3
|
+
*
|
|
4
|
+
* Use `Stream` or explicit acquire/release lifecycles instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-effect-never
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noEffectNever = Rule.banMember("Effect", "never", {
|
|
11
|
+
message:
|
|
12
|
+
"Avoid Effect.never. Use Stream or explicit acquire/release lifecycles.",
|
|
13
|
+
meta: { type: "suggestion" },
|
|
14
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.orElse` wrapping chains with sequencing combinators.
|
|
3
|
+
*
|
|
4
|
+
* Move error handling to a single terminal decision after the pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-effect-orElse-ladder
|
|
7
|
+
*/
|
|
8
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
9
|
+
import { AST, Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
10
|
+
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
|
+
import * as Effect from "effect/Effect"
|
|
12
|
+
|
|
13
|
+
const sequencingCombinators = ["flatMap", "zipRight", "as", "tap", "andThen"]
|
|
14
|
+
|
|
15
|
+
const containsSequencing = (node: unknown): boolean => {
|
|
16
|
+
if (node == null || typeof node !== "object") return false
|
|
17
|
+
const n = node as Record<string, unknown>
|
|
18
|
+
if (n["type"] === "CallExpression" && "callee" in n) {
|
|
19
|
+
const callee = n["callee"] as Record<string, unknown> | null
|
|
20
|
+
if (
|
|
21
|
+
callee != null &&
|
|
22
|
+
callee["type"] === "MemberExpression" &&
|
|
23
|
+
"object" in callee &&
|
|
24
|
+
"property" in callee
|
|
25
|
+
) {
|
|
26
|
+
const obj = callee["object"] as Record<string, unknown> | null
|
|
27
|
+
const prop = callee["property"] as Record<string, unknown> | null
|
|
28
|
+
if (
|
|
29
|
+
obj != null &&
|
|
30
|
+
obj["type"] === "Identifier" &&
|
|
31
|
+
obj["name"] === "Effect" &&
|
|
32
|
+
prop != null &&
|
|
33
|
+
prop["type"] === "Identifier" &&
|
|
34
|
+
typeof prop["name"] === "string" &&
|
|
35
|
+
sequencingCombinators.includes(prop["name"])
|
|
36
|
+
) {
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if ("arguments" in n && Array.isArray(n["arguments"])) {
|
|
41
|
+
for (const arg of n["arguments"]) {
|
|
42
|
+
if (containsSequencing(arg)) return true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if ("body" in n) return containsSequencing(n["body"])
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const noEffectOrElseLadder = Rule.define({
|
|
51
|
+
name: "no-effect-orElse-ladder",
|
|
52
|
+
meta: Rule.meta({
|
|
53
|
+
type: "suggestion",
|
|
54
|
+
description:
|
|
55
|
+
"Avoid Effect.orElse wrapping sequencing chains. Handle errors at a single terminal decision.",
|
|
56
|
+
}),
|
|
57
|
+
create: function* () {
|
|
58
|
+
const ctx = yield* RuleContext
|
|
59
|
+
return {
|
|
60
|
+
CallExpression: (node) => {
|
|
61
|
+
if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "orElse")) return Effect.void
|
|
62
|
+
|
|
63
|
+
if ("arguments" in node && Array.isArray(node.arguments)) {
|
|
64
|
+
for (const arg of node.arguments) {
|
|
65
|
+
if (containsSequencing(arg)) {
|
|
66
|
+
return ctx.report(
|
|
67
|
+
Diagnostic.make({
|
|
68
|
+
node,
|
|
69
|
+
message:
|
|
70
|
+
"Effect.orElse wrapping sequencing chain. Move error handling to a terminal decision after the pipeline.",
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Effect.void
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.succeed("...")` with string literal arguments.
|
|
3
|
+
*
|
|
4
|
+
* Return domain values (Option/Either/tagged unions) instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-string-sentinel-return
|
|
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 noEffectSucceedString = Rule.define({
|
|
14
|
+
name: "no-effect-succeed-string",
|
|
15
|
+
meta: Rule.meta({
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
description:
|
|
18
|
+
"Avoid Effect.succeed with string literals. Return domain values (tagged unions, Option, Either).",
|
|
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 === "Literal" &&
|
|
37
|
+
"value" in arg &&
|
|
38
|
+
typeof arg.value === "string"
|
|
39
|
+
) {
|
|
40
|
+
return ctx.report(
|
|
41
|
+
Diagnostic.make({
|
|
42
|
+
node,
|
|
43
|
+
message:
|
|
44
|
+
"Avoid Effect.succeed with string literals. Use tagged unions, Option, or Either.",
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return Effect.void
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
})
|