Unified · Split
| @@ -152,6 +152,25 @@ Effect.ignore( · 151 unmodified lines | ||
| 152 | 152 | ); |
| 153 | 153 | ``` |
| 154 | 154 | |
| 155 | +### Effect die / orDie escape hatches | |
| 156 | + | |
| 157 | +Bad in domain code: | |
| 158 | + | |
| 159 | +```ts | |
| 160 | +program.pipe(Effect.orDie) | |
| 161 | +Effect.die(error) | |
| 162 | +``` | |
| 163 | + | |
| 164 | +Good: | |
| 165 | + | |
| 166 | +```ts | |
| 167 | +program.pipe( | |
| 168 | + Effect.mapError((cause) => new DomainError({ message: "Operation failed", cause })), | |
| 169 | +) | |
| 170 | +``` | |
| 171 | + | |
| 172 | +`Effect.die`, `Effect.dieMessage`, `Effect.orDie`, and `Effect.orDieWith` turn typed failures into defects. Use them only at a true runtime boundary where the host cannot represent typed failures, and keep that usage behind a narrow lint suppression with a `boundary:` reason. Do not use `orDie` to avoid threading an error type through normal Effect code. | |
| 173 | + | |
| 155 | 174 | ### try/catch |
| 156 | 175 | |
| 157 | 176 | Bad: |
| @@ -8,6 +8,7 @@ · 7 unmodified lines | ||
| 8 | 8 | "executor/no-double-cast": "error", |
| 9 | 9 | "executor/no-cross-package-relative-imports": "error", |
| 10 | 10 | "executor/require-reactivity-keys": "error", |
| 11 | + "executor/no-effect-escape-hatch": "error", | |
| 11 | 12 | "executor/no-effect-internal-tags": "error", |
| 12 | 13 | "executor/no-error-constructor": "error", |
| 13 | 14 | "executor/no-instanceof-error": "error", |
| @@ -47,6 +48,7 @@ · 33 unmodified lines | ||
| 47 | 48 | "packages/kernel/runtime-*/src/**/*.{ts,tsx,js,mjs}", |
| 48 | 49 | ], |
| 49 | 50 | "rules": { |
| 51 | + "executor/no-effect-escape-hatch": "off", | |
| 50 | 52 | "executor/no-error-constructor": "off", |
| 51 | 53 | "executor/no-instanceof-error": "off", |
| 52 | 54 | "executor/no-json-parse": "off", |
| @@ -59,6 +61,7 @@ · 6 unmodified lines | ||
| 59 | 61 | { |
| 60 | 62 | "files": ["apps/marketing/src/**/*.astro"], |
| 61 | 63 | "rules": { |
| 64 | + "executor/no-effect-escape-hatch": "off", | |
| 62 | 65 | "executor/no-error-constructor": "off", |
| 63 | 66 | "executor/no-instanceof-error": "off", |
| 64 | 67 | "executor/no-json-parse": "off", |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-tests.js"; |
| 2 | 2 | import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js"; |
| 3 | 3 | import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js"; |
| 4 | +import noEffectEscapeHatch from "./oxlint-plugin-executor/rules/no-effect-escape-hatch.js"; | |
| 4 | 5 | import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js"; |
| 5 | 6 | import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js"; |
| 6 | 7 | import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js"; |
| @@ -34,6 +35,7 @@ export default { · 27 unmodified lines | ||
| 34 | 35 | "no-double-cast": noDoubleCast, |
| 35 | 36 | "no-cross-package-relative-imports": noCrossPackageRelativeImports, |
| 36 | 37 | "require-reactivity-keys": requireReactivityKeys, |
| 38 | + "no-effect-escape-hatch": noEffectEscapeHatch, | |
| 37 | 39 | "no-effect-internal-tags": noEffectInternalTags, |
| 38 | 40 | "no-error-constructor": noErrorConstructor, |
| 39 | 41 | "no-ts-nocheck": noTsNocheck, |
| @@ -0,0 +1,33 @@ | ||
| 1 | +import { getPropertyName, isTestLike, unwrapExpression } from "../utils.js"; | |
| 2 | + | |
| 3 | +const escapeHatches = new Set(["die", "dieMessage", "orDie", "orDieWith"]); | |
| 4 | + | |
| 5 | +const message = | |
| 6 | + "Do not collapse Effect failures with die/orDie escape hatches. Keep typed errors in the Effect error channel; at true runtime edges use a narrow boundary suppression. Skill: wrdn-effect-typed-errors."; | |
| 7 | + | |
| 8 | +const isEffectEscapeHatch = (node) => { | |
| 9 | + const expression = unwrapExpression(node); | |
| 10 | + if (expression?.type !== "MemberExpression") return false; | |
| 11 | + const property = getPropertyName(expression.property); | |
| 12 | + return escapeHatches.has(property); | |
| 13 | +}; | |
| 14 | + | |
| 15 | +export default { | |
| 16 | + meta: { | |
| 17 | + type: "problem", | |
| 18 | + docs: { | |
| 19 | + description: "Disallow Effect die/orDie escape hatches outside test code.", | |
| 20 | + }, | |
| 21 | + }, | |
| 22 | + create(context) { | |
| 23 | + if (isTestLike(context.filename)) return {}; | |
| 24 | + | |
| 25 | + return { | |
| 26 | + MemberExpression(node) { | |
| 27 | + if (isEffectEscapeHatch(node)) { | |
| 28 | + context.report({ node, message }); | |
| 29 | + } | |
| 30 | + }, | |
| 31 | + }; | |
| 32 | + }, | |
| 33 | +}; | |