@stateward/plugin-errors 1.0.1-rc.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.
Files changed (46) hide show
  1. package/README.md +60 -3
  2. package/dist/catch-hides-unexpected.js +47 -58
  3. package/dist/catch-misses-error.js +57 -67
  4. package/dist/finally-can-throw.js +45 -54
  5. package/dist/inference/format.d.ts +3 -3
  6. package/dist/inference/format.js +5 -5
  7. package/dist/inference/solve.js +1 -1
  8. package/dist/inference/summarize.d.ts +1 -1
  9. package/dist/inference/summarize.js +2 -2
  10. package/dist/must-handle-error.js +58 -66
  11. package/dist/narrow-try.d.ts +6 -1
  12. package/dist/narrow-try.js +137 -77
  13. package/dist/plugin.d.ts +0 -27
  14. package/dist/plugin.js +1 -3
  15. package/dist/react/boundaries.d.ts +3 -3
  16. package/dist/react/boundaries.js +5 -5
  17. package/dist/react/fallbacks.js +2 -2
  18. package/dist/refactors.d.ts +1 -1
  19. package/dist/refactors.js +1 -1
  20. package/dist/render-throw-without-boundary.js +64 -47
  21. package/dist/result-ignored.js +31 -37
  22. package/dist/result-unchecked.js +58 -61
  23. package/dist/rules.meta.d.ts +22 -47
  24. package/dist/rules.meta.js +19 -47
  25. package/dist/throws-doc-mismatch.js +35 -44
  26. package/package.json +2 -2
  27. package/dist/async-handler-unhandled.d.ts +0 -7
  28. package/dist/async-handler-unhandled.js +0 -342
  29. package/dist/catch-without-intent.d.ts +0 -10
  30. package/dist/catch-without-intent.js +0 -590
  31. package/dist/exhaustive-catch.d.ts +0 -6
  32. package/dist/exhaustive-catch.js +0 -190
  33. package/dist/exhaustive-fallback.d.ts +0 -6
  34. package/dist/exhaustive-fallback.js +0 -157
  35. package/dist/finally-may-throw.d.ts +0 -6
  36. package/dist/finally-may-throw.js +0 -184
  37. package/dist/no-swallow.d.ts +0 -8
  38. package/dist/no-swallow.js +0 -320
  39. package/dist/render-throws-without-boundary.d.ts +0 -6
  40. package/dist/render-throws-without-boundary.js +0 -151
  41. package/dist/rethrow-unknown.d.ts +0 -6
  42. package/dist/rethrow-unknown.js +0 -260
  43. package/dist/throw-in-hook.d.ts +0 -8
  44. package/dist/throw-in-hook.js +0 -510
  45. package/dist/throws-matches-doc.d.ts +0 -7
  46. package/dist/throws-matches-doc.js +0 -188
@@ -3,79 +3,75 @@ import { defineRule } from "@stateward/core";
3
3
  import { example, resultShapeOf, shorten } from "./result-type.js";
