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,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Option.fromNullable(x ?? null)` / `Option.fromNullable(x ?? undefined)`.
|
|
3
|
+
*
|
|
4
|
+
* The nullish coalescing is redundant — pass the source directly.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-fromnullable-nullish-coalesce
|
|
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 noFromNullableCoalesce = Rule.define({
|
|
14
|
+
name: "no-fromnullable-coalesce",
|
|
15
|
+
meta: Rule.meta({
|
|
16
|
+
type: "suggestion",
|
|
17
|
+
description:
|
|
18
|
+
"Redundant nullish coalescing in Option.fromNullable. Pass the value directly.",
|
|
19
|
+
}),
|
|
20
|
+
create: function* () {
|
|
21
|
+
const ctx = yield* RuleContext
|
|
22
|
+
return {
|
|
23
|
+
CallExpression: (node) => {
|
|
24
|
+
// Match Option.fromNullable(...) or Option.fromNullishOr(...)
|
|
25
|
+
const ce = node as ESTree.CallExpression
|
|
26
|
+
const isFromNullable =
|
|
27
|
+
AST.isCallOf(ce, "Option", "fromNullable") ||
|
|
28
|
+
AST.isCallOf(ce, "Option", "fromNullishOr")
|
|
29
|
+
if (!isFromNullable) return Effect.void
|
|
30
|
+
|
|
31
|
+
if ("arguments" in node && Array.isArray(node.arguments) && node.arguments.length >= 1) {
|
|
32
|
+
const arg = node.arguments[0]
|
|
33
|
+
if (
|
|
34
|
+
arg != null &&
|
|
35
|
+
typeof arg === "object" &&
|
|
36
|
+
"type" in arg &&
|
|
37
|
+
arg.type === "LogicalExpression" &&
|
|
38
|
+
"operator" in arg &&
|
|
39
|
+
arg.operator === "??"
|
|
40
|
+
) {
|
|
41
|
+
// Check RHS is null or undefined
|
|
42
|
+
if ("right" in arg && arg.right != null && typeof arg.right === "object" && "type" in arg.right) {
|
|
43
|
+
const rhs = arg.right
|
|
44
|
+
const isNullish =
|
|
45
|
+
(rhs.type === "Literal" && "value" in rhs && rhs.value === null) ||
|
|
46
|
+
(rhs.type === "Identifier" && "name" in rhs && rhs.name === "undefined")
|
|
47
|
+
if (isNullish) {
|
|
48
|
+
return ctx.report(
|
|
49
|
+
Diagnostic.make({
|
|
50
|
+
node,
|
|
51
|
+
message:
|
|
52
|
+
"Redundant ?? null/undefined in Option.fromNullable. Pass the value directly.",
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Effect.void
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `console.log/warn/error/info/debug/trace`.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect Logger service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalConsole
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noGlobalConsole = Rule.banMember(
|
|
11
|
+
"console",
|
|
12
|
+
["log", "warn", "error", "info", "debug", "trace"],
|
|
13
|
+
{
|
|
14
|
+
message: "Avoid console.*. Use Effect Logger service.",
|
|
15
|
+
meta: { type: "suggestion" },
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Date.now()` and `new Date()`.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect Clock service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalDate
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noGlobalDate = Rule.banMultiple(
|
|
11
|
+
{
|
|
12
|
+
newExprs: "Date",
|
|
13
|
+
members: [["Date", "now"]],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: "no-global-date",
|
|
17
|
+
message: "Avoid Date.now() and new Date(). Use Clock service.",
|
|
18
|
+
meta: { type: "suggestion" },
|
|
19
|
+
},
|
|
20
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban bare `fetch()` calls.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect HttpClient service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalFetch
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noGlobalFetch = Rule.banCallOf("fetch", {
|
|
11
|
+
message:
|
|
12
|
+
"Avoid global fetch. Use HttpClient from 'effect/unstable/http'.",
|
|
13
|
+
meta: { type: "suggestion" },
|
|
14
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Math.random()`.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect Random service instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalRandom
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noGlobalRandom = Rule.banMember("Math", "random", {
|
|
11
|
+
message: "Avoid Math.random(). Use Random service.",
|
|
12
|
+
meta: { type: "suggestion" },
|
|
13
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `setTimeout` and `setInterval`.
|
|
3
|
+
*
|
|
4
|
+
* Use Effect Schedule or Effect.sleep instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/globalTimers
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noGlobalTimers = Rule.banCallOf(
|
|
11
|
+
["setTimeout", "setInterval"],
|
|
12
|
+
{
|
|
13
|
+
message:
|
|
14
|
+
"Avoid setTimeout/setInterval. Use Effect.sleep or Schedule.",
|
|
15
|
+
meta: { type: "suggestion" },
|
|
16
|
+
},
|
|
17
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `if` statements in Effect codebases.
|
|
3
|
+
*
|
|
4
|
+
* Use `Option.match`, `Either.match`, `Match.value`, or data combinators.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-if-statement
|
|
7
|
+
*
|
|
8
|
+
* Note: Very opinionated — recommended for strict functional presets only.
|
|
9
|
+
*/
|
|
10
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
11
|
+
|
|
12
|
+
export const noIfStatement = Rule.banStatement("IfStatement", {
|
|
13
|
+
message:
|
|
14
|
+
"Avoid if statements. Use Option.match, Either.match, Match.value, or Effect.if.",
|
|
15
|
+
meta: { type: "suggestion" },
|
|
16
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban immediately-invoked function expressions (IIFEs).
|
|
3
|
+
*
|
|
4
|
+
* Use named const binding + flat pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-iife-wrapper
|
|
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
|
+
export const noIifeWrapper = Rule.define({
|
|
13
|
+
name: "no-iife-wrapper",
|
|
14
|
+
meta: Rule.meta({
|
|
15
|
+
type: "suggestion",
|
|
16
|
+
description: "Avoid IIFEs. Use named const bindings + flat pipeline.",
|
|
17
|
+
}),
|
|
18
|
+
create: function* () {
|
|
19
|
+
const ctx = yield* RuleContext
|
|
20
|
+
return {
|
|
21
|
+
CallExpression: (node) => {
|
|
22
|
+
if (!("callee" in node)) return Effect.void
|
|
23
|
+
const callee = node.callee
|
|
24
|
+
if (callee == null || typeof callee !== "object" || !("type" in callee)) return Effect.void
|
|
25
|
+
|
|
26
|
+
const isWrappedFn =
|
|
27
|
+
callee.type === "ArrowFunctionExpression" ||
|
|
28
|
+
callee.type === "FunctionExpression"
|
|
29
|
+
|
|
30
|
+
// Also catch (expr)(...) where expr is parenthesized fn
|
|
31
|
+
const isParenWrapped =
|
|
32
|
+
callee.type === "ParenthesizedExpression" &&
|
|
33
|
+
"expression" in callee &&
|
|
34
|
+
callee.expression != null &&
|
|
35
|
+
typeof callee.expression === "object" &&
|
|
36
|
+
"type" in callee.expression &&
|
|
37
|
+
(callee.expression.type === "ArrowFunctionExpression" ||
|
|
38
|
+
callee.expression.type === "FunctionExpression")
|
|
39
|
+
|
|
40
|
+
if (isWrappedFn || isParenWrapped) {
|
|
41
|
+
return ctx.report(
|
|
42
|
+
Diagnostic.make({
|
|
43
|
+
node,
|
|
44
|
+
message: "Avoid IIFEs. Use named const binding + flat pipeline.",
|
|
45
|
+
}),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return Effect.void
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
})
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Effect.provide(layer)` used inline inside Effect.gen/fn.
|
|
3
|
+
*
|
|
4
|
+
* Provide layers at the boundary, not deep in generators.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-inline-runtime-provide
|
|
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
|
+
import { makeEffectContextTracker } from "./_effect-context.js"
|
|
15
|
+
|
|
16
|
+
export const noInlineRuntimeProvide = Rule.define({
|
|
17
|
+
name: "no-inline-runtime-provide",
|
|
18
|
+
meta: Rule.meta({
|
|
19
|
+
type: "suggestion",
|
|
20
|
+
description:
|
|
21
|
+
"Avoid Effect.provide inside Effect.gen/fn. Provide layers at the boundary.",
|
|
22
|
+
}),
|
|
23
|
+
create: function* () {
|
|
24
|
+
const ctx = yield* RuleContext
|
|
25
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
26
|
+
|
|
27
|
+
return Visitor.merge(tracker, {
|
|
28
|
+
CallExpression: (node) =>
|
|
29
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
30
|
+
if (d <= 0) return Effect.void
|
|
31
|
+
if (!AST.isCallOf(node as ESTree.CallExpression, "Effect", "provide")) return Effect.void
|
|
32
|
+
|
|
33
|
+
return ctx.report(
|
|
34
|
+
Diagnostic.make({
|
|
35
|
+
node,
|
|
36
|
+
message: "Avoid Effect.provide inside Effect.gen/fn. Provide layers at the boundary.",
|
|
37
|
+
}),
|
|
38
|
+
)
|
|
39
|
+
}),
|
|
40
|
+
})
|
|
41
|
+
},
|
|
42
|
+
})
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `instanceof` checks against Schema-derived classes.
|
|
3
|
+
*
|
|
4
|
+
* Use `Schema.is(SchemaType)` instead.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/instanceOfSchema
|
|
7
|
+
*
|
|
8
|
+
* Note: Without type information, we detect common Effect Schema class names
|
|
9
|
+
* used in `instanceof` checks. This is a best-effort heuristic.
|
|
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 noInstanceofSchema = Rule.define({
|
|
16
|
+
name: "no-instanceof-schema",
|
|
17
|
+
meta: Rule.meta({
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
description:
|
|
20
|
+
"Avoid instanceof with Schema types. Use Schema.is(SchemaType) instead.",
|
|
21
|
+
}),
|
|
22
|
+
create: function* () {
|
|
23
|
+
const ctx = yield* RuleContext
|
|
24
|
+
return {
|
|
25
|
+
BinaryExpression: (node) => {
|
|
26
|
+
if (
|
|
27
|
+
"operator" in node &&
|
|
28
|
+
node.operator === "instanceof"
|
|
29
|
+
) {
|
|
30
|
+
// Report all instanceof — in Effect codebases, instanceof is generally wrong
|
|
31
|
+
return ctx.report(
|
|
32
|
+
Diagnostic.make({
|
|
33
|
+
node,
|
|
34
|
+
message:
|
|
35
|
+
"Avoid instanceof. Use Schema.is(SchemaType) for Schema types, or _tag checks for tagged types.",
|
|
36
|
+
}),
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
return Effect.void
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `JSON.parse`/`JSON.stringify` inside Effect.gen/fn context.
|
|
3
|
+
*
|
|
4
|
+
* Use Schema.fromJsonString for type-safe JSON.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/preferSchemaOverJson (inside Effect context)
|
|
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 noJsonInEffect = Rule.define({
|
|
18
|
+
name: "no-json-in-effect",
|
|
19
|
+
meta: Rule.meta({
|
|
20
|
+
type: "suggestion",
|
|
21
|
+
description: "Avoid JSON.parse/stringify inside Effect.gen/fn. Use Schema.fromJsonString.",
|
|
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 (
|
|
31
|
+
d > 0 &&
|
|
32
|
+
Option.isSome(AST.matchMember(node as ESTree.MemberExpression, "JSON", ["parse", "stringify"]))
|
|
33
|
+
) {
|
|
34
|
+
return ctx.report(
|
|
35
|
+
Diagnostic.make({
|
|
36
|
+
node,
|
|
37
|
+
message: "Avoid JSON.parse/stringify inside Effect context. Use Schema.fromJsonString.",
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
return Effect.void
|
|
42
|
+
}),
|
|
43
|
+
})
|
|
44
|
+
},
|
|
45
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `JSON.parse` and `JSON.stringify`.
|
|
3
|
+
*
|
|
4
|
+
* Use Schema.fromJsonString for type-safe JSON handling.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/preferSchemaOverJson
|
|
7
|
+
*/
|
|
8
|
+
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
|
|
10
|
+
export const noJsonParse = Rule.banMember(
|
|
11
|
+
"JSON",
|
|
12
|
+
["parse", "stringify"],
|
|
13
|
+
{
|
|
14
|
+
message:
|
|
15
|
+
"Avoid JSON.parse/stringify. Use Schema.fromJsonString for type-safe JSON.",
|
|
16
|
+
meta: { type: "suggestion" },
|
|
17
|
+
},
|
|
18
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban manual `Effect.Effect<A, E, R>` type annotations.
|
|
3
|
+
*
|
|
4
|
+
* Let return types infer from the Effect/Layer you return.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-manual-effect-channels
|
|
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
|
+
export const noManualEffectChannels = Rule.define({
|
|
13
|
+
name: "no-manual-effect-channels",
|
|
14
|
+
meta: Rule.meta({
|
|
15
|
+
type: "suggestion",
|
|
16
|
+
description:
|
|
17
|
+
"Avoid manual Effect.Effect<A, E, R> annotations. Let types infer from the returned Effect.",
|
|
18
|
+
}),
|
|
19
|
+
create: function* () {
|
|
20
|
+
const ctx = yield* RuleContext
|
|
21
|
+
return {
|
|
22
|
+
TSTypeReference: (node) => {
|
|
23
|
+
if (!("typeName" in node) || node.typeName == null) return Effect.void
|
|
24
|
+
const typeName = node.typeName as unknown as Record<string, unknown>
|
|
25
|
+
|
|
26
|
+
// Effect.Effect<...> or Layer.Layer<...>
|
|
27
|
+
if (typeName["type"] === "TSQualifiedName" && "left" in typeName && "right" in typeName) {
|
|
28
|
+
const left = typeName["left"] as unknown as Record<string, unknown> | null
|
|
29
|
+
const right = typeName["right"] as unknown as Record<string, unknown> | null
|
|
30
|
+
if (
|
|
31
|
+
left != null && left["type"] === "Identifier" &&
|
|
32
|
+
(left["name"] === "Effect" || left["name"] === "Layer") &&
|
|
33
|
+
right != null && right["type"] === "Identifier" &&
|
|
34
|
+
(right["name"] === "Effect" || right["name"] === "Layer")
|
|
35
|
+
) {
|
|
36
|
+
// Only flag if explicit type parameters are provided
|
|
37
|
+
if (
|
|
38
|
+
"typeParameters" in node &&
|
|
39
|
+
node.typeParameters != null &&
|
|
40
|
+
typeof node.typeParameters === "object" &&
|
|
41
|
+
"params" in (node.typeParameters as unknown as Record<string, unknown>) &&
|
|
42
|
+
Array.isArray((node.typeParameters as unknown as Record<string, unknown>)["params"]) &&
|
|
43
|
+
((node.typeParameters as unknown as Record<string, unknown>)["params"] as unknown[]).length > 0
|
|
44
|
+
) {
|
|
45
|
+
return ctx.report(
|
|
46
|
+
Diagnostic.make({
|
|
47
|
+
node,
|
|
48
|
+
message:
|
|
49
|
+
"Avoid manual Effect/Layer channel annotations. Let types infer from the returned value.",
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Effect.void
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban Match.when/orElse branches that contain multi-step Effect sequencing.
|
|
3
|
+
*
|
|
4
|
+
* Select the value in Match, then run one Effect pipeline outside.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-match-effect-branch
|
|
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 sequencingMethods = new Set(["flatMap", "map", "andThen", "tap", "zipRight", "pipe"])
|
|
14
|
+
|
|
15
|
+
const containsEffectSequencing = (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 (callee != null && callee["type"] === "MemberExpression") {
|
|
21
|
+
const obj = callee["object"] as Record<string, unknown> | null
|
|
22
|
+
const prop = callee["property"] as Record<string, unknown> | null
|
|
23
|
+
if (
|
|
24
|
+
obj != null && obj["type"] === "Identifier" && obj["name"] === "Effect" &&
|
|
25
|
+
prop != null && prop["type"] === "Identifier" && typeof prop["name"] === "string" &&
|
|
26
|
+
sequencingMethods.has(prop["name"])
|
|
27
|
+
) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if ("arguments" in n && Array.isArray(n["arguments"])) {
|
|
32
|
+
for (const arg of n["arguments"]) {
|
|
33
|
+
if (containsEffectSequencing(arg)) return true
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if ("body" in n) {
|
|
38
|
+
if (Array.isArray(n["body"])) {
|
|
39
|
+
for (const child of n["body"]) {
|
|
40
|
+
if (containsEffectSequencing(child)) return true
|
|
41
|
+
}
|
|
42
|
+
} else if (containsEffectSequencing(n["body"])) return true
|
|
43
|
+
}
|
|
44
|
+
if ("argument" in n && containsEffectSequencing(n["argument"])) return true
|
|
45
|
+
if ("expression" in n && containsEffectSequencing(n["expression"])) return true
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const noMatchEffectBranch = Rule.define({
|
|
50
|
+
name: "no-match-effect-branch",
|
|
51
|
+
meta: Rule.meta({
|
|
52
|
+
type: "suggestion",
|
|
53
|
+
description:
|
|
54
|
+
"Avoid Effect sequencing inside Match branches. Select value in Match, run Effect outside.",
|
|
55
|
+
}),
|
|
56
|
+
create: function* () {
|
|
57
|
+
const ctx = yield* RuleContext
|
|
58
|
+
return {
|
|
59
|
+
CallExpression: (node) => {
|
|
60
|
+
const ce = node as ESTree.CallExpression
|
|
61
|
+
const isMatchWhen = AST.isCallOf(ce, "Match", "when")
|
|
62
|
+
const isMatchOrElse = AST.isCallOf(ce, "Match", "orElse")
|
|
63
|
+
|
|
64
|
+
if (!isMatchWhen && !isMatchOrElse) return Effect.void
|
|
65
|
+
if (!("arguments" in node) || !Array.isArray(node.arguments)) return Effect.void
|
|
66
|
+
|
|
67
|
+
// Check the callback argument for Effect sequencing
|
|
68
|
+
const callbackIdx = isMatchWhen ? 1 : 0
|
|
69
|
+
const callback = node.arguments[callbackIdx]
|
|
70
|
+
if (callback != null && containsEffectSequencing(callback)) {
|
|
71
|
+
return ctx.report(
|
|
72
|
+
Diagnostic.make({
|
|
73
|
+
node,
|
|
74
|
+
message:
|
|
75
|
+
"Avoid Effect sequencing inside Match branches. Select value in Match, then run one Effect pipeline outside.",
|
|
76
|
+
}),
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
return Effect.void
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Match.when(true/false, () => Effect.void)` and `Match.orElse(() => Effect.void)`.
|
|
3
|
+
*
|
|
4
|
+
* Remove the no-op branch or restructure.
|
|
5
|
+
*
|
|
6
|
+
* Source: biome-effect-linting-rules/no-match-void-branch
|
|
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 isEffectVoid = (node: unknown): boolean => {
|
|
14
|
+
if (node == null || typeof node !== "object") return false
|
|
15
|
+
const n = node as Record<string, unknown>
|
|
16
|
+
if (n["type"] !== "MemberExpression") return false
|
|
17
|
+
const obj = n["object"] as Record<string, unknown> | null
|
|
18
|
+
const prop = n["property"] as Record<string, unknown> | null
|
|
19
|
+
return (
|
|
20
|
+
obj != null && obj["type"] === "Identifier" && obj["name"] === "Effect" &&
|
|
21
|
+
prop != null && prop["type"] === "Identifier" && prop["name"] === "void"
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const isVoidCallback = (node: unknown): boolean => {
|
|
26
|
+
if (node == null || typeof node !== "object") return false
|
|
27
|
+
const n = node as Record<string, unknown>
|
|
28
|
+
if (n["type"] !== "ArrowFunctionExpression" && n["type"] !== "FunctionExpression") return false
|
|
29
|
+
if (!("body" in n)) return false
|
|
30
|
+
return isEffectVoid(n["body"])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const noMatchVoidBranch = Rule.define({
|
|
34
|
+
name: "no-match-void-branch",
|
|
35
|
+
meta: Rule.meta({
|
|
36
|
+
type: "suggestion",
|
|
37
|
+
description: "Avoid no-op Match branches returning Effect.void. Remove the branch or restructure.",
|
|
38
|
+
}),
|
|
39
|
+
create: function* () {
|
|
40
|
+
const ctx = yield* RuleContext
|
|
41
|
+
return {
|
|
42
|
+
CallExpression: (node) => {
|
|
43
|
+
const ce = node as ESTree.CallExpression
|
|
44
|
+
const isMatchWhen = AST.isCallOf(ce, "Match", "when")
|
|
45
|
+
const isMatchOrElse = AST.isCallOf(ce, "Match", "orElse")
|
|
46
|
+
|
|
47
|
+
if (!isMatchWhen && !isMatchOrElse) return Effect.void
|
|
48
|
+
|
|
49
|
+
if (!("arguments" in node) || !Array.isArray(node.arguments)) return Effect.void
|
|
50
|
+
|
|
51
|
+
if (isMatchOrElse && node.arguments.length >= 1 && isVoidCallback(node.arguments[0])) {
|
|
52
|
+
return ctx.report(
|
|
53
|
+
Diagnostic.make({
|
|
54
|
+
node,
|
|
55
|
+
message: "Match.orElse returning Effect.void is a no-op. Remove the branch or restructure.",
|
|
56
|
+
}),
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (isMatchWhen && node.arguments.length >= 2 && isVoidCallback(node.arguments[1])) {
|
|
61
|
+
return ctx.report(
|
|
62
|
+
Diagnostic.make({
|
|
63
|
+
node,
|
|
64
|
+
message: "Match.when branch returning Effect.void is a no-op. Remove the branch or restructure.",
|
|
65
|
+
}),
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Effect.void
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban nested Effect.* calls — `Effect.outer(Effect.inner(...))`.
|
|
3
|
+
*
|
|
4
|
+
* Build values first, then one flat pipeline.
|
|
5
|
+
*
|
|
6
|
+
* Sources: biome-effect-linting-rules/no-nested-effect-call, no-effect-call-in-effect-arg, no-call-tower, no-effect-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
|
+
import * as Option from "effect/Option"
|
|
13
|
+
|
|
14
|
+
const isEffectCall = (node: unknown): boolean => {
|
|
15
|
+
if (node == null || typeof node !== "object") return false
|
|
16
|
+
const n = node as Record<string, unknown>
|
|
17
|
+
if (n["type"] !== "CallExpression") return false
|
|
18
|
+
const callee = n["callee"]
|
|
19
|
+
if (callee == null || typeof callee !== "object") return false
|
|
20
|
+
const c = callee as Record<string, unknown>
|
|
21
|
+
if (c["type"] !== "MemberExpression") return false
|
|
22
|
+
const obj = c["object"]
|
|
23
|
+
if (obj == null || typeof obj !== "object") return false
|
|
24
|
+
const o = obj as Record<string, unknown>
|
|
25
|
+
return o["type"] === "Identifier" && o["name"] === "Effect"
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const noNestedEffectCall = Rule.define({
|
|
29
|
+
name: "no-nested-effect-call",
|
|
30
|
+
meta: Rule.meta({
|
|
31
|
+
type: "suggestion",
|
|
32
|
+
description:
|
|
33
|
+
"Avoid nesting Effect.* calls. Build values first, then use a flat pipeline.",
|
|
34
|
+
}),
|
|
35
|
+
create: function* () {
|
|
36
|
+
const ctx = yield* RuleContext
|
|
37
|
+
return {
|
|
38
|
+
CallExpression: (node) => {
|
|
39
|
+
if (!isEffectCall(node)) return Effect.void
|
|
40
|
+
|
|
41
|
+
const ce = node as ESTree.CallExpression
|
|
42
|
+
if (!("arguments" in ce) || !Array.isArray(ce.arguments)) return Effect.void
|
|
43
|
+
|
|
44
|
+
for (const arg of ce.arguments) {
|
|
45
|
+
if (isEffectCall(arg)) {
|
|
46
|
+
return ctx.report(
|
|
47
|
+
Diagnostic.make({
|
|
48
|
+
node,
|
|
49
|
+
message:
|
|
50
|
+
"Nested Effect.* call detected. Build values first, then use a flat pipeline.",
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return Effect.void
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
})
|