@stateward/plugin-errors 1.0.1 → 1.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/dist/plugin.d.ts CHANGED
@@ -6,33 +6,6 @@ export declare function errors(): import("@stateward/core").PluginOf<"errors", {
6
6
  readonly description: "The branch for the other errors must use the error (rethrow it, wrap it, report it); a generic message alone does not count.";
7
7
  };
8
8
  }, "catch">;
9
- readonly "catch-without-intent": import("@stateward/core").LazyRule<{
10
- readonly loggers: {
11
- readonly kind: "strings";
12
- readonly default: readonly ["console", "logger", "log"];
13
- readonly description: "Objects whose every method only logs (globs like *Logger): a catch that only calls them swallows the error.";
14
- };
15
- readonly logMethods: {
16
- readonly kind: "strings";
17
- readonly default: readonly ["error", "warn", "info", "debug"];
18
- readonly description: "Method names that only log, on any object (`Sentry.captureException` is not one).";
19
- };
20
- readonly logFunctions: {
21
- readonly kind: "strings";
22
- readonly default: readonly [];
23
- readonly description: "Plain functions of your own that only log, like logError (globs allowed).";
24
- };
25
- readonly edgeHelpers: {
26
- readonly kind: "strings";
27
- readonly default: readonly [];
28
- readonly description: "Your functions that report an error where it ends (to a service, to the user): a catch that calls one is handled, like reportAndShow (globs allowed). Sentry, Bugsnag and Datadog are known without it.";
29
- };
30
- readonly swallowHelpers: {
31
- readonly kind: "strings";
32
- readonly default: readonly [];
33
- readonly description: "Functions that mark an error as ignored on purpose, like ignoreError (globs allowed): a catch that calls one is not reported.";
34
- };
35
- }, "catch">;
36
9
  readonly "finally-can-throw": import("@stateward/core").LazyRule<{
37
10
  readonly severities: {
38
11
  readonly kind: "severities";
package/dist/plugin.js CHANGED
@@ -7,7 +7,6 @@ import { RULES } from "./rules.meta.js";
7
7
  /** Each rule's module, imported the first time the rule is on. */
8
8
  const load = {
9
9
  catchHidesUnexpected: moduleLoader(() => import("./catch-hides-unexpected.js")),
10
- catchWithoutIntent: moduleLoader(() => import("./catch-without-intent.js")),
11
10
  finallyCanThrow: moduleLoader(() => import("./finally-can-throw.js")),
12
11
  catchMissesError: moduleLoader(() => import("./catch-misses-error.js")),
13
12
  throwsDocMismatch: moduleLoader(() => import("./throws-doc-mismatch.js")),
@@ -20,10 +19,9 @@ const load = {
20
19
  export function errors() {
21
20
  return definePlugin({
22
21
  name: "errors",
23
- description: "Errors are handled where they can happen: nothing swallowed, nothing lost in finally, no Result dropped.",
22
+ description: "Errors are handled where they can happen: none ignored, none replaced in `finally`, no `Result` dropped.",
24
23
  rules: {
25
24
  "catch-hides-unexpected": lazyRule(RULES["catch-hides-unexpected"], load.catchHidesUnexpected, (module) => module.catchHidesUnexpected),
26
- "catch-without-intent": lazyRule(RULES["catch-without-intent"], load.catchWithoutIntent, (module) => module.catchWithoutIntent),
27
25
  "finally-can-throw": lazyRule(RULES["finally-can-throw"], load.finallyCanThrow, (module) => module.finallyCanThrow),
28
26
  "catch-misses-error": lazyRule(RULES["catch-misses-error"], load.catchMissesError, (module) => module.catchMissesError),
29
27
  "throws-doc-mismatch": lazyRule(RULES["throws-doc-mismatch"], load.throwsDocMismatch, (module) => module.throwsDocMismatch),
@@ -74,10 +74,10 @@ export declare function throwsOf(summaries: ReadonlyMap<string, Json>): Readonly
74
74
  /** The pack's view of the app's render tree, built once per summaries map. */
75
75
  export declare function modelOf(summaries: ReadonlyMap<string, Json>): Model;
76
76
  /** The line every boundary hover ends with: what no boundary ever catches. */
77
- export declare const NOT_CAUGHT = "not caught here: errors in event handlers and async code";
77
+ export declare const NOT_CAUGHT = "not caught by a boundary: errors in event handlers and async code";
78
78
  /**
79
- * On a boundary's tag: `catches: NotFoundError (via <UserPage> → useUser) | others stateward
80
- * cannot see (from …)`, then `NOT_CAUGHT`. Undefined when `offset` is on no boundary's tag.
79
+ * On a boundary's tag: `catches: NotFoundError (via <UserPage> → useUser) | others not known
80
+ * (from …)`, then `NOT_CAUGHT`. Undefined when `offset` is on no boundary's tag.
81
81
  */
82
82
  export declare function boundaryLines(model: Model, file: string, offset: number): string[] | undefined;
83
83
  //# sourceMappingURL=boundaries.d.ts.map
@@ -226,7 +226,7 @@ export function modelOf(summaries) {
226
226
  location,
227
227
  what: info.via === null
228
228
  ? inEffect
229
- ? "this effect throws"
229
+ ? "an effect throws"
230
230
  : `\`${name}\` throws`
231
231
  : `\`${info.via.text}\` can throw`,
232
232
  });
@@ -343,10 +343,10 @@ export function modelOf(summaries) {
343
343
  return model;
344
344
  }
345
345
  /** The line every boundary hover ends with: what no boundary ever catches. */
346
- export const NOT_CAUGHT = "not caught here: errors in event handlers and async code";
346
+ export const NOT_CAUGHT = "not caught by a boundary: errors in event handlers and async code";
347
347
  /**
348
- * On a boundary's tag: `catches: NotFoundError (via <UserPage> → useUser) | others stateward
349
- * cannot see (from …)`, then `NOT_CAUGHT`. Undefined when `offset` is on no boundary's tag.
348
+ * On a boundary's tag: `catches: NotFoundError (via <UserPage> → useUser) | others not known
349
+ * (from …)`, then `NOT_CAUGHT`. Undefined when `offset` is on no boundary's tag.
350
350
  */
351
351
  export function boundaryLines(model, file, offset) {
352
352
  const element = model.byFile.get(file)?.find((one) => offset >= one.at[0] && offset < one.at[1]);
@@ -364,7 +364,7 @@ export function boundaryLines(model, file, offset) {
364
364
  ? [...names, unknownText(caught.unknowns, caught.more)]
365
365
  : names;
366
366
  return [
367
- parts.length === 0 ? "catches nothing stateward can see" : `catches: ${parts.join(" | ")}`,
367
+ parts.length === 0 ? "catches nothing known" : `catches: ${parts.join(" | ")}`,
368
368
  NOT_CAUGHT,
369
369
  ];
370
370
  }
@@ -69,10 +69,10 @@ export function checkFallbacks(model, ignored, report) {
69
69
  }));
70
70
  related.push({
71
71
  location: { file: chain.file, start: chain.at[0], end: chain.at[1] },
72
- message: `\`${fallback.name}\` branches on the error here`,
72
+ message: `\`${fallback.name}\` branches on the error`,
73
73
  });
74
74
  report({
75
- message: fitted((cut) => `\`${cut(fallback.name)}\` does not handle ${codeList(missing.map((one) => one.name), cut)}, which this boundary can catch`),
75
+ message: fitted((cut) => `\`${cut(fallback.name)}\` does not handle ${codeList(missing.map((one) => one.name), cut)}, which its boundary can catch`),
76
76
  hint: hint(fallback, chain, catchName(first.id, classes), missing.length),
77
77
  location: { file, start: fallback.at[0], end: fallback.at[1] },
78
78
  related,
@@ -1,7 +1,7 @@
1
1
  import type { EditorContributions } from "@stateward/core";
2
2
  type CodeActions = NonNullable<EditorContributions["codeActions"]>;
3
3
  /** The title of the refactoring that wraps a catch-all for the errors its `try` can throw. */
4
- export declare const HANDLE_KNOWN_TITLE = "Handle the known errors here, rethrow the rest";
4
+ export declare const HANDLE_KNOWN_TITLE = "Handle the known errors, rethrow the rest";
5
5
  /**
6
6
  * On a `catch (e)` with no `instanceof` branches, whose `try` can throw at least one error class
7
7
  * stateward can name and whose body does not already throw: wraps the body as
package/dist/refactors.js CHANGED
@@ -4,7 +4,7 @@ import { catchName } from "./inference/format.js";
4
4
  import { analyse } from "./inference/solve.js";
5
5
  import { factSummaries } from "./inlay.js";
6
6
  /** The title of the refactoring that wraps a catch-all for the errors its `try` can throw. */
7
- export const HANDLE_KNOWN_TITLE = "Handle the known errors here, rethrow the rest";
7
+ export const HANDLE_KNOWN_TITLE = "Handle the known errors, rethrow the rest";
8
8
  /**
9
9
  * On a `catch (e)` with no `instanceof` branches, whose `try` can throw at least one error class
10
10
  * stateward can name and whose body does not already throw: wraps the body as
@@ -7,73 +7,88 @@ import { codeList, fitted, withExample } from "./text.js";
7
7
  export const GROUP = "render";
8
8
  const EXPLAIN = `# errors/render-throw-without-boundary
9
9
 
10
- A component that can throw a known error while rendering has an error boundary above it.
10
+ An error thrown while rendering, with no error boundary above the component, unmounts the whole React tree and leaves a blank page.
11
11
 
12
- ## Why
12
+ \`\`\`tsx example-bad
13
+ import { createRoot } from "react-dom/client";
13
14
 
14
- When a component throws while React renders it, or in one of its effects, React unmounts everything up to the nearest error boundary. With no boundary above, the whole app goes blank. A \`<UserPage>\` whose \`useUser\` throws \`NotFoundError\` for a wrong id takes the menu, the header and every other route down with it, where a boundary could have shown "this user does not exist".
15
+ class NotFoundError extends Error {}
15
16
 
16
- ## When to use it
17
+ function UserPage({ id }: { id: string }) {
18
+ if (id === "") throw new NotFoundError("no such user");
19
+ return <h1>User {id}</h1>;
20
+ }
17
21
 
18
- Use it in React apps whose code throws its own error classes. It is a core rule, a warning in every preset: whether a page should handle an error itself or let a boundary do it is your call. If every throw in render is a bug you want to see crash in development, turn it off.
22
+ function App() {
23
+ return (
24
+ <main>
25
+ <nav>Menu</nav>
26
+ <UserPage id={location.hash.slice(1)} />
27
+ </main>
28
+ );
29
+ }
19
30
 
20
- ## What it catches
31
+ createRoot(document.body).render(<App />);
32
+ \`\`\`
21
33
 
22
- A place that renders a component which can throw a known error while rendering, with no error boundary above it on the way from your entry point (\`createRoot(…).render(…)\` or \`hydrateRoot\`). The component counts what its body and its hooks throw, what its \`useEffect\` and \`useLayoutEffect\` callbacks throw, and what the components it renders outside a boundary throw. A boundary is react-error-boundary's \`<ErrorBoundary>\` (the \`boundaries\` option adds other packages' boundaries), or a class of yours with \`getDerivedStateFromError\` or \`componentDidCatch\`.
34
+ Put an error boundary around the part that can fail, so the rest of the app stays:
23
35
 
24
- It is reported once, at the highest place where the boundary is missing, with the path down to the call that throws.
36
+ \`\`\`tsx example-good
37
+ import { createRoot } from "react-dom/client";
38
+ import { ErrorBoundary } from "react-error-boundary";
25
39
 
26
- A boundary around the whole app (the one that wraps the tree at \`createRoot(…).render(…)\`) does not count: it catches the error, but the whole app is replaced by its fallback, which is rarely what you want for one page. The finding is then at the highest place below it that renders the component that throws. If an app-wide fallback is what you want, set \`rootBoundaryCounts\` to \`true\`: the rule then says nothing for an app with a boundary at its root, and \`stateward plan\` says so (\`silent: your app's root has an error boundary\`).
40
+ class NotFoundError extends Error {}
27
41
 
28
- Two kinds of throw are left out, since they name a programming mistake of the caller rather than a failure to show: the usual check in a context hook (\`if (!ctx) throw new Error("useAuth must be used within AuthProvider")\` on a value from \`useContext\`, or a comparison with a "missing" constant declared at the top of the file), and a check on what the caller passed when its type already rules the value out (\`props.title === undefined\` with \`title: string\`, a missing value from a context hook). A check on an optional prop or a parameter that can be \`undefined\` (\`title?: string\`, \`useLabel(label: string | undefined)\`) counts: any caller can leave it out, so it is a failure to show. A throw on a truthy value (\`if (error) throw error\`) or on a comparison (\`id === ""\`) still counts. Set \`skipContextGuards\` or \`skipInputGuards\` to \`false\` to count those too.
42
+ function UserPage({ id }: { id: string }) {
43
+ if (id === "") throw new NotFoundError("no such user");
44
+ return <h1>User {id}</h1>;
45
+ }
29
46
 
30
- ### Bad
47
+ function App() {
48
+ return (
49
+ <main>
50
+ <nav>Menu</nav>
51
+ <ErrorBoundary fallback={<p>This user does not exist.</p>}>
52
+ <UserPage id={location.hash.slice(1)} />
53
+ </ErrorBoundary>
54
+ </main>
55
+ );
56
+ }
31
57
 
32
- \`\`\`tsx
33
- // useUser throws NotFoundError for an id it does not know.
34
- createRoot(root).render(<App />); // App renders <UserPage /> for /users/:id
58
+ createRoot(document.body).render(<App />);
35
59
  \`\`\`
36
60
 
37
- ### Good
61
+ There is no quick fix: where the boundary goes, and what it shows, is your call.
38
62
 
39
- \`\`\`tsx
40
- createRoot(root).render(
41
- <ErrorBoundary FallbackComponent={ErrorPage}>
42
- <App />
43
- </ErrorBoundary>,
44
- );
45
- \`\`\`
46
-
47
- ### Fix
63
+ ## Why it matters
48
64
 
49
- Put an error boundary above the place the message names, or nearer to the component that throws (around one route). There is no quick fix: where the boundary goes, and what it shows, is your call.
65
+ When a component throws while React renders it, or in one of its effects, React unmounts everything up to the nearest error boundary. With no boundary above, the whole app goes blank: a \`<UserPage>\` that throws \`NotFoundError\` for a wrong id takes the menu and every other route down with it, where a boundary could have shown "this user does not exist".
50
66
 
51
67
  ## What it does not catch
52
68
 
53
- - Errors in event handlers, timers and async code: a boundary never catches them, so they are not counted. \`async/handler-rejection-lost\` checks async handlers.
54
- - Errors stateward cannot see (a package it cannot read, a callback, a dynamic call): it is about known error classes, so these never make a finding. The hover on a boundary lists them.
55
- - An app with no \`createRoot(…).render(…)\` or \`hydrateRoot\` it can find: nothing is checked.
56
- - Routes in \`createBrowserRouter([...])\` route objects: React Router catches their errors itself (\`errorElement\`). \`memo\`, \`forwardRef\` and \`lazy\` components, and boundaries from other packages you have not listed in \`boundaries\`, are not followed.
57
- - An element passed to a function or stored in an object or array (\`showToast(<UserPage />)\`): where it renders cannot be followed, so it never makes a finding. An element held in a local \`const\` or \`let\` is checked where the variable is rendered.
58
- - A class that a \`try\` in the component may catch (with a test other than \`instanceof\`): it is not counted.
59
- - A throw that is not of a known error class (\`throw "no cart"\`), and a thrown promise (Suspense).
69
+ It starts from your entry point (\`createRoot(…).render(…)\` or \`hydrateRoot\`) and reports, once, the highest place that renders a component which can throw a known error while rendering, with no boundary above; the related places show the path down to the throw. A component counts what its body and its hooks throw, what its \`useEffect\` and \`useLayoutEffect\` callbacks throw, and what the components it renders outside a boundary throw. A boundary is react-error-boundary's \`<ErrorBoundary>\` (the \`boundaries\` option adds other packages'), or a class of yours with \`getDerivedStateFromError\` or \`componentDidCatch\`. It is silent on:
70
+
71
+ - errors in event handlers, timers and async code: a boundary never catches them (\`async/handler-rejection-lost\` checks async handlers);
72
+ - errors stateward cannot see (a package it cannot read, a callback, a dynamic call): the hover on a boundary lists them;
73
+ - an app with no \`createRoot(…).render(…)\` or \`hydrateRoot\` it can find: nothing is checked;
74
+ - routes in \`createBrowserRouter([...])\` route objects (React Router catches their errors itself with \`errorElement\`); \`memo\`, \`forwardRef\` and \`lazy\` components; boundaries from packages not listed in \`boundaries\`;
75
+ - an element passed to a function or stored in an object or array (\`showToast(<UserPage />)\`). An element held in a local \`const\` or \`let\` is checked where it is rendered;
76
+ - a class that a \`try\` in the component may catch (with a test other than \`instanceof\`);
77
+ - a throw that is not of a known error class (\`throw "no cart"\`), and a thrown promise (Suspense);
78
+ - two kinds of throw that name a caller's programming mistake rather than a failure to show: the usual check in a context hook (\`if (!ctx) throw new Error("useAuth must be used within AuthProvider")\` on a value from \`useContext\`, or a comparison with a "missing" constant declared at the top of the file), and a check on a value the caller's type already rules out (\`props.title === undefined\` with \`title: string\`). A check on an optional prop or a parameter that can be \`undefined\` still counts, as does a throw on a truthy value (\`if (error) throw error\`) or a comparison (\`id === ""\`). Set \`skipContextGuards\` or \`skipInputGuards\` to \`false\` to count those too.
79
+
80
+ ## Getting the best from it
81
+
82
+ A boundary around the whole app (the one that wraps the tree at \`createRoot(…).render(…)\`) does not count: it catches the error, but the whole app is replaced by its fallback, which is rarely what you want for one page. The finding is then at the highest place below it. If an app-wide fallback is what you want, set \`rootBoundaryCounts: true\`; \`stateward plan\` then says the rule is silent because your app's root has an error boundary.
60
83
 
61
84
  ## Turn it on or off
62
85
 
63
- In \`stateward.config.ts\`:
86
+ Whether a page should handle an error itself or let a boundary do it is your call, so it is a warning. If every throw in render is a bug that should crash in development, turn it off.
64
87
 
65
88
  \`\`\`ts
66
89
  rules: { "errors/render-throw-without-boundary": "off" } // or "error"
67
- groups: { "errors/react": "off" } // the whole group
68
- rules: {
69
- "errors/render-throw-without-boundary": [
70
- "warning",
71
- { boundaries: { "react-error-boundary": ["ErrorBoundary"], "@sentry/react": ["ErrorBoundary"] } },
72
- ],
73
- } // Sentry's boundary counts too
74
- rules: {
75
- "errors/render-throw-without-boundary": ["warning", { rootBoundaryCounts: true }],
76
- } // a boundary around the whole app is enough
90
+ rules: { "errors/render-throw-without-boundary": ["warning", { boundaries: { "react-error-boundary": ["ErrorBoundary"], "@sentry/react": ["ErrorBoundary"] } }] }
91
+ rules: { "errors/render-throw-without-boundary": ["warning", { rootBoundaryCounts: true }] } // a boundary around the whole app is enough
77
92
  \`\`\`
78
93
  `;
79
94
  /** At most this many notes; a longer path keeps its first steps and its last. */
@@ -86,12 +101,12 @@ function rootHeadline(component, names) {
86
101
  return fitted((cut) => `\`<${cut(component)}>\` can throw ${codeList(names, cut)} while rendering; only the app-wide boundary catches it`);
87
102
  }
88
103
  function rootHint(component) {
89
- return withExample("the whole app is replaced; add a boundary around it, or set `rootBoundaryCounts` if that is what you want", ["<ErrorBoundary FallbackComponent={ErrorPage}>", ` <${component} />`, "</ErrorBoundary>"]);
104
+ return withExample("add a boundary around it, or set `rootBoundaryCounts` if replacing the whole app is intended:", ["<ErrorBoundary FallbackComponent={ErrorPage}>", ` <${component} />`, "</ErrorBoundary>"]);
90
105
  }
91
106
  /** The reason `plan` gives when a root boundary makes the rule silent. */
92
107
  export const ROOT_BOUNDARY_SILENT = "your app's root has an error boundary";
93
108
  function hint(component) {
94
- return withExample("wrap it in an error boundary, here or nearer to what throws; for example with react-error-boundary:", ["<ErrorBoundary FallbackComponent={ErrorPage}>", ` <${component} />`, "</ErrorBoundary>"]);
109
+ return withExample("wrap it in an error boundary, at this render or nearer to what throws; for example with react-error-boundary:", ["<ErrorBoundary FallbackComponent={ErrorPage}>", ` <${component} />`, "</ErrorBoundary>"]);
95
110
  }
96
111
  /** The renders down from the site to the component that throws `cls`, then the call that throws it. */
97
112
  function pathNotes(model, site, cls) {
@@ -101,7 +116,7 @@ function pathNotes(model, site, cls) {
101
116
  return [];
102
117
  const notes = path.map((edge) => ({
103
118
  location: { file: edge.file, start: edge.render.at[0], end: edge.render.at[1] },
104
- message: `\`<${edge.render.name}>\` is rendered here`,
119
+ message: `\`<${edge.render.name}>\` is rendered`,
105
120
  }));
106
121
  const thrower = path.at(-1)?.to ?? site.to;
107
122
  const brought = model.own(thrower).classes.get(cls);
@@ -138,11 +153,13 @@ function belowRoot(model, entry) {
138
153
  export const renderThrowWithoutBoundary = defineRule({
139
154
  kind: "project",
140
155
  meta: {
141
- description: "A component that can throw a known error while rendering has an error boundary above it.",
156
+ description: "An error thrown while rendering, with no error boundary above the component, unmounts the whole React tree and leaves a blank page.",
157
+ summary: "a component that can throw a known error while rendering, with no error boundary above it",
142
158
  defaultSeverity: "warning",
143
159
  tier: "core",
144
160
  group: GROUP,
145
161
  needsJsDoc: true,
162
+ needsTypes: { library: "React", modules: ["react"] },
146
163
  options: {
147
164
  skipContextGuards: option.boolean({
148
165
  default: true,
@@ -3,62 +3,55 @@ import { defineRule, matchesName, option } from "@stateward/core";
3
3
  import { example, resultShapeOf, shorten } from "./result-type.js";
4
4
  const EXPLAIN = `# errors/result-ignored
5
5
 
6
- The Result of a call is checked or passed on, not dropped as a statement, after void or after await.
6
+ A \`Result\` dropped as a statement discards its error, so a failed call looks the same as a successful one.
7
7
 
8
- ## Why
9
-
10
- A \`Result\` is how a function says "this can fail". Call \`saveUser(user)\` as a statement and the answer is thrown away: nobody sees the error, and the code goes on as if the user was saved. The failure shows up later, as missing data.
11
-
12
- ## When to use it
13
-
14
- Use it if your functions return a \`Result\` instead of throwing: your own, neverthrow's, or a \`safeParse\` result. It is on as an error in every preset. To drop a result on purpose, list the function in \`ignore\`, or set \`allowVoid\` and write \`void save()\`.
15
-
16
- ## What it catches
8
+ \`\`\`ts example-bad
9
+ type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
17
10
 
18
- A call whose value is a Result (or a \`Promise\` of one) that is dropped: written as a statement, after \`void\`, or after \`await\` on its own. A Result is recognised by its shape, so no library is needed:
11
+ declare function saveUser(name: string): Promise<Result<void>>;
19
12
 
20
- - a union told apart by \`ok: true\` and \`ok: false\` (\`{ ok: true; value: T } | { ok: false; error: E }\`);
21
- - a union told apart by \`success: true\` and \`success: false\` (zod's \`safeParse\`);
22
- - a type with \`isOk()\` and \`isErr()\` methods (neverthrow);
23
- - a type the \`resultTypes\` option names (\`["Result", "Either"]\`), whatever its shape.
13
+ export async function rename(name: string): Promise<void> {
14
+ await saveUser(name);
15
+ }
16
+ \`\`\`
24
17
 
25
- ### Bad
18
+ Check the result, or return it to your caller:
26
19
 
27
- \`\`\`ts
20
+ \`\`\`ts example-good
28
21
  type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
29
- declare function saveUser(user: User): Promise<Result<void>>;
30
-
31
- await saveUser(user);
32
- \`\`\`
33
22
 
34
- ### Good
23
+ declare function saveUser(name: string): Promise<Result<void>>;
35
24
 
36
- \`\`\`ts
37
- const saved = await saveUser(user);
38
- if (!saved.ok) return saved;
25
+ export async function rename(name: string): Promise<Result<void>> {
26
+ const saved = await saveUser(name);
27
+ if (!saved.ok) return saved;
28
+ return { ok: true, value: undefined };
29
+ }
39
30
  \`\`\`
40
31
 
41
- ### Fix
32
+ The message shows how, with your names and your Result's shape. There is no quick fix.
33
+
34
+ ## Why it matters
42
35
 
43
- Check the result, or return it to your caller; the message shows how with your names and your Result's shape. There is no quick fix.
36
+ A \`Result\` is how a function says "this can fail". Call \`saveUser(name)\` as a statement and the answer is discarded: no code reads the error, and the code goes on as if the user was saved. The failure shows up later, as missing data.
44
37
 
45
38
  ## What it does not catch
46
39
 
47
- - A result that is assigned, returned, passed on, used in a condition or spread.
48
- - A Result of another shape (an \`Either\` with \`_tag\`, a tuple \`[error, value]\`), unless \`resultTypes\` names it.
49
- - A result dropped in a callback that returns nothing (\`items.forEach((item) => save(item))\`), or after \`&&\` or \`?.\`.
50
- - A result stored and never looked at (\`const saved = saveUser(user);\` with \`saved\` unused).
51
- - Reading \`.value\` without checking \`.ok\`.
40
+ A Result is recognised by its shape, so no library is needed: a union told apart by \`ok: true\` and \`ok: false\`, or by \`success: true\` and \`success: false\` (zod's \`safeParse\`); a type with \`isOk()\` and \`isErr()\` methods (neverthrow); or a type the \`resultTypes\` option names. It is silent on:
41
+
42
+ - a result that is assigned, returned, passed on, used in a condition or spread;
43
+ - a Result of another shape (an \`Either\` with \`_tag\`, a tuple \`[error, value]\`), unless \`resultTypes\` names it;
44
+ - a result dropped in a callback that returns nothing (\`items.forEach((item) => save(item))\`), or after \`&&\` or \`?.\`;
45
+ - a result stored and never looked at (\`const saved = saveUser(name);\` with \`saved\` unused), and reading \`.value\` without checking \`.ok\`: \`errors/result-unchecked\` looks at those reads.
52
46
 
53
47
  ## Turn it on or off
54
48
 
55
- In \`stateward.config.ts\`:
49
+ To drop a result on purpose, list the function in \`ignore\`, or set \`allowVoid\` and write \`void save()\`:
56
50
 
57
51
  \`\`\`ts
58
- rules: { "errors/result-ignored": "off" } // or "warning"
59
- groups: { "values/results": "off" } // the whole group
60
52
  rules: { "errors/result-ignored": ["error", { allowVoid: true }] } // \`void save()\` drops it on purpose
61
53
  rules: { "errors/result-ignored": ["error", { resultTypes: ["Either"] }] } // your own name for a Result
54
+ rules: { "errors/result-ignored": "off" }
62
55
  \`\`\`
63
56
  `;
64
57
  /** The longest callee a headline shows before it is cut, and the longest call an example line shows. */
@@ -136,7 +129,7 @@ function checkStatement(statement, ctx) {
136
129
  ctx.report({
137
130
  node: statement,
138
131
  about: statement,
139
- message: `the result of \`${calleeText(call, ctx)}(…)\` is dropped, so its error is never handled`,
132
+ message: `the \`Result\` of \`${calleeText(call, ctx)}(…)\` is dropped: its error is never handled`,
140
133
  hint: [
141
134
  "check it, or pass it on:",
142
135
  example([`const saved = ${promised ? "await " : ""}${callText};`, checkLine(shape)]),
@@ -146,7 +139,8 @@ function checkStatement(statement, ctx) {
146
139
  export const resultIgnored = defineRule({
147
140
  kind: "file",
148
141
  meta: {
149
- description: "The Result of a call is checked or passed on, not dropped as a statement, after void or after await.",
142
+ description: "A `Result` dropped as a statement discards its error, so a failed call looks the same as a successful one.",
143
+ summary: "a `Result` dropped as a statement, after `void` or after `await`",
150
144
  defaultSeverity: "error",
151
145
  tier: "core",
152
146
  group: "callers",