4
4
  const EXPLAIN = `# errors/result-unchecked
5
5
 
6
- A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.
6
+ A call that returns its failure instead of throwing never reaches the \`catch\`; without a check, the code after it runs as if the call worked.
7
7
 
8
- ## Why
8
+ \`\`\`ts example-bad
9
+ type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
9
10
 
10
- Some calls do not throw when they fail: they resolve to data or an error. An RTK Query trigger without \`.unwrap()\` is the common one. Wrap \`await deletePlan(id)\` in a \`try\` and the \`catch\` never runs: a failed delete still shows "Deleted" and closes the dialog. Store the result and show its error, and the lines after still run: "Saved" appears after a save that failed.
11
+ declare function saveSettings(values: string): Promise<Result<void>>;
12
+ declare function toast(text: string): void;
11
13
 
12
- ## When to use it
13
-
14
- Use it if you call RTK Query triggers, or functions that return a Result (\`{ ok: true; value } | { ok: false; error }\`, zod's \`safeParse\`, neverthrow), and await them in event handlers. It is a warning in every preset.
15
-
16
- ## What it catches
14
+ export async function submit(values: string): Promise<void> {
15
+ const result = await saveSettings(values);
16
+ if (!result.ok) toast("Could not save");
17
+ toast("Saved");
18
+ }
19
+ \`\`\`
17
20
 
18
- A call whose value is data or an error: a promise with an \`unwrap()\` method that resolves to a union with an \`error\` member (RTK Query's triggers; found by the type, not by a name), or a Result recognised as \`errors/result-ignored\` recognises it. Then:
21
+ When it failed, stop there: \`return\` (or \`throw\`) in the check:
19
22
 
20
- - awaited as a statement inside a \`try\` whose \`catch\` nothing else can reach (no call before it, no other \`await\` or \`throw\` in the \`try\`): the \`catch\` never runs;
21
- - awaited as a statement inside a \`try\` after another call (an optimistic \`setFolders(…)\`, a \`parse(values)\`), with more lines after it: when it fails, those lines run (\`notify("Folder updated!")\`) and the \`catch\` (the rollback) does not;
22
- - a trigger awaited as a statement outside a \`try\`, with more lines after it (\`await sendReply(x); setText("")\`);
23
- - stored, then its data read before it is checked;
24
- - stored, its error read in an \`if\` that does not \`return\` or \`throw\` (\`"error" in result\`, \`result.error\`, or an \`error\` or \`isError\` taken out of it), with more lines after that \`if\`.
23
+ \`\`\`ts example-good
24
+ type Result<T> = { ok: true; value: T } | { ok: false; error: Error };
25
25
 
26
- ### Bad
26
+ declare function saveSettings(values: string): Promise<Result<void>>;
27
+ declare function toast(text: string): void;
27
28
 
28
- \`\`\`ts
29
- try {
30
- await deletePlan(id);
31
- toast.success("Deleted");
32
- } catch {
33
- toast.error("Could not delete");
29
+ export async function submit(values: string): Promise<void> {
30
+ const result = await saveSettings(values);
31
+ if (!result.ok) {
32
+ toast("Could not save");
33
+ return;
34
+ }
35
+ toast("Saved");
34
36
  }
35
-
36
- const result = await saveSettings(values);
37
- if ("error" in result) toast.error("Could not save");
38
- toast.success("Saved");
39
37
  \`\`\`
40
38
 
41
- ### Good
39
+ For an RTK Query trigger, add \`.unwrap()\` so a failure throws: \`await deletePlan(id).unwrap()\`. Where the result is dropped, a quick fix adds it.
42
40
 
43
- \`\`\`ts
44
- try {
45
- await deletePlan(id).unwrap();
46
- toast.success("Deleted");
47
- } catch {
48
- toast.error("Could not delete");
49
- }
41
+ ## Why it matters
50
42
 
51
- const result = await saveSettings(values);
52
- if ("error" in result) {
53
- toast.error("Could not save");
54
- return;
55
- }
56
- toast.success("Saved");
57
- \`\`\`
43
+ Some calls do not throw when they fail: they resolve to data or an error. An RTK Query trigger without \`.unwrap()\` is the common one. Wrap \`await deletePlan(id)\` in a \`try\` and the \`catch\` never runs: a failed delete still shows "Deleted" and closes the dialog. Store the result and show its error without stopping, and the lines after still run: "Saved" appears after a save that failed.
58
44
 
59
- ### Fix
45
+ ## When it says “not checked”
60
46
 
61
- Add \`.unwrap()\` so a failure throws (a quick fix where the result is dropped), or check the result and \`return\` (or \`throw\`) when it failed. Code that says what it means is left alone: \`void trigger(…)\`, \`.unwrap()\`, a check (\`"error" in result\`, \`if (result.error) return\`, \`isOk(result)\`, \`const { data, isError } = await dispatch(…); if (isError) throw …\`) before the success path, an \`if\` with an \`else\` for the success path, and a component that reads the trigger's own \`isError\` or \`error\` from its hook (\`const [save, { isError }] = useSaveMutation()\`).
47
+ An \`if\` that tests the result in a way stateward cannot read (\`if (result.error && isApiError(result.error))\`) is listed as a place not checked. Test it in one of the ways below, then narrow further inside.
62
48
 
63
49
  ## What it does not catch
64
50
 
65
- - A Result (\`ok\`, \`success\`, neverthrow) dropped as a statement: that is \`errors/result-ignored\`'s.
66
- - Lines after the call that only set back a pending state set before it (\`setSaving(true)\` … \`setSaving(false)\`): they run either way on purpose.
67
- - A \`try\` with another \`await\` or a \`throw\` in it: the \`catch\` may be there for that.
68
- - A result passed on, returned or stored elsewhere, or checked in a callback.
69
- - A promise that rejects on failure (react-query's \`mutateAsync\`, \`fetch\`): it has no \`unwrap()\` that resolves to data or an error.
70
- - An \`if\` that tests the result in a way stateward cannot read (\`if (result.error && isApiError(result.error))\`): it is listed as a place not checked.
51
+ It looks at a call whose value is data or an error: a promise with an \`unwrap()\` method that resolves to a union with an \`error\` member (RTK Query's triggers, found by the type, not by a name), or a Result as \`errors/result-ignored\` recognises it. It reports such a call when:
71
52
 
72
- ## Turn it on or off
53
+ - it is awaited as a statement inside a \`try\` whose \`catch\` nothing else can reach (no call before it, no other \`await\` or \`throw\` in the \`try\`): the \`catch\` never runs;
54
+ - it is awaited as a statement inside a \`try\` after another call (an optimistic \`setFolders(…)\`), with more lines after it: when it fails, those lines run and the \`catch\` (the rollback) does not;
55
+ - a trigger is awaited as a statement outside a \`try\`, with more lines after it (\`await sendReply(x); setText("")\`);
56
+ - it is stored, then its data is read before it is checked;
57
+ - it is stored, and its error is read in an \`if\` that does not \`return\` or \`throw\` (\`"error" in result\`, \`result.error\`, \`!result.ok\`, or an \`error\` or \`isError\` taken out of it), with more lines after that \`if\`.
58
+
59
+ It is silent on:
73
60
 
74
- In \`stateward.config.ts\`:
61
+ - a Result dropped as a statement: that is \`errors/result-ignored\`;
62
+ - lines after the call that only set back a pending state set before it (\`setSaving(true)\` … \`setSaving(false)\`): they run either way on purpose;
63
+ - a \`try\` with another \`await\` or a \`throw\` in it: the \`catch\` may be there for that;
64
+ - a result passed on, returned, stored elsewhere, or checked in a callback;
65
+ - a promise that rejects on failure (react-query's \`mutateAsync\`, \`fetch\`): it has no \`unwrap()\` that resolves to data or an error.
66
+
67
+ ## Getting the best from it
68
+
69
+ Code that says what it means is left alone: \`void trigger(…)\`, \`.unwrap()\`, a check (\`"error" in result\`, \`if (result.error) return\`, \`isOk(result)\`, \`const { data, isError } = await dispatch(…); if (isError) throw …\`) before the success path, an \`if\` with an \`else\` for the success path, and a component that reads the trigger's own \`isError\` or \`error\` from its hook (\`const [save, { isError }] = useSaveMutation()\`).
70
+
71
+ ## Turn it on or off
75
72
 
76
73
  \`\`\`ts
77
74
  rules: { "errors/result-unchecked": "off" } // or "error"
78
- groups: { "errors/callers": "off" } // the whole group
79
75
  \`\`\`
80
76
  `;
