@stateward/plugin-errors 0.0.0-stage → 1.0.1-rc.1
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/LICENSE +21 -0
- package/README.md +20 -2
- package/dist/async-handler-unhandled.d.ts +7 -0
- package/dist/async-handler-unhandled.js +342 -0
- package/dist/catch-hides-unexpected.d.ts +6 -0
- package/dist/catch-hides-unexpected.js +260 -0
- package/dist/catch-misses-error.d.ts +6 -0
- package/dist/catch-misses-error.js +208 -0
- package/dist/catch-without-intent.d.ts +10 -0
- package/dist/catch-without-intent.js +590 -0
- package/dist/exhaustive-catch.d.ts +6 -0
- package/dist/exhaustive-catch.js +190 -0
- package/dist/exhaustive-fallback.d.ts +6 -0
- package/dist/exhaustive-fallback.js +157 -0
- package/dist/finally-can-throw.d.ts +6 -0
- package/dist/finally-can-throw.js +184 -0
- package/dist/finally-may-throw.d.ts +6 -0
- package/dist/finally-may-throw.js +184 -0
- package/dist/hover.d.ts +11 -0
- package/dist/hover.js +62 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +2 -0
- package/dist/inference/builtins.d.ts +52 -0
- package/dist/inference/builtins.js +179 -0
- package/dist/inference/catches.d.ts +9 -0
- package/dist/inference/catches.js +33 -0
- package/dist/inference/deps.d.ts +12 -0
- package/dist/inference/deps.js +79 -0
- package/dist/inference/format.d.ts +25 -0
- package/dist/inference/format.js +72 -0
- package/dist/inference/packages.d.ts +40 -0
- package/dist/inference/packages.js +723 -0
- package/dist/inference/project.d.ts +8 -0
- package/dist/inference/project.js +22 -0
- package/dist/inference/solve.d.ts +128 -0
- package/dist/inference/solve.js +575 -0
- package/dist/inference/summarize.d.ts +50 -0
- package/dist/inference/summarize.js +1324 -0
- package/dist/inference/summary.d.ts +191 -0
- package/dist/inference/summary.js +237 -0
- package/dist/inlay.d.ts +19 -0
- package/dist/inlay.js +168 -0
- package/dist/must-handle-error.d.ts +55 -0
- package/dist/must-handle-error.js +794 -0
- package/dist/narrow-try.d.ts +52 -0
- package/dist/narrow-try.js +641 -0
- package/dist/no-swallow.d.ts +8 -0
- package/dist/no-swallow.js +320 -0
- package/dist/plugin.d.ts +116 -0
- package/dist/plugin.js +39 -0
- package/dist/react/boundaries.d.ts +83 -0
- package/dist/react/boundaries.js +371 -0
- package/dist/react/fallbacks.d.ts +12 -0
- package/dist/react/fallbacks.js +83 -0
- package/dist/react/render-guards.d.ts +24 -0
- package/dist/react/render-guards.js +271 -0
- package/dist/react/tree.d.ts +98 -0
- package/dist/react/tree.js +863 -0
- package/dist/refactors.d.ts +15 -0
- package/dist/refactors.js +69 -0
- package/dist/render-throw-without-boundary.d.ts +11 -0
- package/dist/render-throw-without-boundary.js +223 -0
- package/dist/render-throws-without-boundary.d.ts +6 -0
- package/dist/render-throws-without-boundary.js +151 -0
- package/dist/result-ignored.d.ts +6 -0
- package/dist/result-ignored.js +167 -0
- package/dist/result-type.d.ts +18 -0
- package/dist/result-type.js +55 -0
- package/dist/result-unchecked.d.ts +2 -0
- package/dist/result-unchecked.js +746 -0
- package/dist/rethrow-unknown.d.ts +6 -0
- package/dist/rethrow-unknown.js +260 -0
- package/dist/rules.meta.d.ts +214 -0
- package/dist/rules.meta.js +212 -0
- package/dist/syntax.d.ts +41 -0
- package/dist/syntax.js +171 -0
- package/dist/text.d.ts +22 -0
- package/dist/text.js +43 -0
- package/dist/throw-in-hook.d.ts +8 -0
- package/dist/throw-in-hook.js +510 -0
- package/dist/throws-doc-mismatch.d.ts +7 -0
- package/dist/throws-doc-mismatch.js +188 -0
- package/dist/throws-matches-doc.d.ts +7 -0
- package/dist/throws-matches-doc.js +188 -0
- package/package.json +34 -3
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { defineRule, matchesName, option } from "@stateward/core";
|
|
2
|
+
import { catchName, className } from "./inference/format.js";
|
|
3
|
+
import { analyse, collected, evaluated, relation } from "./inference/solve.js";
|
|
4
|
+
import { summarizeOnce } from "./inference/project.js";
|
|
5
|
+
import { codeList, fitted } from "./text.js";
|
|
6
|
+
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
7
|
+
export const GROUP = "callers";
|
|
8
|
+
const EXPLAIN = `# errors/throws-doc-mismatch
|
|
9
|
+
|
|
10
|
+
A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
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
|
+
/** @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
|
|
36
|
+
}
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
### Good
|
|
40
|
+
|
|
41
|
+
\`\`\`ts
|
|
42
|
+
/** @throws {NotFoundError | SyntaxError} */
|
|
43
|
+
export async function loadUser(id: string): Promise<User> {
|
|
44
|
+
const text = await readUser(id);
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### Fix
|
|
50
|
+
|
|
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.
|
|
52
|
+
|
|
53
|
+
## What it does not catch
|
|
54
|
+
|
|
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\`.
|
|
61
|
+
|
|
62
|
+
## Turn it on or off
|
|
63
|
+
|
|
64
|
+
In \`stateward.config.ts\`:
|
|
65
|
+
|
|
66
|
+
\`\`\`ts
|
|
67
|
+
rules: { "errors/throws-doc-mismatch": "off" } // or "warning"
|
|
68
|
+
groups: { "errors/inference": "off" } // the whole group
|
|
69
|
+
rules: { "errors/throws-doc-mismatch": ["error", { severities: { extra: "warning" } }] } // a stale tag only warns
|
|
70
|
+
\`\`\`
|
|
71
|
+
`;
|
|
72
|
+
function locationOf(file, at) {
|
|
73
|
+
return { file, start: at[0], end: at[1] };
|
|
74
|
+
}
|
|
75
|
+
const OPTIONS = {
|
|
76
|
+
severities: option.severities({
|
|
77
|
+
kinds: {
|
|
78
|
+
missing: "the function can throw a class its `@throws` does not list.",
|
|
79
|
+
extra: "the `@throws` lists a class the function cannot throw (stale, not dangerous).",
|
|
80
|
+
},
|
|
81
|
+
description: "A severity per kind of mismatch.",
|
|
82
|
+
}),
|
|
83
|
+
ignoreClasses: option.strings({
|
|
84
|
+
default: [],
|
|
85
|
+
description: "Error classes a `@throws` never needs to list (TypeError, *AbortError), as a catch names them.",
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
function check(entry, analysis, ctx) {
|
|
89
|
+
const doc = entry.fact.doc;
|
|
90
|
+
if (doc === null)
|
|
91
|
+
return;
|
|
92
|
+
const { classes } = analysis;
|
|
93
|
+
const result = evaluated(entry, analysis).body;
|
|
94
|
+
const all = collected();
|
|
95
|
+
for (const part of [result.throws, result.rejects]) {
|
|
96
|
+
for (const [id, info] of part.classes) {
|
|
97
|
+
const known = all.classes.get(id);
|
|
98
|
+
if (known === undefined)
|
|
99
|
+
all.classes.set(id, { ...info });
|
|
100
|
+
else
|
|
101
|
+
known.certain ||= info.certain;
|
|
102
|
+
}
|
|
103
|
+
for (const [key, source] of part.unknowns)
|
|
104
|
+
all.unknowns.set(key, source);
|
|
105
|
+
}
|
|
106
|
+
const name = entry.fact.name;
|
|
107
|
+
const firstUnknown = [...all.unknowns.values()][0];
|
|
108
|
+
for (const item of doc.entries) {
|
|
109
|
+
const location = locationOf(entry.file, item.at);
|
|
110
|
+
if (item.id === null) {
|
|
111
|
+
ctx.unknown({
|
|
112
|
+
message: fitted((cut) => `stateward cannot find the class \`${cut(item.name)}\` this \`@throws\` names`),
|
|
113
|
+
hint: "name an error class that is in scope in this file, or import it",
|
|
114
|
+
location,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const id = item.id;
|
|
119
|
+
const possible = [...all.classes.keys()].some((thrown) => relation(id, thrown, classes) !== "no");
|
|
120
|
+
if (possible)
|
|
121
|
+
continue;
|
|
122
|
+
const listed = className(id, classes);
|
|
123
|
+
if (firstUnknown !== undefined) {
|
|
124
|
+
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`,
|
|
127
|
+
location,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
ctx.report({
|
|
132
|
+
message: fitted((cut) => `\`@throws\` lists \`${cut(listed)}\`, which \`${cut(name)}\` cannot throw`),
|
|
133
|
+
hint: `remove \`${listed}\` from the tag, or throw it where it can happen`,
|
|
134
|
+
location,
|
|
135
|
+
findingKind: "extra",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (doc.untyped.length > 0 || doc.entries.some((item) => item.id === null))
|
|
139
|
+
return;
|
|
140
|
+
const missing = [];
|
|
141
|
+
for (const [id, info] of all.classes) {
|
|
142
|
+
if (!info.certain || matchesName(catchName(id, classes), ctx.options.ignoreClasses))
|
|
143
|
+
continue;
|
|
144
|
+
const verdicts = doc.entries.map((item) => item.id === null ? "undecided" : relation(item.id, id, classes));
|
|
145
|
+
if (verdicts.includes("yes") || verdicts.includes("undecided"))
|
|
146
|
+
continue;
|
|
147
|
+
const via = info.via;
|
|
148
|
+
missing.push({
|
|
149
|
+
id,
|
|
150
|
+
related: via === null
|
|
151
|
+
? undefined
|
|
152
|
+
: {
|
|
153
|
+
location: { file: via.file, start: via.start, end: via.end },
|
|
154
|
+
message: `\`${via.text}\` can throw \`${className(id, classes)}\``,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (missing.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
const names = missing.map((one) => className(one.id, classes)).sort();
|
|
161
|
+
const tagNames = [...new Set(missing.map((one) => catchName(one.id, classes)))].sort();
|
|
162
|
+
ctx.report({
|
|
163
|
+
message: fitted((cut) => `\`${cut(name)}\` can throw ${codeList(names, cut)}, which its \`@throws\` does not list`),
|
|
164
|
+
hint: `add ${missing.length === 1 ? "it" : "them"} to the tag: \`@throws {${tagNames.join(" | ")}}\``,
|
|
165
|
+
location: locationOf(entry.file, entry.fact.at),
|
|
166
|
+
related: missing.flatMap((one) => (one.related === undefined ? [] : [one.related])).slice(0, 3),
|
|
167
|
+
findingKind: "missing",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export const throwsDocMismatch = defineRule({
|
|
171
|
+
kind: "project",
|
|
172
|
+
meta: {
|
|
173
|
+
description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.",
|
|
174
|
+
defaultSeverity: "error",
|
|
175
|
+
tier: "core",
|
|
176
|
+
group: GROUP,
|
|
177
|
+
needsJsDoc: true,
|
|
178
|
+
options: OPTIONS,
|
|
179
|
+
explain: EXPLAIN,
|
|
180
|
+
},
|
|
181
|
+
summarize: (ctx) => summarizeOnce(ctx),
|
|
182
|
+
check(summaries, ctx) {
|
|
183
|
+
const analysis = analyse(summaries);
|
|
184
|
+
for (const entry of analysis.functions.values())
|
|
185
|
+
check(entry, analysis, ctx);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=throws-doc-mismatch.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
2
|
+
export declare const GROUP = "inference";
|
|
3
|
+
export declare const throwsMatchesDoc: import("@stateward/core").ProjectRule<import("./index.js").ThrowsSummary, {
|
|
4
|
+
severities: import("@stateward/core").SeveritiesOption<"missing" | "extra">;
|
|
5
|
+
ignoreClasses: import("@stateward/core").StringsOption;
|
|
6
|
+
}, "inference">;
|
|
7
|
+
//# sourceMappingURL=throws-matches-doc.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { defineRule, matchesName, option } from "@stateward/core";
|
|
2
|
+
import { catchName, className } from "./inference/format.js";
|
|
3
|
+
import { analyse, collected, evaluated, relation } from "./inference/solve.js";
|
|
4
|
+
import { summarizeOnce } from "./inference/project.js";
|
|
5
|
+
import { HEADLINE_MAX, codeList, fitted } from "./text.js";
|
|
6
|
+
/** The group the rule belongs to in the docs and in `stateward status`. */
|
|
7
|
+
export const GROUP = "inference";
|
|
8
|
+
const EXPLAIN = `# errors/throws-matches-doc
|
|
9
|
+
|
|
10
|
+
A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
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
|
+
/** @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
|
|
36
|
+
}
|
|
37
|
+
\`\`\`
|
|
38
|
+
|
|
39
|
+
### Good
|
|
40
|
+
|
|
41
|
+
\`\`\`ts
|
|
42
|
+
/** @throws {NotFoundError | SyntaxError} */
|
|
43
|
+
export async function loadUser(id: string): Promise<User> {
|
|
44
|
+
const text = await readUser(id);
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
}
|
|
47
|
+
\`\`\`
|
|
48
|
+
|
|
49
|
+
### Fix
|
|
50
|
+
|
|
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.
|
|
52
|
+
|
|
53
|
+
## What it does not catch
|
|
54
|
+
|
|
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\`.
|
|
61
|
+
|
|
62
|
+
## Turn it on or off
|
|
63
|
+
|
|
64
|
+
In \`stateward.config.ts\`:
|
|
65
|
+
|
|
66
|
+
\`\`\`ts
|
|
67
|
+
rules: { "errors/throws-matches-doc": "off" } // or "warning"
|
|
68
|
+
groups: { "errors/inference": "off" } // the whole group
|
|
69
|
+
rules: { "errors/throws-matches-doc": ["error", { severities: { extra: "warning" } }] } // a stale tag only warns
|
|
70
|
+
\`\`\`
|
|
71
|
+
`;
|
|
72
|
+
function locationOf(file, at) {
|
|
73
|
+
return { file, start: at[0], end: at[1] };
|
|
74
|
+
}
|
|
75
|
+
const OPTIONS = {
|
|
76
|
+
severities: option.severities({
|
|
77
|
+
kinds: {
|
|
78
|
+
missing: "the function can throw a class its `@throws` does not list.",
|
|
79
|
+
extra: "the `@throws` lists a class the function cannot throw (stale, not dangerous).",
|
|
80
|
+
},
|
|
81
|
+
description: "A severity per kind of mismatch.",
|
|
82
|
+
}),
|
|
83
|
+
ignoreClasses: option.strings({
|
|
84
|
+
default: [],
|
|
85
|
+
description: "Error classes a `@throws` never needs to list (TypeError, *AbortError), as a catch names them.",
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
function check(entry, analysis, ctx) {
|
|
89
|
+
const doc = entry.fact.doc;
|
|
90
|
+
if (doc === null)
|
|
91
|
+
return;
|
|
92
|
+
const { classes } = analysis;
|
|
93
|
+
const result = evaluated(entry, analysis).body;
|
|
94
|
+
const all = collected();
|
|
95
|
+
for (const part of [result.throws, result.rejects]) {
|
|
96
|
+
for (const [id, info] of part.classes) {
|
|
97
|
+
const known = all.classes.get(id);
|
|
98
|
+
if (known === undefined)
|
|
99
|
+
all.classes.set(id, { ...info });
|
|
100
|
+
else
|
|
101
|
+
known.certain ||= info.certain;
|
|
102
|
+
}
|
|
103
|
+
for (const [key, source] of part.unknowns)
|
|
104
|
+
all.unknowns.set(key, source);
|
|
105
|
+
}
|
|
106
|
+
const name = entry.fact.name;
|
|
107
|
+
const firstUnknown = [...all.unknowns.values()][0];
|
|
108
|
+
for (const item of doc.entries) {
|
|
109
|
+
const location = locationOf(entry.file, item.at);
|
|
110
|
+
if (item.id === null) {
|
|
111
|
+
ctx.unknown({
|
|
112
|
+
message: fitted(HEADLINE_MAX, (cut) => `stateward cannot find the class \`${cut(item.name)}\` this \`@throws\` names`),
|
|
113
|
+
hint: "name an error class that is in scope in this file, or import it",
|
|
114
|
+
location,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const id = item.id;
|
|
119
|
+
const possible = [...all.classes.keys()].some((thrown) => relation(id, thrown, classes) !== "no");
|
|
120
|
+
if (possible)
|
|
121
|
+
continue;
|
|
122
|
+
const listed = className(id, classes);
|
|
123
|
+
if (firstUnknown !== undefined) {
|
|
124
|
+
ctx.unknown({
|
|
125
|
+
message: fitted(HEADLINE_MAX, (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`,
|
|
127
|
+
location,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
ctx.report({
|
|
132
|
+
message: fitted(HEADLINE_MAX, (cut) => `\`@throws\` lists \`${cut(listed)}\`, which \`${cut(name)}\` cannot throw`),
|
|
133
|
+
hint: `remove \`${listed}\` from the tag, or throw it where it can happen`,
|
|
134
|
+
location,
|
|
135
|
+
findingKind: "extra",
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (doc.untyped.length > 0 || doc.entries.some((item) => item.id === null))
|
|
139
|
+
return;
|
|
140
|
+
const missing = [];
|
|
141
|
+
for (const [id, info] of all.classes) {
|
|
142
|
+
if (!info.certain || matchesName(catchName(id, classes), ctx.options.ignoreClasses))
|
|
143
|
+
continue;
|
|
144
|
+
const verdicts = doc.entries.map((item) => item.id === null ? "undecided" : relation(item.id, id, classes));
|
|
145
|
+
if (verdicts.includes("yes") || verdicts.includes("undecided"))
|
|
146
|
+
continue;
|
|
147
|
+
const via = info.via;
|
|
148
|
+
missing.push({
|
|
149
|
+
id,
|
|
150
|
+
related: via === null
|
|
151
|
+
? undefined
|
|
152
|
+
: {
|
|
153
|
+
location: { file: via.file, start: via.start, end: via.end },
|
|
154
|
+
message: `\`${via.text}\` can throw \`${className(id, classes)}\``,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (missing.length === 0)
|
|
159
|
+
return;
|
|
160
|
+
const names = missing.map((one) => className(one.id, classes)).sort();
|
|
161
|
+
const tagNames = [...new Set(missing.map((one) => catchName(one.id, classes)))].sort();
|
|
162
|
+
ctx.report({
|
|
163
|
+
message: fitted(HEADLINE_MAX, (cut) => `\`${cut(name)}\` can throw ${codeList(names, cut)}, which its \`@throws\` does not list`),
|
|
164
|
+
hint: `add ${missing.length === 1 ? "it" : "them"} to the tag: \`@throws {${tagNames.join(" | ")}}\``,
|
|
165
|
+
location: locationOf(entry.file, entry.fact.at),
|
|
166
|
+
related: missing.flatMap((one) => (one.related === undefined ? [] : [one.related])).slice(0, 3),
|
|
167
|
+
findingKind: "missing",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
export const throwsMatchesDoc = defineRule({
|
|
171
|
+
kind: "project",
|
|
172
|
+
meta: {
|
|
173
|
+
description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.",
|
|
174
|
+
defaultSeverity: "error",
|
|
175
|
+
tier: "core",
|
|
176
|
+
group: GROUP,
|
|
177
|
+
needsJsDoc: true,
|
|
178
|
+
options: OPTIONS,
|
|
179
|
+
explain: EXPLAIN,
|
|
180
|
+
},
|
|
181
|
+
summarize: (ctx) => summarizeOnce(ctx),
|
|
182
|
+
check(summaries, ctx) {
|
|
183
|
+
const analysis = analyse(summaries);
|
|
184
|
+
for (const entry of analysis.functions.values())
|
|
185
|
+
check(entry, analysis, ctx);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
//# sourceMappingURL=throws-matches-doc.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stateward/plugin-errors",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.1-rc.1",
|
|
4
|
+
"description": "stateward rules for errors: errors are handled where they can happen.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/khoing1111/ts-stateward.git",
|
|
9
|
+
"directory": "packages/plugin-errors"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/khoing1111/ts-stateward/tree/main/packages/plugin-errors#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/khoing1111/ts-stateward/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"source": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"!dist/test",
|
|
31
|
+
"!dist/**/*.map",
|
|
32
|
+
"!dist/**/*.tsbuildinfo"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@stateward/core": "1.0.1-rc.1"
|
|
36
|
+
}
|
|
6
37
|
}
|