oxlint-plugin-effect 0.0.0 → 0.1.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 +37 -0
- package/package.json +6 -7
- package/src/presets/core.ts +23 -12
- package/src/presets/effect-native.ts +18 -25
- package/src/presets/full.ts +32 -46
- 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 +17 -28
- 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 +89 -0
- package/src/rules/no-effect-fn-generator.ts +1 -2
- package/src/rules/no-effect-gen-adapter.ts +1 -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-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 +111 -173
- package/tests/ban-rules.test.ts +0 -167
- package/tests/context-rules.test.ts +75 -158
- 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-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: biome-effect-linting-rules/no-match-void-branch
|
|
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 isEffectVoid = (node: unknown): boolean => {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban consecutive `Effect.provide()` calls — merge into one.
|
|
3
|
+
*
|
|
4
|
+
* Detects: `effect.pipe(Effect.provide(A), Effect.provide(B))`
|
|
5
|
+
* Suggests: `effect.pipe(Effect.provide(Layer.mergeAll(A, B)))`
|
|
6
|
+
*
|
|
7
|
+
* This is an AST heuristic — it catches the common patterns without
|
|
8
|
+
* needing the type checker to confirm they're actually Layer values.
|
|
9
|
+
*
|
|
10
|
+
* Source: language-service/multipleEffectProvide
|
|
11
|
+
*/
|
|
12
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
13
|
+
import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
14
|
+
import * as Effect from "effect/Effect"
|
|
15
|
+
import * as Option from "effect/Option"
|
|
16
|
+
|
|
17
|
+
const isEffectProvide = (node: ESTree.Node): boolean => {
|
|
18
|
+
if (node.type !== "CallExpression") return false
|
|
19
|
+
return Option.isSome(AST.matchCallOf(node as ESTree.CallExpression, "Effect", "provide"))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const countProvides = (args: ReadonlyArray<ESTree.Node>): number => {
|
|
23
|
+
let count = 0
|
|
24
|
+
for (const arg of args) {
|
|
25
|
+
if (isEffectProvide(arg as ESTree.Node)) count++
|
|
26
|
+
}
|
|
27
|
+
return count
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const noMultipleEffectProvide = Rule.define({
|
|
31
|
+
name: "no-multiple-effect-provide",
|
|
32
|
+
meta: Rule.meta({
|
|
33
|
+
type: "suggestion",
|
|
34
|
+
description:
|
|
35
|
+
"Avoid consecutive Effect.provide() calls. Merge into Effect.provide(Layer.mergeAll(...)).",
|
|
36
|
+
}),
|
|
37
|
+
create: function* () {
|
|
38
|
+
const ctx = yield* RuleContext
|
|
39
|
+
return {
|
|
40
|
+
CallExpression: (node) => {
|
|
41
|
+
const call = node as ESTree.CallExpression
|
|
42
|
+
const args = call.arguments
|
|
43
|
+
if (args.length < 3) return Effect.void
|
|
44
|
+
|
|
45
|
+
// Check bare pipe() or .pipe() with multiple Effect.provide args
|
|
46
|
+
const calleeName = Option.getOrUndefined(AST.calleeName(call))
|
|
47
|
+
const callee = call.callee
|
|
48
|
+
|
|
49
|
+
const isPipe =
|
|
50
|
+
calleeName === "pipe" ||
|
|
51
|
+
(callee.type === "MemberExpression" &&
|
|
52
|
+
"property" in callee &&
|
|
53
|
+
(callee as unknown as Record<string, unknown>)["computed"] !== true &&
|
|
54
|
+
(() => {
|
|
55
|
+
const prop = (callee as unknown as Record<string, unknown>)["property"]
|
|
56
|
+
return prop != null && typeof prop === "object" && "name" in prop && prop.name === "pipe"
|
|
57
|
+
})())
|
|
58
|
+
|
|
59
|
+
if (!isPipe) return Effect.void
|
|
60
|
+
|
|
61
|
+
if (countProvides(args as ReadonlyArray<ESTree.Node>) >= 2) {
|
|
62
|
+
return ctx.report(
|
|
63
|
+
Diagnostic.make({
|
|
64
|
+
node,
|
|
65
|
+
message:
|
|
66
|
+
"Multiple Effect.provide() in pipe. Merge into Effect.provide(Layer.mergeAll(...)).",
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return Effect.void
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
})
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* Sources: biome-effect-linting-rules/no-nested-effect-call, no-effect-call-in-effect-arg, no-call-tower, no-effect-ladder
|
|
7
7
|
*/
|
|
8
8
|
import type { ESTree } from "@oxlint/plugins"
|
|
9
|
-
import {
|
|
10
|
-
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
10
|
import * as Effect from "effect/Effect"
|
|
12
|
-
import * as Option from "effect/Option"
|
|
13
11
|
|
|
14
12
|
const isEffectCall = (node: unknown): boolean => {
|
|
15
13
|
if (node == null || typeof node !== "object") return false
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* Sources: biome-effect-linting-rules/no-nested-effect-gen, language-service/nestedEffectGenYield
|
|
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
|
|
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
* Sources: biome-effect-linting-rules/no-pipe-ladder, language-service/unnecessaryPipeChain
|
|
7
7
|
*/
|
|
8
8
|
import type { ESTree } from "@oxlint/plugins"
|
|
9
|
-
import {
|
|
10
|
-
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
10
|
import * as Effect from "effect/Effect"
|
|
12
|
-
import * as Option from "effect/Option"
|
|
13
|
-
import { pipe } from "effect/Function"
|
|
14
11
|
|
|
15
12
|
const isPipeCall = (node: ESTree.Node): boolean => {
|
|
16
13
|
if (node.type === "CallExpression" && "callee" in node) {
|
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ban `new Error(...)
|
|
2
|
+
* Ban `new Error(...)` and native error constructors. Context-aware messaging.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Inside Effect.gen/fn: "Use yield* new MyError() (yieldable TaggedErrorClass)"
|
|
5
|
+
* Outside: "Define class MyError extends Schema.TaggedErrorClass..."
|
|
5
6
|
*
|
|
6
7
|
* Source: language-service/globalErrorInEffectFailure, extendsNativeError
|
|
7
8
|
*/
|
|
8
|
-
import {
|
|
9
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
10
|
+
import { Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
|
+
import * as Effect from "effect/Effect"
|
|
12
|
+
import * as Ref from "effect/Ref"
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import { makeEffectContextTracker } from "./_effect-context.js"
|
|
15
|
+
|
|
16
|
+
const nativeErrors = new Set([
|
|
17
|
+
"Error", "TypeError", "RangeError", "ReferenceError", "SyntaxError",
|
|
18
|
+
"URIError", "EvalError",
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
export const noNewError = Rule.define({
|
|
22
|
+
name: "no-new-error",
|
|
23
|
+
meta: Rule.meta({
|
|
24
|
+
type: "suggestion",
|
|
25
|
+
description: "Avoid native Error constructors. Use Schema.TaggedErrorClass.",
|
|
26
|
+
}),
|
|
27
|
+
create: function* () {
|
|
28
|
+
const ctx = yield* RuleContext
|
|
29
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
30
|
+
|
|
31
|
+
return Visitor.merge(tracker, {
|
|
32
|
+
NewExpression: (node) =>
|
|
33
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
34
|
+
const callee = (node as unknown as Record<string, unknown>)["callee"] as ESTree.Node
|
|
35
|
+
if (
|
|
36
|
+
callee.type === "Identifier" &&
|
|
37
|
+
"name" in callee &&
|
|
38
|
+
nativeErrors.has(callee.name as string)
|
|
39
|
+
) {
|
|
40
|
+
return ctx.report(
|
|
41
|
+
Diagnostic.make({
|
|
42
|
+
node,
|
|
43
|
+
message: d > 0
|
|
44
|
+
? "Avoid native Error inside Effect.gen/fn. Define: class MyError extends Schema.TaggedErrorClass<MyError>()(\"MyError\", { ... }) {} — then yield* new MyError()."
|
|
45
|
+
: "Avoid native Error constructors. Define: class MyError extends Schema.TaggedErrorClass<MyError>()(\"MyError\", { message: Schema.String }) {}",
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
return Effect.void
|
|
50
|
+
}),
|
|
51
|
+
})
|
|
16
52
|
},
|
|
17
|
-
)
|
|
53
|
+
})
|
|
@@ -1,14 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Ban `new Promise(
|
|
2
|
+
* Ban `new Promise()`. Context-aware messaging.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Inside Effect.gen/fn: "Use Effect.tryPromise or Effect.async"
|
|
5
|
+
* Outside: "Use Effect.tryPromise or wrap with Effect.fn"
|
|
5
6
|
*
|
|
6
7
|
* Source: language-service/newPromise
|
|
7
8
|
*/
|
|
8
|
-
import { Rule } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import { Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Ref from "effect/Ref"
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
import { makeEffectContextTracker } from "./_effect-context.js"
|
|
14
|
+
|
|
15
|
+
export const noNewPromise = Rule.define({
|
|
16
|
+
name: "no-new-promise",
|
|
17
|
+
meta: Rule.meta({
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
description: "Avoid new Promise(). Use Effect.async or Effect.tryPromise.",
|
|
20
|
+
}),
|
|
21
|
+
create: function* () {
|
|
22
|
+
const ctx = yield* RuleContext
|
|
23
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
24
|
+
|
|
25
|
+
return Visitor.merge(tracker, {
|
|
26
|
+
NewExpression: (node) =>
|
|
27
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
28
|
+
const callee = (node as unknown as Record<string, unknown>)["callee"]
|
|
29
|
+
if (
|
|
30
|
+
callee != null &&
|
|
31
|
+
typeof callee === "object" &&
|
|
32
|
+
"type" in callee &&
|
|
33
|
+
callee.type === "Identifier" &&
|
|
34
|
+
"name" in callee &&
|
|
35
|
+
callee.name === "Promise"
|
|
36
|
+
) {
|
|
37
|
+
return ctx.report(
|
|
38
|
+
Diagnostic.make({
|
|
39
|
+
node,
|
|
40
|
+
message: d > 0
|
|
41
|
+
? "Avoid new Promise() inside Effect.gen/fn. Use Effect.async for callback APIs, Effect.tryPromise for existing promises, or Effect.promise for infallible promises."
|
|
42
|
+
: "Avoid new Promise(). Use Effect.async for callback APIs, Effect.tryPromise for existing promises — wrap this function with Effect.fn.",
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
return Effect.void
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
49
|
+
},
|
|
14
50
|
})
|
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Source: language-service/nodeBuiltinImport
|
|
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 { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
9
|
import * as Effect from "effect/Effect"
|
|
12
10
|
import * as Option from "effect/Option"
|
|
13
11
|
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
* Source: biome-effect-linting-rules/no-option-boolean-normalization
|
|
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 noOptionBooleanNormalization = Rule.define({
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban positional error/cause arguments in Effect.log* calls.
|
|
3
|
+
*
|
|
4
|
+
* `Effect.logWarning("message", error)` loses error structure.
|
|
5
|
+
* Use `Effect.logWarning("message").pipe(Effect.annotateLogs({ error }))` instead,
|
|
6
|
+
* or pass cause via `Effect.logWarning("message", Cause.fail(error))`.
|
|
7
|
+
*
|
|
8
|
+
* Source: agent session analysis — 57 occurrences across 6 projects
|
|
9
|
+
*/
|
|
10
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
11
|
+
import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import * as Option from "effect/Option"
|
|
14
|
+
|
|
15
|
+
const logMethods = new Set([
|
|
16
|
+
"log",
|
|
17
|
+
"logDebug",
|
|
18
|
+
"logInfo",
|
|
19
|
+
"logWarning",
|
|
20
|
+
"logError",
|
|
21
|
+
"logFatal",
|
|
22
|
+
"logTrace",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
export const noPositionalLogError = Rule.define({
|
|
26
|
+
name: "no-positional-log-error",
|
|
27
|
+
meta: Rule.meta({
|
|
28
|
+
type: "suggestion",
|
|
29
|
+
description:
|
|
30
|
+
"Avoid passing errors as positional arguments to Effect.log*. Use Effect.annotateLogs instead.",
|
|
31
|
+
}),
|
|
32
|
+
create: function* () {
|
|
33
|
+
const ctx = yield* RuleContext
|
|
34
|
+
return {
|
|
35
|
+
CallExpression: (node) => {
|
|
36
|
+
const call = node as ESTree.CallExpression
|
|
37
|
+
const names = Option.getOrUndefined(
|
|
38
|
+
AST.memberNames(call.callee as ESTree.MemberExpression),
|
|
39
|
+
)
|
|
40
|
+
if (names === undefined) return Effect.void
|
|
41
|
+
const [obj, prop] = names
|
|
42
|
+
if (obj !== "Effect") return Effect.void
|
|
43
|
+
if (!logMethods.has(prop)) return Effect.void
|
|
44
|
+
|
|
45
|
+
// Effect.log* with more than 1 argument — the second arg is likely
|
|
46
|
+
// an error/cause being passed positionally
|
|
47
|
+
const args = call.arguments
|
|
48
|
+
if (args.length < 2) return Effect.void
|
|
49
|
+
|
|
50
|
+
// The first arg should be a string message. If second arg exists,
|
|
51
|
+
// it's likely an error being passed positionally.
|
|
52
|
+
const secondArg = args[1]
|
|
53
|
+
if (secondArg === undefined) return Effect.void
|
|
54
|
+
|
|
55
|
+
// Allow Cause.fail/Cause.die as second arg (that's the proper API)
|
|
56
|
+
if (secondArg.type === "CallExpression") {
|
|
57
|
+
const secondNames = Option.getOrUndefined(
|
|
58
|
+
AST.memberNames((secondArg as ESTree.CallExpression).callee as ESTree.MemberExpression),
|
|
59
|
+
)
|
|
60
|
+
if (secondNames !== undefined) {
|
|
61
|
+
const [sObj] = secondNames
|
|
62
|
+
if (sObj === "Cause") return Effect.void
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return ctx.report(
|
|
67
|
+
Diagnostic.make({
|
|
68
|
+
node: secondArg,
|
|
69
|
+
message:
|
|
70
|
+
`Avoid passing errors as positional args to Effect.${prop}. Use .pipe(Effect.annotateLogs({ error: String(err) })) or pass a Cause.`,
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban redundant `_tag` in `Schema.TaggedStruct("Foo", { _tag: Schema.Literal("Foo") })`.
|
|
3
|
+
*
|
|
4
|
+
* `TaggedStruct` already provides `_tag` automatically.
|
|
5
|
+
*
|
|
6
|
+
* Source: language-service/redundantSchemaTagIdentifier
|
|
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 isTagKey = (key: ESTree.Node): boolean => {
|
|
14
|
+
if (key.type === "Identifier" && "name" in key && key.name === "_tag") return true
|
|
15
|
+
if (key.type === "Literal" && "value" in key && key.value === "_tag") return true
|
|
16
|
+
return false
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const noRedundantSchemaTagIdentifier = Rule.define({
|
|
20
|
+
name: "no-redundant-schema-tag-identifier",
|
|
21
|
+
meta: Rule.meta({
|
|
22
|
+
type: "suggestion",
|
|
23
|
+
description:
|
|
24
|
+
"Redundant _tag in TaggedStruct. TaggedStruct already provides _tag automatically.",
|
|
25
|
+
}),
|
|
26
|
+
create: function* () {
|
|
27
|
+
const ctx = yield* RuleContext
|
|
28
|
+
return {
|
|
29
|
+
CallExpression: (node) => {
|
|
30
|
+
const call = node as ESTree.CallExpression
|
|
31
|
+
if (Option.isNone(AST.matchCallOf(call, "Schema", "TaggedStruct"))) return Effect.void
|
|
32
|
+
|
|
33
|
+
// TaggedStruct("tag", { fields })
|
|
34
|
+
const args = call.arguments
|
|
35
|
+
if (args.length < 2) return Effect.void
|
|
36
|
+
const fieldsArg = args[1]
|
|
37
|
+
if (fieldsArg === undefined) return Effect.void
|
|
38
|
+
if (fieldsArg.type !== "ObjectExpression") return Effect.void
|
|
39
|
+
|
|
40
|
+
const props = (fieldsArg as unknown as Record<string, unknown>)["properties"]
|
|
41
|
+
if (!Array.isArray(props)) return Effect.void
|
|
42
|
+
|
|
43
|
+
for (const prop of props) {
|
|
44
|
+
const p = prop as ESTree.Node
|
|
45
|
+
if (p.type !== "Property") continue
|
|
46
|
+
const key = (p as unknown as Record<string, unknown>)["key"] as ESTree.Node
|
|
47
|
+
if (isTagKey(key)) {
|
|
48
|
+
return ctx.report(
|
|
49
|
+
Diagnostic.make({
|
|
50
|
+
node: p,
|
|
51
|
+
message:
|
|
52
|
+
"Redundant _tag property. Schema.TaggedStruct already provides _tag automatically.",
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Effect.void
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
})
|
|
@@ -6,8 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Source: biome-effect-linting-rules/no-return-in-arrow
|
|
8
8
|
*/
|
|
9
|
-
import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
10
|
-
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
11
10
|
import * as Effect from "effect/Effect"
|
|
12
11
|
|
|
13
12
|
export const noReturnInArrow = Rule.define({
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `return null`, `return undefined`, `return void 0`. Context-aware messaging.
|
|
3
|
+
*
|
|
4
|
+
* Inside Effect.gen/fn: "Use Option.none() or Effect.void"
|
|
5
|
+
* Outside: "Use Option.none() — convert nullish at system boundaries with Option.fromNullable"
|
|
6
|
+
*
|
|
7
|
+
* Source: biome-effect-linting-rules/no-return-null (extended)
|
|
8
|
+
*/
|
|
9
|
+
import { Diagnostic, Rule, Visitor, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
10
|
+
import * as Effect from "effect/Effect"
|
|
11
|
+
import * as Ref from "effect/Ref"
|
|
12
|
+
|
|
13
|
+
import { makeEffectContextTracker } from "./_effect-context.js"
|
|
14
|
+
|
|
15
|
+
export const noReturnNullish = Rule.define({
|
|
16
|
+
name: "no-return-nullish",
|
|
17
|
+
meta: Rule.meta({
|
|
18
|
+
type: "suggestion",
|
|
19
|
+
description:
|
|
20
|
+
"Avoid returning null/undefined. Use Option.none() for absence or Effect.void for void effects.",
|
|
21
|
+
}),
|
|
22
|
+
create: function* () {
|
|
23
|
+
const ctx = yield* RuleContext
|
|
24
|
+
const [depth, tracker] = yield* makeEffectContextTracker
|
|
25
|
+
|
|
26
|
+
return Visitor.merge(tracker, {
|
|
27
|
+
ReturnStatement: (node) =>
|
|
28
|
+
Effect.flatMap(Ref.get(depth), (d) => {
|
|
29
|
+
if (
|
|
30
|
+
node.type !== "ReturnStatement" ||
|
|
31
|
+
!("argument" in node) ||
|
|
32
|
+
node.argument === null ||
|
|
33
|
+
node.argument === undefined
|
|
34
|
+
) {
|
|
35
|
+
return Effect.void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const arg = node.argument as unknown as Record<string, unknown>
|
|
39
|
+
|
|
40
|
+
// return null
|
|
41
|
+
if (arg["type"] === "Literal" && arg["value"] === null) {
|
|
42
|
+
return ctx.report(
|
|
43
|
+
Diagnostic.make({
|
|
44
|
+
node,
|
|
45
|
+
message: d > 0
|
|
46
|
+
? "Avoid returning null inside Effect.gen/fn. Use Option.none() for absence, or yield* Effect.fail(new MyError()) for errors."
|
|
47
|
+
: "Avoid returning null. Use Option.none() for absence — convert nullish values at system boundaries with Option.fromNullable().",
|
|
48
|
+
}),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// return undefined
|
|
53
|
+
if (arg["type"] === "Identifier" && arg["name"] === "undefined") {
|
|
54
|
+
return ctx.report(
|
|
55
|
+
Diagnostic.make({
|
|
56
|
+
node,
|
|
57
|
+
message: d > 0
|
|
58
|
+
? "Avoid returning undefined inside Effect.gen/fn. Use Effect.void for void effects, or Option.none() for absence."
|
|
59
|
+
: "Avoid returning undefined. Use Option.none() for absence, or restructure as an Effect returning void.",
|
|
60
|
+
}),
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// return void 0
|
|
65
|
+
if (arg["type"] === "UnaryExpression" && arg["operator"] === "void") {
|
|
66
|
+
return ctx.report(
|
|
67
|
+
Diagnostic.make({
|
|
68
|
+
node,
|
|
69
|
+
message: d > 0
|
|
70
|
+
? "Avoid returning void inside Effect.gen/fn. Use Effect.void for void effects."
|
|
71
|
+
: "Avoid returning void expression. Use Option.none() for absence.",
|
|
72
|
+
}),
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Effect.void
|
|
77
|
+
}),
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Schema.Struct({ _tag: Schema.Literal("Foo"), ... })`.
|
|
3
|
+
* Use `Schema.TaggedStruct("Foo", { ... })` instead.
|
|
4
|
+
*
|
|
5
|
+
* Source: language-service/schemaStructWithTag
|
|
6
|
+
*/
|
|
7
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
8
|
+
import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Option from "effect/Option"
|
|
11
|
+
|
|
12
|
+
const isTagKey = (key: ESTree.Node): boolean => {
|
|
13
|
+
if (key.type === "Identifier" && "name" in key && key.name === "_tag") return true
|
|
14
|
+
if (key.type === "Literal" && "value" in key && key.value === "_tag") return true
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const noSchemaStructWithTag = Rule.define({
|
|
19
|
+
name: "no-schema-struct-with-tag",
|
|
20
|
+
meta: Rule.meta({
|
|
21
|
+
type: "suggestion",
|
|
22
|
+
description:
|
|
23
|
+
'Use Schema.TaggedStruct("Tag", { ... }) instead of Schema.Struct({ _tag: Schema.Literal("Tag"), ... }).',
|
|
24
|
+
}),
|
|
25
|
+
create: function* () {
|
|
26
|
+
const ctx = yield* RuleContext
|
|
27
|
+
return {
|
|
28
|
+
CallExpression: (node) => {
|
|
29
|
+
const call = node as ESTree.CallExpression
|
|
30
|
+
if (Option.isNone(AST.matchCallOf(call, "Schema", "Struct"))) return Effect.void
|
|
31
|
+
|
|
32
|
+
const args = call.arguments
|
|
33
|
+
if (args.length !== 1) return Effect.void
|
|
34
|
+
const arg = args[0]
|
|
35
|
+
if (arg === undefined) return Effect.void
|
|
36
|
+
if (arg.type !== "ObjectExpression") return Effect.void
|
|
37
|
+
|
|
38
|
+
const props = (arg as unknown as Record<string, unknown>)["properties"]
|
|
39
|
+
if (!Array.isArray(props)) return Effect.void
|
|
40
|
+
|
|
41
|
+
for (const prop of props) {
|
|
42
|
+
const p = prop as ESTree.Node
|
|
43
|
+
if (p.type !== "Property") continue
|
|
44
|
+
const key = (p as unknown as Record<string, unknown>)["key"] as ESTree.Node
|
|
45
|
+
if (!isTagKey(key)) continue
|
|
46
|
+
|
|
47
|
+
// Check value is Schema.Literal(...)
|
|
48
|
+
const value = (p as unknown as Record<string, unknown>)["value"] as ESTree.Node
|
|
49
|
+
if (value.type === "CallExpression" && Option.isSome(AST.matchCallOf(value as ESTree.CallExpression, "Schema", "Literal"))) {
|
|
50
|
+
return ctx.report(
|
|
51
|
+
Diagnostic.make({
|
|
52
|
+
node,
|
|
53
|
+
message:
|
|
54
|
+
'Use Schema.TaggedStruct("Tag", { ... }) instead of Schema.Struct({ _tag: Schema.Literal(...), ... }).',
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Effect.void
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ban `Schema.Union(Schema.Literal("a"), Schema.Literal("b"))`.
|
|
3
|
+
* Use `Schema.Literal("a", "b")` instead.
|
|
4
|
+
*
|
|
5
|
+
* Source: language-service/schemaUnionOfLiterals
|
|
6
|
+
*/
|
|
7
|
+
import type { ESTree } from "@oxlint/plugins"
|
|
8
|
+
import { AST, Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Option from "effect/Option"
|
|
11
|
+
|
|
12
|
+
const isSchemaLiteral = (node: ESTree.Node): boolean => {
|
|
13
|
+
if (node.type !== "CallExpression") return false
|
|
14
|
+
return Option.isSome(AST.matchCallOf(node as ESTree.CallExpression, "Schema", "Literal"))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const noSchemaUnionOfLiterals = Rule.define({
|
|
18
|
+
name: "no-schema-union-of-literals",
|
|
19
|
+
meta: Rule.meta({
|
|
20
|
+
type: "suggestion",
|
|
21
|
+
description:
|
|
22
|
+
"Use Schema.Literal('a', 'b') instead of Schema.Union(Schema.Literal('a'), Schema.Literal('b')).",
|
|
23
|
+
}),
|
|
24
|
+
create: function* () {
|
|
25
|
+
const ctx = yield* RuleContext
|
|
26
|
+
return {
|
|
27
|
+
CallExpression: (node) => {
|
|
28
|
+
const call = node as ESTree.CallExpression
|
|
29
|
+
if (Option.isNone(AST.matchCallOf(call, "Schema", "Union"))) return Effect.void
|
|
30
|
+
|
|
31
|
+
const args = call.arguments
|
|
32
|
+
if (args.length < 2) return Effect.void
|
|
33
|
+
|
|
34
|
+
// Check if all arguments are Schema.Literal calls
|
|
35
|
+
const allLiterals = args.every((arg) => isSchemaLiteral(arg as ESTree.Node))
|
|
36
|
+
if (!allLiterals) return Effect.void
|
|
37
|
+
|
|
38
|
+
return ctx.report(
|
|
39
|
+
Diagnostic.make({
|
|
40
|
+
node,
|
|
41
|
+
message:
|
|
42
|
+
"Use Schema.Literal('a', 'b') instead of Schema.Union(Schema.Literal('a'), Schema.Literal('b')).",
|
|
43
|
+
}),
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
})
|
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Note: Very aggressive — should probably be off by default or in a strict preset only.
|
|
9
9
|
*/
|
|
10
|
-
import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
11
|
-
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
10
|
+
import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
12
11
|
import * as Effect from "effect/Effect"
|
|
13
12
|
|
|
14
13
|
export const noStringSentinelConst = Rule.define({
|
package/src/rules/no-ternary.ts
CHANGED
|
@@ -7,9 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Note: Very opinionated — recommended for strict functional presets only.
|
|
9
9
|
*/
|
|
10
|
-
import { Diagnostic, Rule } from "../vendor/effect-oxlint/index.js"
|
|
11
|
-
import { RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
12
|
-
import * as Effect from "effect/Effect"
|
|
10
|
+
import { Diagnostic, Rule, RuleContext } from "../vendor/effect-oxlint/index.js"
|
|
13
11
|
|
|
14
12
|
export const noTernary = Rule.define({
|
|
15
13
|
name: "no-ternary",
|