History

Ban Effect escape hatches

Rhys Sullivan committed 6 months ago

Commit 427d4e21 parent 10d34f84 files · +57 −0Browse files

Unified · Split

4 files
  1. .agents/skills/wrdn-effect-typed-errors/SKILL.md +19 −0
  2. .oxlintrc.jsonc +3 −0
  3. scripts/oxlint-plugin-executor.js +2 −0
  4. scripts/oxlint-plugin-executor/rules/no-effect-escape-hatch.js +33 −0
.agents/skills/wrdn-effect-typed-errors/SKILL.md+19−0
@@ -152,6 +152,25 @@ Effect.ignore( · 151 unmodified lines
152);152);
153```153```
154 154
155### Effect die / orDie escape hatches
156
157Bad in domain code:
158
159```ts
160program.pipe(Effect.orDie)
161Effect.die(error)
162```
163
164Good:
165
166```ts
167program.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### try/catch174### try/catch
156 175
157Bad:176Bad:
.oxlintrc.jsonc+3−0
@@ -8,6 +8,7 @@ · 7 unmodified lines
8 "executor/no-double-cast": "error",8 "executor/no-double-cast": "error",
9 "executor/no-cross-package-relative-imports": "error",9 "executor/no-cross-package-relative-imports": "error",
10 "executor/require-reactivity-keys": "error",10 "executor/require-reactivity-keys": "error",
11 "executor/no-effect-escape-hatch": "error",
11 "executor/no-effect-internal-tags": "error",12 "executor/no-effect-internal-tags": "error",
12 "executor/no-error-constructor": "error",13 "executor/no-error-constructor": "error",
13 "executor/no-instanceof-error": "error",14 "executor/no-instanceof-error": "error",
@@ -47,6 +48,7 @@ · 33 unmodified lines
47 "packages/kernel/runtime-*/src/**/*.{ts,tsx,js,mjs}",48 "packages/kernel/runtime-*/src/**/*.{ts,tsx,js,mjs}",
48 ],49 ],
49 "rules": {50 "rules": {
51 "executor/no-effect-escape-hatch": "off",
50 "executor/no-error-constructor": "off",52 "executor/no-error-constructor": "off",
51 "executor/no-instanceof-error": "off",53 "executor/no-instanceof-error": "off",
52 "executor/no-json-parse": "off",54 "executor/no-json-parse": "off",
@@ -59,6 +61,7 @@ · 6 unmodified lines
59 {61 {
60 "files": ["apps/marketing/src/**/*.astro"],62 "files": ["apps/marketing/src/**/*.astro"],
61 "rules": {63 "rules": {
64 "executor/no-effect-escape-hatch": "off",
62 "executor/no-error-constructor": "off",65 "executor/no-error-constructor": "off",
63 "executor/no-instanceof-error": "off",66 "executor/no-instanceof-error": "off",
64 "executor/no-json-parse": "off",67 "executor/no-json-parse": "off",
scripts/oxlint-plugin-executor.js+2−0
@@ -1,6 +1,7 @@
1import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-tests.js";1import noConditionalTests from "./oxlint-plugin-executor/rules/no-conditional-tests.js";
2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";2import noCrossPackageRelativeImports from "./oxlint-plugin-executor/rules/no-cross-package-relative-imports.js";
3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";3import noDoubleCast from "./oxlint-plugin-executor/rules/no-double-cast.js";
4import noEffectEscapeHatch from "./oxlint-plugin-executor/rules/no-effect-escape-hatch.js";
4import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";5import noEffectInternalTags from "./oxlint-plugin-executor/rules/no-effect-internal-tags.js";
5import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js";6import noErrorConstructor from "./oxlint-plugin-executor/rules/no-error-constructor.js";
6import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";7import noInlineObjectTypeAssertion from "./oxlint-plugin-executor/rules/no-inline-object-type-assertion.js";
@@ -34,6 +35,7 @@ export default { · 27 unmodified lines
34 "no-double-cast": noDoubleCast,35 "no-double-cast": noDoubleCast,
35 "no-cross-package-relative-imports": noCrossPackageRelativeImports,36 "no-cross-package-relative-imports": noCrossPackageRelativeImports,
36 "require-reactivity-keys": requireReactivityKeys,37 "require-reactivity-keys": requireReactivityKeys,
38 "no-effect-escape-hatch": noEffectEscapeHatch,
37 "no-effect-internal-tags": noEffectInternalTags,39 "no-effect-internal-tags": noEffectInternalTags,
38 "no-error-constructor": noErrorConstructor,40 "no-error-constructor": noErrorConstructor,
39 "no-ts-nocheck": noTsNocheck,41 "no-ts-nocheck": noTsNocheck,
scripts/oxlint-plugin-executor/rules/no-effect-escape-hatch.js+33−0
@@ -0,0 +1,33 @@
1import { getPropertyName, isTestLike, unwrapExpression } from "../utils.js";
2
3const escapeHatches = new Set(["die", "dieMessage", "orDie", "orDieWith"]);
4
5const 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
8const 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
15export 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};