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.
Files changed (81) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/package.json +6 -7
  3. package/src/presets/core.ts +23 -12
  4. package/src/presets/effect-native.ts +18 -25
  5. package/src/presets/full.ts +32 -46
  6. package/src/presets/functional.ts +18 -0
  7. package/src/presets/index.ts +2 -0
  8. package/src/presets/strict.ts +77 -0
  9. package/src/rules/index.ts +17 -28
  10. package/src/rules/no-arrow-ladder.ts +1 -2
  11. package/src/rules/no-async-function.ts +1 -2
  12. package/src/rules/no-catch-all-to-map-error.ts +89 -0
  13. package/src/rules/no-effect-fn-generator.ts +1 -2
  14. package/src/rules/no-effect-gen-adapter.ts +1 -2
  15. package/src/rules/no-effect-map-flatten.ts +82 -0
  16. package/src/rules/no-effect-map-void.ts +1 -2
  17. package/src/rules/no-effect-orElse-ladder.ts +1 -2
  18. package/src/rules/no-effect-succeed-string.ts +1 -2
  19. package/src/rules/no-effect-succeed-variable.ts +1 -2
  20. package/src/rules/no-effect-succeed-void.ts +1 -2
  21. package/src/rules/no-effect-sync-console.ts +1 -2
  22. package/src/rules/no-effect-sync-wrapper.ts +1 -2
  23. package/src/rules/no-effect-type-alias.ts +1 -2
  24. package/src/rules/no-effect-wrapper-alias.ts +1 -4
  25. package/src/rules/no-extends-native-error.ts +1 -2
  26. package/src/rules/no-flatmap-ladder.ts +1 -3
  27. package/src/rules/no-fromnullable-coalesce.ts +1 -2
  28. package/src/rules/no-global-error-in-catch.ts +104 -0
  29. package/src/rules/no-global-error-in-failure.ts +83 -0
  30. package/src/rules/no-globals.ts +178 -0
  31. package/src/rules/no-iife-wrapper.ts +1 -2
  32. package/src/rules/{no-inline-runtime-provide.ts → no-inline-provide.ts} +3 -4
  33. package/src/rules/no-instanceof-schema.ts +2 -3
  34. package/src/rules/no-manual-effect-channels.ts +1 -2
  35. package/src/rules/no-match-effect-branch.ts +1 -2
  36. package/src/rules/no-match-void-branch.ts +1 -2
  37. package/src/rules/no-multiple-effect-provide.ts +75 -0
  38. package/src/rules/no-nested-effect-call.ts +1 -3
  39. package/src/rules/no-nested-effect-gen.ts +1 -2
  40. package/src/rules/no-nested-pipe.ts +1 -4
  41. package/src/rules/no-new-error.ts +46 -10
  42. package/src/rules/no-new-promise.ts +43 -7
  43. package/src/rules/no-node-builtin-import.ts +1 -3
  44. package/src/rules/no-option-boolean-normalization.ts +1 -2
  45. package/src/rules/no-positional-log-error.ts +76 -0
  46. package/src/rules/no-redundant-schema-tag-identifier.ts +62 -0
  47. package/src/rules/no-return-in-arrow.ts +1 -2
  48. package/src/rules/no-return-nullish.ts +80 -0
  49. package/src/rules/no-schema-struct-with-tag.ts +64 -0
  50. package/src/rules/no-schema-union-of-literals.ts +48 -0
  51. package/src/rules/no-string-sentinel-const.ts +1 -2
  52. package/src/rules/no-ternary.ts +1 -3
  53. package/src/rules/no-throw-statement.ts +32 -6
  54. package/src/rules/no-try-catch.ts +32 -7
  55. package/src/rules/no-unnecessary-arrow-block.ts +1 -2
  56. package/src/rules/no-unnecessary-effect-gen.ts +1 -3
  57. package/src/rules/no-unnecessary-pipe-chain.ts +47 -0
  58. package/src/rules/no-unnecessary-pipe.ts +1 -2
  59. package/src/vendor/effect-oxlint/Rule.ts +111 -173
  60. package/tests/ban-rules.test.ts +0 -167
  61. package/tests/context-rules.test.ts +75 -158
  62. package/tests/pattern-rules.test.ts +39 -50
  63. package/src/rules/no-console-in-effect.ts +0 -46
  64. package/src/rules/no-date-in-effect.ts +0 -61
  65. package/src/rules/no-dynamic-import.ts +0 -13
  66. package/src/rules/no-fetch-in-effect.ts +0 -47
  67. package/src/rules/no-global-console.ts +0 -17
  68. package/src/rules/no-global-date.ts +0 -20
  69. package/src/rules/no-global-fetch.ts +0 -14
  70. package/src/rules/no-global-random.ts +0 -13
  71. package/src/rules/no-global-timers.ts +0 -17
  72. package/src/rules/no-json-in-effect.ts +0 -45
  73. package/src/rules/no-json-parse.ts +0 -18
  74. package/src/rules/no-platform-globals.ts +0 -117
  75. package/src/rules/no-process-env-in-effect.ts +0 -39
  76. package/src/rules/no-process-env.ts +0 -13
  77. package/src/rules/no-random-in-effect.ts +0 -39
  78. package/src/rules/no-return-null.ts +0 -47
  79. package/src/rules/no-throw-in-effect-gen.ts +0 -42
  80. package/src/rules/no-timers-in-effect.ts +0 -47
  81. 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 { AST, 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
- 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 { AST, Diagnostic, Rule, Visitor } 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
- 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
- * Use Schema.TaggedErrorClass or Data.TaggedError instead.
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 { Rule } from "../vendor/effect-oxlint/index.js"
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
- export const noNewError = Rule.banNewExpr(
11
- ["Error", "TypeError", "RangeError", "ReferenceError", "SyntaxError"],
12
- {
13
- message:
14
- "Avoid native Error constructors. Use Schema.TaggedErrorClass or Data.TaggedError.",
15
- meta: { type: "suggestion" },
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
- * Use Effect.async, Effect.tryPromise, or Effect.promise instead.
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
- export const noNewPromise = Rule.banNewExpr("Promise", {
11
- message:
12
- "Avoid new Promise(). Use Effect.async, Effect.tryPromise, or Effect.promise.",
13
- meta: { type: "suggestion" },
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 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"
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({
@@ -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",