81
77
  /** The longest call an example line shows before its arguments become `…`. */
@@ -428,7 +424,7 @@ function checkTry(statement, ctx) {
428
424
  continue;
429
425
  ctx.report({
430
426
  node: inner,
431
- message: `the lines after this run even when \`${calleeText(call, ctx)}(…)\` failed, and the \`catch\` does not`,
427
+ message: `code after the call runs even when \`${calleeText(call, ctx)}(…)\` failed, and the \`catch\` does not`,
432
428
  hint: [
433
429
  'add `.unwrap()` so a failure reaches the `catch` (quick fix), or check `"error" in result` and return:',
434
430
  example([
@@ -445,9 +441,9 @@ function checkTry(statement, ctx) {
445
441
  ctx.report({
446
442
  // The statement, as `errors/result-ignored` reports it, so that rule can yield to this one.
447
443
  node: inner,
448
- message: `\`${calleeText(call, ctx)}(…)\` does not throw when it fails, so this \`catch\` never runs`,
444
+ message: `\`catch\` never runs for \`${calleeText(call, ctx)}(…)\`: it does not throw when it fails`,
449
445
  hint: [
450
- 'the lines after it still run on failure; add `.unwrap()` (quick fix), or check `"error" in result` and return:',
446
+ 'add `.unwrap()` (quick fix), or check `"error" in result` and return, so the code after it does not run on failure:',
451
447
  example([`const result = await ${callText(call, ctx)};`, `if ("error" in result) return;`]),
452
448
  ].join("\n"),
453
449
  fixes: [unwrapFix(call, "Add `.unwrap()` so a failure reaches the `catch`", ctx)],
@@ -531,12 +527,12 @@ function checkDropped(statement, ctx) {
531
527
  return;
532
528
  ctx.report({
533
529
  node: statement,
534
- message: `the lines after this run even when \`${calleeText(call, ctx)}(…)\` failed`,
530
+ message: `code after the call runs even when \`${calleeText(call, ctx)}(…)\` failed`,
535
531
  hint: [
536
532
  'add `.unwrap()` so a failure throws (quick fix), or check `"error" in result` and return:',
537
533
  example([`const result = await ${callText(call, ctx)};`, `if ("error" in result) return;`]),
538
534
  ].join("\n"),
539
- fixes: [unwrapFix(call, "Add `.unwrap()` so a failure stops this function", ctx)],
535
+ fixes: [unwrapFix(call, "Add `.unwrap()` so a failure stops the function", ctx)],
540
536
  });
541
537
  }
542
538
  /** What a stored result is reached by: `const r = …` or `const { data, error } = …`. */
@@ -561,8 +557,8 @@ function trackedOf(declaration, call, kind, ctx) {
561
557
  return aliases.size === 0 ? undefined : { whole: undefined, aliases, callee, kind };
562
558
  }
563
559
  function goesOnHeadline(tracked, after) {
564
- const what = after === "if" ? "this `if`" : "this";
565
- return `the lines after ${what} run even when \`${tracked.callee}(…)\` failed`;
560
+ const what = after === "if" ? "the `if`" : "the call";
561
+ return `code after ${what} runs even when \`${tracked.callee}(…)\` failed`;
566
562
  }
567
563
  /**
568
564
  * The statements that run after a check that is the last of its list: those after the enclosing
@@ -644,8 +640,8 @@ function followStored(statement, before, rest, tracked, call, ctx) {
644
640
  case "undecided":
645
641
  ctx.unknown({
646
642
  node: outcome.test,
647
- message: `stateward cannot tell whether this \`if\` checks that \`${tracked.callee}(…)\` failed`,
648
- hint: `test the result itself to make it checkable, as in \`${checkLine(tracked)}\``,
643
+ message: `not checked: whether the \`if\` tests that \`${tracked.callee}(…)\` failed: it does not test the result itself`,
644
+ hint: `test the result itself, as in \`${checkLine(tracked)}\`, so the check can be followed`,
649
645
  });
650
646
  return;
651
647
  case "goes-on": {
@@ -723,7 +719,8 @@ function checkDeclaration(statement, ctx) {
723
719
  export const resultUnchecked = defineRule({
724
720
  kind: "file",
725
721
  meta: {
726
- description: "A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.",
722
+ description: "A call that returns its failure instead of throwing never reaches the `catch`; without a check, the code after it runs as if the call worked.",
723
+ summary: "a call that fails in its value (an RTK Query trigger, a Result) used as if it had worked",
727
724
  defaultSeverity: "warning",
728
725
  tier: "core",
729
726
  group: "callers",
@@ -2,7 +2,8 @@ export declare const RULES: {
2
2
  readonly "catch-hides-unexpected": {
3
3
  readonly kind: "file";
4
4
  readonly meta: {
5
- readonly description: "A catch that handles some errors with instanceof also rethrows or handles all the others.";
5
+ readonly description: "A `catch` that tests some error classes and has no last branch ignores every other error, bugs included; rethrowing the rest keeps them visible.";
6
+ readonly summary: "a `catch` that handles some error classes with `instanceof` and ignores the rest";
6
7
  readonly defaultSeverity: "error";
7
8
  readonly tier: "core";
8
9
  readonly group: "catch";
@@ -15,48 +16,11 @@ export declare const RULES: {
15
16
  };
16
17
  };
17
18
  };
18
- readonly "catch-without-intent": {
19
- readonly kind: "file";
20
- readonly meta: {
21
- readonly description: "Experimental: on real projects it is mostly wrong; it may change or be removed. A catch that handles every error says how: it narrows, reports, or falls back; one that only logs, or calls something stateward cannot judge, and carries on is reported.";
22
- readonly defaultSeverity: "warning";
23
- readonly tier: "addon";
24
- readonly addon: "catch-intent";
25
- readonly group: "catch";
26
- readonly heuristic: true;
27
- readonly options: {
28
- readonly loggers: {
29
- readonly kind: "strings";
30
- readonly default: readonly ["console", "logger", "log"];
31
- readonly description: "Objects whose every method only logs (globs like *Logger): a catch that only calls them swallows the error.";
32
- };
33
- readonly logMethods: {
34
- readonly kind: "strings";
35
- readonly default: readonly ["error", "warn", "info", "debug"];
36
- readonly description: "Method names that only log, on any object (`Sentry.captureException` is not one).";
37
- };
38
- readonly logFunctions: {
39
- readonly kind: "strings";
40
- readonly default: readonly [];
41
- readonly description: "Plain functions of your own that only log, like logError (globs allowed).";
42
- };
43
- readonly edgeHelpers: {
44
- readonly kind: "strings";
45
- readonly default: readonly [];
46
- 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.";
47
- };
48
- readonly swallowHelpers: {
49
- readonly kind: "strings";
50
- readonly default: readonly [];
51
- readonly description: "Functions that mark an error as ignored on purpose, like ignoreError (globs allowed): a catch that calls one is not reported.";
52
- };
53
- };
54
- };
55
- };
56
19
  readonly "finally-can-throw": {
57
20
  readonly kind: "file";
58
21
  readonly meta: {
59
- readonly description: "A finally block does not call something that throws, so the error that led to it is not lost.";
22
+ readonly description: "An error thrown in `finally` replaces the error that was already thrown, so the report names the cleanup instead of the original cause.";
23
+ readonly summary: "a `finally` that calls something that can throw and replace the original error";
60
24
  readonly defaultSeverity: "error";
61
25
  readonly tier: "core";
62
26
  readonly group: "catch";
@@ -77,7 +41,8 @@ export declare const RULES: {
77
41
  readonly "catch-misses-error": {
78
42
  readonly kind: "project";
79
43
  readonly meta: {
80
- readonly description: "A catch that branches with instanceof handles every error class its try can throw, or rethrows the rest; so does an error boundary's fallback.";
44
+ readonly description: "An error class the `try` can throw and the `catch` does not test goes to the generic branch, or is ignored; a branch for it, or a rethrow, handles it explicitly.";
45
+ readonly summary: "a `catch` that branches on error classes but misses one its `try` can throw";
81
46
  readonly defaultSeverity: "error";
82
47
  readonly tier: "core";
83
48
  readonly group: "catch";
@@ -94,7 +59,8 @@ export declare const RULES: {
94
59
  readonly "throws-doc-mismatch": {
95
60
  readonly kind: "project";
96
61
  readonly meta: {
97
- readonly description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.";
62
+ readonly description: "Callers decide what to catch from `@throws`: a class it omits reaches them unhandled, and a class listed in excess asks them to handle a case that cannot happen.";
63
+ readonly summary: "a `@throws` tag that lists an error the function cannot throw, or misses one it can";
98
64
  readonly defaultSeverity: "error";
99
65
  readonly tier: "core";
100
66
  readonly group: "callers";
@@ -120,11 +86,16 @@ export declare const RULES: {
120
86
  readonly "render-throw-without-boundary": {
121
87
  readonly kind: "project";
122
88
  readonly meta: {
123
- readonly description: "A component that can throw a known error while rendering has an error boundary above it.";
89
+ readonly description: "An error thrown while rendering, with no error boundary above the component, unmounts the whole React tree and leaves a blank page.";
90
+ readonly summary: "a component that can throw a known error while rendering, with no error boundary above it";
124
91
  readonly defaultSeverity: "warning";
125
92
  readonly tier: "core";
126
93
  readonly group: "render";
127
94
  readonly needsJsDoc: true;
95
+ readonly needsTypes: {
96
+ readonly library: "React";
97
+ readonly modules: readonly ["react"];
98
+ };
128
99
  readonly options: {
129
100
  readonly skipContextGuards: {
130
101
  readonly kind: "boolean";
@@ -154,7 +125,8 @@ export declare const RULES: {
154
125
  readonly "must-handle-error": {
155
126
  readonly kind: "project";
156
127
  readonly meta: {
157
- readonly description: "An error class marked @mustHandle is named in a catch or passed on with @throws by every call that can throw it.";
128
+ readonly description: "An error marked `@mustHandle` needs a decision at each call, such as a message to the user; a call that neither catches it nor passes it on lets it reach the top.";
129
+ readonly summary: "a call that can throw an error marked `@mustHandle`, with no code that handles or passes it on";
158
130
  readonly defaultSeverity: "error";
159
131
  readonly tier: "core";
160
132
  readonly group: "callers";
@@ -165,7 +137,8 @@ export declare const RULES: {
165
137
  readonly "narrow-try": {
166
138
  readonly kind: "project";
167
139
  readonly meta: {
168
- readonly description: "Calls to your own code placed in a `try` after the call it is for: move them after the `try`.";
140
+ readonly description: "A bug in code inside a `try` meant for another call is caught and reported as that call's failure; moving the code after the `try` keeps the two apart.";
141
+ readonly summary: "calls to your own code inside a `try` after the call it is for, so their bugs look like its failure";
169
142
  readonly defaultSeverity: "warning";
170
143
  readonly tier: "addon";
171
144
  readonly addon: "narrow-try";
@@ -176,7 +149,8 @@ export declare const RULES: {
176
149
  readonly "result-ignored": {
177
150
  readonly kind: "file";
178
151
  readonly meta: {
179
- readonly description: "The Result of a call is checked or passed on, not dropped as a statement, after void or after await.";
152
+ readonly description: "A `Result` dropped as a statement discards its error, so a failed call looks the same as a successful one.";
153
+ readonly summary: "a `Result` dropped as a statement, after `void` or after `await`";
180
154
  readonly defaultSeverity: "error";
181
155
  readonly tier: "core";
182
156
  readonly group: "callers";
@@ -203,7 +177,8 @@ export declare const RULES: {
203
177
  readonly "result-unchecked": {
204
178
  readonly kind: "file";
205
179
  readonly meta: {
206
- readonly description: "A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.";
180
+ readonly description: "A call that returns its failure instead of throwing never reaches the `catch`; without a check, the code after it runs as if the call worked.";
181
+ readonly summary: "a call that fails in its value (an RTK Query trigger, a Result) used as if it had worked";
207
182
  readonly defaultSeverity: "warning";
208
183
  readonly tier: "core";
209
184
  readonly group: "callers";
@@ -2,7 +2,8 @@ export const RULES = {
2
2
  "catch-hides-unexpected": {
3
3
  kind: "file",
4
4
  meta: {
5
- description: "A catch that handles some errors with instanceof also rethrows or handles all the others.",
5
+ description: "A `catch` that tests some error classes and has no last branch ignores every other error, bugs included; rethrowing the rest keeps them visible.",
6
+ summary: "a `catch` that handles some error classes with `instanceof` and ignores the rest",
6
7
  defaultSeverity: "error",
7
8
  tier: "core",
8
9
  group: "catch",
@@ -15,48 +16,11 @@ export const RULES = {
15
16
  },
16
17
  },
17
18
  },
18
- "catch-without-intent": {
19
- kind: "file",
20
- meta: {
21
- description: "Experimental: on real projects it is mostly wrong; it may change or be removed. A catch that handles every error says how: it narrows, reports, or falls back; one that only logs, or calls something stateward cannot judge, and carries on is reported.",
22
- defaultSeverity: "warning",
23
- tier: "addon",
24
- addon: "catch-intent",
25
- group: "catch",
26
- heuristic: true,
27
- options: {
28
- loggers: {
29
- kind: "strings",
30
- default: ["console", "logger", "log"],
31
- description: "Objects whose every method only logs (globs like *Logger): a catch that only calls them swallows the error.",
32
- },
33
- logMethods: {
34
- kind: "strings",
35
- default: ["error", "warn", "info", "debug"],
36
- description: "Method names that only log, on any object (`Sentry.captureException` is not one).",
37
- },
38
- logFunctions: {
39
- kind: "strings",
40
- default: [],
41
- description: "Plain functions of your own that only log, like logError (globs allowed).",
42
- },
43
- edgeHelpers: {
44
- kind: "strings",
45
- default: [],
46
- 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.",
47
- },
48
- swallowHelpers: {
49
- kind: "strings",
50
- default: [],
51
- description: "Functions that mark an error as ignored on purpose, like ignoreError (globs allowed): a catch that calls one is not reported.",
52
- },
53
- },
54
- },
55
- },
56
19
  "finally-can-throw": {
57
20
  kind: "file",
58
21
  meta: {
59
- description: "A finally block does not call something that throws, so the error that led to it is not lost.",
22
+ description: "An error thrown in `finally` replaces the error that was already thrown, so the report names the cleanup instead of the original cause.",
23
+ summary: "a `finally` that calls something that can throw and replace the original error",
60
24
  defaultSeverity: "error",
61
25
  tier: "core",
62
26
  group: "catch",
@@ -77,7 +41,8 @@ export const RULES = {
77
41
  "catch-misses-error": {
78
42
  kind: "project",
79
43
  meta: {
80
- description: "A catch that branches with instanceof handles every error class its try can throw, or rethrows the rest; so does an error boundary's fallback.",
44
+ description: "An error class the `try` can throw and the `catch` does not test goes to the generic branch, or is ignored; a branch for it, or a rethrow, handles it explicitly.",
45
+ summary: "a `catch` that branches on error classes but misses one its `try` can throw",
81
46
  defaultSeverity: "error",
82
47
  tier: "core",
83
48
  group: "catch",
@@ -94,7 +59,8 @@ export const RULES = {
94
59
  "throws-doc-mismatch": {
95
60
  kind: "project",
96
61
  meta: {
97
- description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.",
62
+ description: "Callers decide what to catch from `@throws`: a class it omits reaches them unhandled, and a class listed in excess asks them to handle a case that cannot happen.",
63
+ summary: "a `@throws` tag that lists an error the function cannot throw, or misses one it can",
98
64
  defaultSeverity: "error",
99
65
  tier: "core",
100
66
  group: "callers",
@@ -120,11 +86,13 @@ export const RULES = {
120
86
  "render-throw-without-boundary": {
121
87
  kind: "project",
122
88
  meta: {
123
- description: "A component that can throw a known error while rendering has an error boundary above it.",
89
+ description: "An error thrown while rendering, with no error boundary above the component, unmounts the whole React tree and leaves a blank page.",
90
+ summary: "a component that can throw a known error while rendering, with no error boundary above it",
124
91
  defaultSeverity: "warning",
125
92
  tier: "core",
126
93
  group: "render",
127
94
  needsJsDoc: true,
95
+ needsTypes: { library: "React", modules: ["react"] },
128
96
  options: {
129
97
  skipContextGuards: {
130
98
  kind: "boolean",
@@ -152,7 +120,8 @@ export const RULES = {
152
120
  "must-handle-error": {
153
121
  kind: "project",
154
122
  meta: {
155
- description: "An error class marked @mustHandle is named in a catch or passed on with @throws by every call that can throw it.",
123
+ description: "An error marked `@mustHandle` needs a decision at each call, such as a message to the user; a call that neither catches it nor passes it on lets it reach the top.",
124
+ summary: "a call that can throw an error marked `@mustHandle`, with no code that handles or passes it on",
156
125
  defaultSeverity: "error",
157
126
  tier: "core",
158
127
  group: "callers",
@@ -163,7 +132,8 @@ export const RULES = {
163
132
  "narrow-try": {
164
133
  kind: "project",
165
134
  meta: {
166
- description: "Calls to your own code placed in a `try` after the call it is for: move them after the `try`.",
135
+ description: "A bug in code inside a `try` meant for another call is caught and reported as that call's failure; moving the code after the `try` keeps the two apart.",
136
+ summary: "calls to your own code inside a `try` after the call it is for, so their bugs look like its failure",
167
137
  defaultSeverity: "warning",
168
138
  tier: "addon",
169
139
  addon: "narrow-try",
@@ -174,7 +144,8 @@ export const RULES = {
174
144
  "result-ignored": {
175
145
  kind: "file",
176
146
  meta: {
177
- description: "The Result of a call is checked or passed on, not dropped as a statement, after void or after await.",
147
+ description: "A `Result` dropped as a statement discards its error, so a failed call looks the same as a successful one.",
148
+ summary: "a `Result` dropped as a statement, after `void` or after `await`",
178
149
  defaultSeverity: "error",
179
150
  tier: "core",
180
151
  group: "callers",
@@ -201,7 +172,8 @@ export const RULES = {
201
172
  "result-unchecked": {
202
173
  kind: "file",
203
174
  meta: {
204
- description: "A call that reports failure in its value, not by throwing, is checked before the code goes on as if it worked.",
175
+ description: "A call that returns its failure instead of throwing never reaches the `catch`; without a check, the code after it runs as if the call worked.",
176
+ summary: "a call that fails in its value (an RTK Query trigger, a Result) used as if it had worked",
205
177
  defaultSeverity: "warning",
206
178
  tier: "core",
207
179
  group: "callers",
@@ -7,65 +7,55 @@ import { codeList, fitted } from "./text.js";
7
7
  export const GROUP = "callers";
8
8
  const EXPLAIN = `# errors/throws-doc-mismatch
9
9
 
10
- A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.
10
+ Callers decide what to catch from \`@throws\`: a class it omits reaches them unhandled, and a class listed in excess asks them to handle a case that cannot happen.
11
11
 
12
- ## Why
12
+ \`\`\`ts example-bad
13
+ export class NotFoundError extends Error {}
13
14
 
14
- \`@throws {T}\` tells callers which errors to expect, and stateward checks every caller's \`catch\` against it. A tag that has drifted from the code misleads both. If \`loadUser\` says \`@throws {NotFoundError}\` but also calls \`JSON.parse\`, a \`SyntaxError\` slips past a \`catch\` written for \`NotFoundError\`. A tag that lists an error the function can no longer throw sends callers to handle something that never comes.
15
-
16
- ## When to use it
17
-
18
- Use it if you write \`@throws\` tags, typically on public functions and module boundaries. It is on as an error in every preset, and does nothing for functions without the tag, so it costs nothing until you start writing them.
19
-
20
- ## What it catches
21
-
22
- It compares the tag with what the function can throw (its own \`throw new X(…)\`, the functions it calls, and the built-ins stateward knows), and reports:
23
-
24
- - a class in the tag that the function cannot throw, neither it nor a subclass;
25
- - a class the function can throw that the tag does not cover, by itself or by a class it extends.
26
-
27
- On a function that returns a promise, \`@throws\` means what the promise rejects with: what callers see when they \`await\` it or use \`.catch\`.
28
-
29
- ### Bad
30
-
31
- \`\`\`ts
32
15
  /** @throws {NotFoundError} */
33
- export async function loadUser(id: string): Promise<User> {
34
- const text = await readUser(id);
35
- return JSON.parse(text); // SyntaxError, not listed
16
+ export function loadUser(text: string): { name: string } {
17
+ if (text === "") throw new NotFoundError("no such user");
18
+ return JSON.parse(text) as { name: string };
36
19
  }
37
20
  \`\`\`
38
21
 
39
- ### Good
22
+ Update the tag to what the function can throw; the message gives the one to write:
23
+
24
+ \`\`\`ts example-good
25
+ export class NotFoundError extends Error {}
40
26
 
41
- \`\`\`ts
42
27
  /** @throws {NotFoundError | SyntaxError} */
43
- export async function loadUser(id: string): Promise<User> {
44
- const text = await readUser(id);
45
- return JSON.parse(text);
28
+ export function loadUser(text: string): { name: string } {
29
+ if (text === "") throw new NotFoundError("no such user");
30
+ return JSON.parse(text) as { name: string };
46
31
  }
47
32
  \`\`\`
48
33
 
49
- ### Fix
34
+ The message links to the call that throws each missing class. There is no quick fix.
35
+
36
+ ## Why it matters
50
37
 
51
- Update the tag; the message gives the one to write and links to the call that throws each missing class. There is no quick fix.
38
+ \`@throws {T}\` tells callers which errors to expect, and stateward checks every caller's \`catch\` against it. A tag that has drifted from the code misleads both: here a \`SyntaxError\` from \`JSON.parse\` slips past a \`catch\` written for \`NotFoundError\`. A tag that lists an error the function can no longer throw sends callers to handle something that never comes.
39
+
40
+ ## When it says “not checked”
41
+
42
+ - A listed class that comes from a call stateward cannot see into (a package, a callback, a dynamic call): it names the call. Remove the class from the tag if the function cannot throw it, or declare the package's errors under \`facts\` in the config (\`facts: { "config-lib": { load: { throws: ["ConfigError"] } } }\`).
43
+ - A name in the tag that is not a class in scope: import the class, or use a name in scope.
52
44
 
53
45
  ## What it does not catch
54
46
 
55
- - A function without \`@throws\`.
56
- - A tag without a type (\`@throws when the id is empty\`): it only says the function throws.
57
- - A listed class that comes from a call stateward cannot see into (a package, a callback, a dynamic call). It says so in an info finding, naming the call: remove the class from the tag if the function cannot throw it, or declare the package's errors under \`facts\` in the config.
58
- - A name in the tag that is not a class in scope: also an info finding. Import the class, or use a name in scope.
59
- - A class that a surrounding \`try\` may catch: only classes that surely escape are reported as missing.
60
- - Errors from the JavaScript engine itself, such as reading a property of \`undefined\`.
47
+ It compares the tag with what the function can throw (its own \`throw new X(…)\`, the functions it calls, and the built-ins stateward knows) and reports a class in the tag that the function cannot throw (neither it nor a subclass), and a class it can throw that the tag does not cover. On a function that returns a promise, \`@throws\` means what the promise rejects with. It is silent on:
48
+
49
+ - a function without \`@throws\`, or a tag without a type (\`@throws when the id is empty\`): that only says the function throws;
50
+ - a class that a surrounding \`try\` may catch: only classes that surely escape are reported as missing;
51
+ - errors from the JavaScript engine itself, such as reading a property of \`undefined\`.
61
52
 
62
53
  ## Turn it on or off
63
54
 
64
- In \`stateward.config.ts\`:
55
+ It does nothing for functions without the tag, so it costs nothing until you start writing them.
65
56
 
66
57
  \`\`\`ts
67
58
  rules: { "errors/throws-doc-mismatch": "off" } // or "warning"
68
- groups: { "errors/inference": "off" } // the whole group
69
59
  rules: { "errors/throws-doc-mismatch": ["error", { severities: { extra: "warning" } }] } // a stale tag only warns
70
60
  \`\`\`
71
61
  `;
@@ -109,7 +99,7 @@ function check(entry, analysis, ctx) {
109
99
  const location = locationOf(entry.file, item.at);
110
100
  if (item.id === null) {
111
101
  ctx.unknown({
112
- message: fitted((cut) => `stateward cannot find the class \`${cut(item.name)}\` this \`@throws\` names`),
102
+ message: fitted((cut) => `not checked: \`@throws\` names \`${cut(item.name)}\`: no class of that name is in scope`),
113
103
  hint: "name an error class that is in scope in this file, or import it",
114
104
  location,
115
105
  });
@@ -122,14 +112,14 @@ function check(entry, analysis, ctx) {
122
112
  const listed = className(id, classes);
123
113
  if (firstUnknown !== undefined) {
124
114
  ctx.unknown({
125
- message: fitted((cut) => `stateward cannot tell whether \`${cut(name)}\` throws \`${cut(listed)}\` (via \`${cut(firstUnknown.text)}\`: ${firstUnknown.reason})`),
126
- hint: `if \`${name}\` cannot throw it, remove \`${listed}\` from the tag; else add \`@throws\` where it comes from`,
115
+ message: fitted((cut) => `not checked: whether \`${cut(name)}\` throws \`${cut(listed)}\`: what \`${cut(firstUnknown.text)}\` throws is not known (${firstUnknown.reason})`),
116
+ hint: `remove \`${listed}\` from the tag if \`${name}\` cannot throw it; otherwise add \`@throws\` where it comes from`,
127
117
  location,
128
118
  });
129
119
  continue;
130
120
  }
131
121
  ctx.report({
132
- message: fitted((cut) => `\`@throws\` lists \`${cut(listed)}\`, which \`${cut(name)}\` cannot throw`),
122
+ message: fitted((cut) => `\`@throws\` of \`${cut(name)}\` lists \`${cut(listed)}\`, which it cannot throw`),
133
123
  hint: `remove \`${listed}\` from the tag, or throw it where it can happen`,
134
124
  location,
135
125
  findingKind: "extra",
@@ -160,7 +150,7 @@ function check(entry, analysis, ctx) {
160
150
  const names = missing.map((one) => className(one.id, classes)).sort();
161
151
  const tagNames = [...new Set(missing.map((one) => catchName(one.id, classes)))].sort();
162
152
  ctx.report({
163
- message: fitted((cut) => `\`${cut(name)}\` can throw ${codeList(names, cut)}, which its \`@throws\` does not list`),
153
+ message: fitted((cut) => `\`@throws\` of \`${cut(name)}\` does not list ${codeList(names, cut)}`),
164
154
  hint: `add ${missing.length === 1 ? "it" : "them"} to the tag: \`@throws {${tagNames.join(" | ")}}\``,
165
155
  location: locationOf(entry.file, entry.fact.at),
166
156
  related: missing.flatMap((one) => (one.related === undefined ? [] : [one.related])).slice(0, 3),
@@ -170,7 +160,8 @@ function check(entry, analysis, ctx) {
170
160
  export const throwsDocMismatch = defineRule({
171
161
  kind: "project",
172
162
  meta: {
173
- description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.",
163
+ description: "Callers decide what to catch from `@throws`: a class it omits reaches them unhandled, and a class listed in excess asks them to handle a case that cannot happen.",
164
+ summary: "a `@throws` tag that lists an error the function cannot throw, or misses one it can",
174
165
  defaultSeverity: "error",
175
166
  tier: "core",
176
167
  group: GROUP,