oxlint-plugin-effect 0.0.0 → 0.2.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/CHANGELOG.md +55 -0
- package/package.json +6 -7
- package/src/presets/core.ts +24 -13
- package/src/presets/effect-native.ts +18 -25
- package/src/presets/full.ts +33 -47
- package/src/presets/functional.ts +18 -0
- package/src/presets/index.ts +2 -0
- package/src/presets/strict.ts +77 -0
- package/src/rules/index.ts +18 -29
- package/src/rules/no-arrow-ladder.ts +1 -2
- package/src/rules/no-async-function.ts +1 -2
- package/src/rules/no-catch-all-to-map-error.ts +90 -0
- package/src/rules/no-effect-fn-generator.ts +1 -2
- package/src/rules/no-effect-gen-adapter.ts +2 -2
- package/src/rules/no-effect-map-flatten.ts +82 -0
- package/src/rules/no-effect-map-void.ts +1 -2
- package/src/rules/no-effect-orElse-ladder.ts +1 -2
- package/src/rules/no-effect-succeed-string.ts +1 -2
- package/src/rules/no-effect-succeed-variable.ts +1 -2
- package/src/rules/no-effect-succeed-void.ts +1 -2
- package/src/rules/no-effect-sync-console.ts +1 -2
- package/src/rules/no-effect-sync-wrapper.ts +1 -2
- package/src/rules/no-effect-type-alias.ts +1 -2
- package/src/rules/no-effect-wrapper-alias.ts +1 -4
- package/src/rules/no-extends-native-error.ts +1 -2
- package/src/rules/no-flatmap-ladder.ts +1 -3
- package/src/rules/no-fromnullable-coalesce.ts +1 -2
- package/src/rules/no-global-error-in-catch.ts +104 -0
- package/src/rules/no-global-error-in-failure.ts +83 -0
- package/src/rules/no-globals.ts +178 -0
- package/src/rules/no-iife-wrapper.ts +1 -2
- package/src/rules/{no-inline-runtime-provide.ts → no-inline-provide.ts} +3 -4
- package/src/rules/no-instanceof-schema.ts +2 -3
- package/src/rules/no-manual-effect-channels.ts +1 -2
- package/src/rules/no-match-effect-branch.ts +1 -2
- package/src/rules/no-match-void-branch.ts +1 -2
- package/src/rules/no-multiple-effect-provide.ts +75 -0
- package/src/rules/no-nested-effect-call.ts +1 -3
- package/src/rules/no-nested-effect-gen.ts +1 -2
- package/src/rules/no-nested-pipe.ts +1 -4
- package/src/rules/no-new-error.ts +46 -10
- package/src/rules/no-new-promise.ts +43 -7
- package/src/rules/no-node-builtin-import.ts +1 -3
- package/src/rules/no-option-boolean-normalization.ts +1 -2
- package/src/rules/no-positional-log-error.ts +76 -0
- package/src/rules/no-redundant-schema-tag-identifier.ts +62 -0
- package/src/rules/no-return-in-arrow.ts +1 -2
- package/src/rules/no-return-nullish.ts +80 -0
- package/src/rules/no-run-in-effect-gen.ts +66 -0
- package/src/rules/no-runtime-run-fork.ts +1 -1
- package/src/rules/no-schema-struct-with-tag.ts +64 -0
- package/src/rules/no-schema-union-of-literals.ts +48 -0
- package/src/rules/no-string-sentinel-const.ts +1 -2
- package/src/rules/no-ternary.ts +1 -3
- package/src/rules/no-throw-statement.ts +32 -6
- package/src/rules/no-try-catch.ts +32 -7
- package/src/rules/no-unnecessary-arrow-block.ts +1 -2
- package/src/rules/no-unnecessary-effect-gen.ts +1 -3
- package/src/rules/no-unnecessary-pipe-chain.ts +47 -0
- package/src/rules/no-unnecessary-pipe.ts +1 -2
- package/src/vendor/effect-oxlint/Rule.ts +141 -183
- package/tests/ban-rules.test.ts +0 -184
- package/tests/context-rules.test.ts +127 -145
- package/tests/pattern-rules.test.ts +39 -50
- package/src/rules/no-console-in-effect.ts +0 -46
- package/src/rules/no-date-in-effect.ts +0 -61
- package/src/rules/no-dynamic-import.ts +0 -13
- package/src/rules/no-fetch-in-effect.ts +0 -47
- package/src/rules/no-global-console.ts +0 -17
- package/src/rules/no-global-date.ts +0 -20
- package/src/rules/no-global-fetch.ts +0 -14
- package/src/rules/no-global-random.ts +0 -13
- package/src/rules/no-global-timers.ts +0 -17
- package/src/rules/no-json-in-effect.ts +0 -45
- package/src/rules/no-json-parse.ts +0 -18
- package/src/rules/no-platform-globals.ts +0 -117
- package/src/rules/no-process-env-in-effect.ts +0 -39
- package/src/rules/no-process-env.ts +0 -13
- package/src/rules/no-random-in-effect.ts +0 -39
- package/src/rules/no-return-null.ts +0 -47
- package/src/rules/no-run-in-effect.ts +0 -18
- package/src/rules/no-throw-in-effect-gen.ts +0 -42
- package/src/rules/no-timers-in-effect.ts +0 -47
- package/src/rules/no-try-catch-in-effect-gen.ts +0 -42
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* Source: language-service/effectGenUsesAdapter
|
|
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 noEffectGenAdapter = Rule.define({
|
|
@@ -15,6 +14,7 @@ export const noEffectGenAdapter = Rule.define({
|
|
|
15
14
|
meta: Rule.meta({
|
|
16
15
|
type: "suggestion",
|
|
17
16
|
description: "Deprecated adapter parameter in Effect.gen. Use `yield*` directly.",
|
|
17
|
+
effectVersion: "v3",
|
|
18
18
|
}),
|
|
19
19
|
create: function* () {
|
|
20
20
|
const ctx = yield* RuleContext
|
|
@@ -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
|
|
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,
|
|
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
|
|
17
|
-
name: "no-inline-
|
|
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.
|
|
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({
|