@stateward/plugin-errors 0.0.0-stage → 1.0.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 (85) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -2
  3. package/dist/async-handler-unhandled.d.ts +7 -0
  4. package/dist/async-handler-unhandled.js +342 -0
  5. package/dist/catch-hides-unexpected.d.ts +6 -0
  6. package/dist/catch-hides-unexpected.js +260 -0
  7. package/dist/catch-misses-error.d.ts +6 -0
  8. package/dist/catch-misses-error.js +208 -0
  9. package/dist/catch-without-intent.d.ts +10 -0
  10. package/dist/catch-without-intent.js +590 -0
  11. package/dist/exhaustive-catch.d.ts +6 -0
  12. package/dist/exhaustive-catch.js +190 -0
  13. package/dist/exhaustive-fallback.d.ts +6 -0
  14. package/dist/exhaustive-fallback.js +157 -0
  15. package/dist/finally-can-throw.d.ts +6 -0
  16. package/dist/finally-can-throw.js +184 -0
  17. package/dist/finally-may-throw.d.ts +6 -0
  18. package/dist/finally-may-throw.js +184 -0
  19. package/dist/hover.d.ts +11 -0
  20. package/dist/hover.js +62 -0
  21. package/dist/index.d.ts +7 -0
  22. package/dist/index.js +2 -0
  23. package/dist/inference/builtins.d.ts +52 -0
  24. package/dist/inference/builtins.js +179 -0
  25. package/dist/inference/catches.d.ts +9 -0
  26. package/dist/inference/catches.js +33 -0
  27. package/dist/inference/deps.d.ts +12 -0
  28. package/dist/inference/deps.js +79 -0
  29. package/dist/inference/format.d.ts +25 -0
  30. package/dist/inference/format.js +72 -0
  31. package/dist/inference/packages.d.ts +40 -0
  32. package/dist/inference/packages.js +723 -0
  33. package/dist/inference/project.d.ts +8 -0
  34. package/dist/inference/project.js +22 -0
  35. package/dist/inference/solve.d.ts +128 -0
  36. package/dist/inference/solve.js +575 -0
  37. package/dist/inference/summarize.d.ts +50 -0
  38. package/dist/inference/summarize.js +1324 -0
  39. package/dist/inference/summary.d.ts +191 -0
  40. package/dist/inference/summary.js +237 -0
  41. package/dist/inlay.d.ts +19 -0
  42. package/dist/inlay.js +168 -0
  43. package/dist/must-handle-error.d.ts +55 -0
  44. package/dist/must-handle-error.js +794 -0
  45. package/dist/narrow-try.d.ts +52 -0
  46. package/dist/narrow-try.js +641 -0
  47. package/dist/no-swallow.d.ts +8 -0
  48. package/dist/no-swallow.js +320 -0
  49. package/dist/plugin.d.ts +116 -0
  50. package/dist/plugin.js +39 -0
  51. package/dist/react/boundaries.d.ts +83 -0
  52. package/dist/react/boundaries.js +371 -0
  53. package/dist/react/fallbacks.d.ts +12 -0
  54. package/dist/react/fallbacks.js +83 -0
  55. package/dist/react/render-guards.d.ts +24 -0
  56. package/dist/react/render-guards.js +271 -0
  57. package/dist/react/tree.d.ts +98 -0
  58. package/dist/react/tree.js +863 -0
  59. package/dist/refactors.d.ts +15 -0
  60. package/dist/refactors.js +69 -0
  61. package/dist/render-throw-without-boundary.d.ts +11 -0
  62. package/dist/render-throw-without-boundary.js +223 -0
  63. package/dist/render-throws-without-boundary.d.ts +6 -0
  64. package/dist/render-throws-without-boundary.js +151 -0
  65. package/dist/result-ignored.d.ts +6 -0
  66. package/dist/result-ignored.js +167 -0
  67. package/dist/result-type.d.ts +18 -0
  68. package/dist/result-type.js +55 -0
  69. package/dist/result-unchecked.d.ts +2 -0
  70. package/dist/result-unchecked.js +746 -0
  71. package/dist/rethrow-unknown.d.ts +6 -0
  72. package/dist/rethrow-unknown.js +260 -0
  73. package/dist/rules.meta.d.ts +214 -0
  74. package/dist/rules.meta.js +212 -0
  75. package/dist/syntax.d.ts +41 -0
  76. package/dist/syntax.js +171 -0
  77. package/dist/text.d.ts +22 -0
  78. package/dist/text.js +43 -0
  79. package/dist/throw-in-hook.d.ts +8 -0
  80. package/dist/throw-in-hook.js +510 -0
  81. package/dist/throws-doc-mismatch.d.ts +7 -0
  82. package/dist/throws-doc-mismatch.js +188 -0
  83. package/dist/throws-matches-doc.d.ts +7 -0
  84. package/dist/throws-matches-doc.js +188 -0
  85. package/package.json +34 -3
@@ -0,0 +1,212 @@
1
+ export const RULES = {
2
+ "catch-hides-unexpected": {
3
+ kind: "file",
4
+ meta: {
5
+ description: "A catch that handles some errors with instanceof also rethrows or handles all the others.",
6
+ defaultSeverity: "error",
7
+ tier: "core",
8
+ group: "catch",
9
+ options: {
10
+ restUsesError: {
11
+ kind: "boolean",
12
+ default: false,
13
+ description: "The branch for the other errors must use the error (rethrow it, wrap it, report it); a generic message alone does not count.",
14
+ },
15
+ },
16
+ },
17
+ },
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
+ "finally-can-throw": {
57
+ kind: "file",
58
+ meta: {
59
+ description: "A finally block does not call something that throws, so the error that led to it is not lost.",
60
+ defaultSeverity: "error",
61
+ tier: "core",
62
+ group: "catch",
63
+ options: {
64
+ severities: {
65
+ kind: "severities",
66
+ default: {},
67
+ kinds: {
68
+ throw: "a `throw` written in the finally: it throws whenever it runs.",
69
+ builtin: "a built-in such as `JSON.parse` or `new URL`: it throws only on input it cannot handle.",
70
+ "own-function": "a function in the same file with a `throw` in it: it throws on some paths.",
71
+ },
72
+ description: "A severity per kind of call, where you trust one kind less.",
73
+ },
74
+ },
75
+ },
76
+ },
77
+ "catch-misses-error": {
78
+ kind: "project",
79
+ 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.",
81
+ defaultSeverity: "error",
82
+ tier: "core",
83
+ group: "catch",
84
+ needsJsDoc: true,
85
+ options: {
86
+ ignoreClasses: {
87
+ kind: "strings",
88
+ default: [],
89
+ description: "Error classes a catch never needs a branch for, as its instanceof test names them (SyntaxError, *AbortError).",
90
+ },
91
+ },
92
+ },
93
+ },
94
+ "throws-doc-mismatch": {
95
+ kind: "project",
96
+ meta: {
97
+ description: "A function's @throws tag lists exactly the error classes it can throw, as far as stateward can see.",
98
+ defaultSeverity: "error",
99
+ tier: "core",
100
+ group: "callers",
101
+ needsJsDoc: true,
102
+ options: {
103
+ severities: {
104
+ kind: "severities",
105
+ default: {},
106
+ kinds: {
107
+ missing: "the function can throw a class its `@throws` does not list.",
108
+ extra: "the `@throws` lists a class the function cannot throw (stale, not dangerous).",
109
+ },
110
+ description: "A severity per kind of mismatch.",
111
+ },
112
+ ignoreClasses: {
113
+ kind: "strings",
114
+ default: [],
115
+ description: "Error classes a `@throws` never needs to list (TypeError, *AbortError), as a catch names them.",
116
+ },
117
+ },
118
+ },
119
+ },
120
+ "render-throw-without-boundary": {
121
+ kind: "project",
122
+ meta: {
123
+ description: "A component that can throw a known error while rendering has an error boundary above it.",
124
+ defaultSeverity: "warning",
125
+ tier: "core",
126
+ group: "render",
127
+ needsJsDoc: true,
128
+ options: {
129
+ skipContextGuards: {
130
+ kind: "boolean",
131
+ default: true,
132
+ description: 'Leave out the usual check in a context hook: `if (!ctx) throw new Error("useAuth must be used within AuthProvider")`.',
133
+ },
134
+ skipInputGuards: {
135
+ kind: "boolean",
136
+ default: true,
137
+ description: "Leave out a throw when a prop or a parameter whose type rules it out, or a context hook's value, is missing (a mistake of the caller).",
138
+ },
139
+ rootBoundaryCounts: {
140
+ kind: "boolean",
141
+ default: false,
142
+ description: "Count the error boundary around the whole app as handling the error. Off by default: the whole app is then replaced by its fallback, so the rule asks for a boundary nearer the page.",
143
+ },
144
+ boundaries: {
145
+ kind: "record",
146
+ default: { "react-error-boundary": ["ErrorBoundary"] },
147
+ description: 'Error boundary components from packages, by package: add Sentry\'s with `{ "@sentry/react": ["ErrorBoundary"] }` (keep react-error-boundary\'s to keep it).',
148
+ },
149
+ },
150
+ },
151
+ },
152
+ "must-handle-error": {
153
+ kind: "project",
154
+ 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.",
156
+ defaultSeverity: "error",
157
+ tier: "core",
158
+ group: "callers",
159
+ needsJsDoc: true,
160
+ listUnknowns: true,
161
+ },
162
+ },
163
+ "narrow-try": {
164
+ kind: "project",
165
+ meta: {
166
+ description: "Calls to your own code placed in a `try` after the call it is for: move them after the `try`.",
167
+ defaultSeverity: "warning",
168
+ tier: "addon",
169
+ addon: "narrow-try",
170
+ group: "catch",
171
+ needsJsDoc: true,
172
+ },
173
+ },
174
+ "result-ignored": {
175
+ kind: "file",
176
+ meta: {
177
+ description: "The Result of a call is checked or passed on, not dropped as a statement, after void or after await.",
178
+ defaultSeverity: "error",
179
+ tier: "core",
180
+ group: "callers",
181
+ yieldsTo: ["result-unchecked"],
182
+ options: {
183
+ allowVoid: {
184
+ kind: "boolean",
185
+ default: false,
186
+ description: "Take `void save()` as dropping the Result on purpose, and stay quiet.",
187
+ },
188
+ resultTypes: {
189
+ kind: "strings",
190
+ default: [],
191
+ description: "Your names for a Result type whose shape stateward does not recognise (`Either`, `Outcome*`; globs allowed).",
192
+ },
193
+ ignore: {
194
+ kind: "strings",
195
+ default: [],
196
+ description: "Functions whose Result may be dropped, by the name they are called with (`logEvent`, `*.track`; globs allowed).",
197
+ },
198
+ },
199
+ },
200
+ },
201
+ "result-unchecked": {
202
+ kind: "file",
203
+ 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.",
205
+ defaultSeverity: "warning",
206
+ tier: "core",
207
+ group: "callers",
208
+ heuristic: true,
209
+ },
210
+ },
211
+ };
212
+ //# sourceMappingURL=rules.meta.js.map
@@ -0,0 +1,41 @@
1
+ import ts from "@stateward/core/typescript";
2
+ import type { FileContext } from "@stateward/core";
3
+ export declare function stripParentheses(expression: ts.Expression): ts.Expression;
4
+ /** A function, method or accessor: code in it runs when it is called, not where it is written. */
5
+ export declare function isFunctionBoundary(node: ts.Node): boolean;
6
+ /**
7
+ * Visits `root` and what runs with it, in source order, without entering a nested function or
8
+ * class. `guarded` is true inside the `try` block of a `try` that has a `catch`: an error there is
9
+ * caught. The `catch` and `finally` blocks of that `try` are not guarded by it.
10
+ */
11
+ export declare function scanRunning(root: ts.Node, visit: (node: ts.Node, guarded: boolean) => void): void;
12
+ export declare function hasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean;
13
+ /** A function that is declared here with a body: a declaration, a method, or one bound to a name. */
14
+ export type DeclaredFunction = ts.FunctionDeclaration | ts.MethodDeclaration | ts.ArrowFunction | ts.FunctionExpression;
15
+ /**
16
+ * The function that `callee` (`save`, `this.save`, `service.save`) names, when it is declared in
17
+ * this file and has a body. Resolved by declaration, so a shadowing name or an import is not
18
+ * mistaken for it.
19
+ */
20
+ export declare function sameFileFunction(callee: ts.Expression, ctx: FileContext): DeclaredFunction | undefined;
21
+ /**
22
+ * Whether `name` is the language's own global (`JSON`, `URL`, `fetch`): it resolves to a
23
+ * declaration outside the project, so a local variable of the same name is not taken for it.
24
+ */
25
+ export declare function isBuiltinGlobal(name: ts.Identifier, ctx: FileContext): boolean;
26
+ /** Whether the function has a `throw` that no `try` / `catch` of its own catches. */
27
+ export declare function throwsUncaught(fn: DeclaredFunction): boolean;
28
+ /**
29
+ * Whether a `catch` tests what it caught before handling it: `e instanceof X`, `e.name === …`,
30
+ * `"status" in e`, or a type guard `isApiError(e)`, in an `if`, a `switch` or a `?:`. Such a catch
31
+ * says which errors it expects; `errors/catch-hides-unexpected` checks what it does with the rest.
32
+ */
33
+ export declare function testsCaught(clause: ts.CatchClause): boolean;
34
+ /**
35
+ * Whether `call`, in `clause`, hands the caught error to a callback the function around it was
36
+ * given (`reject(e)`, `next(e)`, `onError(e)`): the caller gets it, as with a rethrow.
37
+ */
38
+ export declare function handsOnCaught(call: ts.CallExpression, clause: ts.CatchClause): boolean;
39
+ /** Whether `test` holds for `root` or a node under it, without entering nested functions or classes. */
40
+ export declare function someRunning(root: ts.Node, test: (node: ts.Node) => boolean): boolean;
41
+ //# sourceMappingURL=syntax.d.ts.map
package/dist/syntax.js ADDED
@@ -0,0 +1,171 @@
1
+ import ts from "@stateward/core/typescript";
2
+ export function stripParentheses(expression) {
3
+ let node = expression;
4
+ while (ts.isParenthesizedExpression(node))
5
+ node = node.expression;
6
+ return node;
7
+ }
8
+ /** A function, method or accessor: code in it runs when it is called, not where it is written. */
9
+ export function isFunctionBoundary(node) {
10
+ return ts.isFunctionLike(node) || ts.isClassDeclaration(node) || ts.isClassExpression(node);
11
+ }
12
+ /**
13
+ * Visits `root` and what runs with it, in source order, without entering a nested function or
14
+ * class. `guarded` is true inside the `try` block of a `try` that has a `catch`: an error there is
15
+ * caught. The `catch` and `finally` blocks of that `try` are not guarded by it.
16
+ */
17
+ export function scanRunning(root, visit) {
18
+ const walk = (node, guarded) => {
19
+ if (isFunctionBoundary(node))
20
+ return;
21
+ visit(node, guarded);
22
+ if (ts.isTryStatement(node)) {
23
+ walk(node.tryBlock, guarded || node.catchClause !== undefined);
24
+ if (node.catchClause !== undefined)
25
+ walk(node.catchClause, guarded);
26
+ if (node.finallyBlock !== undefined)
27
+ walk(node.finallyBlock, guarded);
28
+ return;
29
+ }
30
+ ts.forEachChild(node, (child) => {
31
+ walk(child, guarded);
32
+ });
33
+ };
34
+ walk(root, false);
35
+ }
36
+ export function hasModifier(node, kind) {
37
+ return ts.canHaveModifiers(node) && ts.getModifiers(node)?.some((m) => m.kind === kind) === true;
38
+ }
39
+ function functionOfDeclaration(node) {
40
+ if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
41
+ return node.body === undefined ? undefined : node;
42
+ }
43
+ if (ts.isVariableDeclaration(node) ||
44
+ ts.isPropertyDeclaration(node) ||
45
+ ts.isPropertyAssignment(node)) {
46
+ const initializer = node.initializer === undefined ? undefined : stripParentheses(node.initializer);
47
+ if (initializer !== undefined &&
48
+ (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
49
+ return initializer;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+ /**
55
+ * The function that `callee` (`save`, `this.save`, `service.save`) names, when it is declared in
56
+ * this file and has a body. Resolved by declaration, so a shadowing name or an import is not
57
+ * mistaken for it.
58
+ */
59
+ export function sameFileFunction(callee, ctx) {
60
+ const target = stripParentheses(callee);
61
+ const name = ts.isIdentifier(target)
62
+ ? target
63
+ : ts.isPropertyAccessExpression(target)
64
+ ? target.name
65
+ : undefined;
66
+ if (name === undefined || ts.isPrivateIdentifier(name))
67
+ return undefined;
68
+ const symbol = ctx.types.symbolAt(name);
69
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol);
70
+ if (declaration?.node.getSourceFile() !== ctx.sourceFile)
71
+ return undefined;
72
+ return functionOfDeclaration(declaration.node);
73
+ }
74
+ /**
75
+ * Whether `name` is the language's own global (`JSON`, `URL`, `fetch`): it resolves to a
76
+ * declaration outside the project, so a local variable of the same name is not taken for it.
77
+ */
78
+ export function isBuiltinGlobal(name, ctx) {
79
+ const symbol = ctx.types.symbolAt(name);
80
+ const declaration = symbol === undefined ? undefined : ctx.types.declarationOf(symbol);
81
+ return declaration !== undefined && !ctx.types.isOwnFile(declaration.file);
82
+ }
83
+ /** Whether the function has a `throw` that no `try` / `catch` of its own catches. */
84
+ export function throwsUncaught(fn) {
85
+ let thrown = false;
86
+ scanRunning(fn.body ?? fn, (node, guarded) => {
87
+ if (ts.isThrowStatement(node) && !guarded)
88
+ thrown = true;
89
+ });
90
+ return thrown;
91
+ }
92
+ /**
93
+ * Whether a `catch` tests what it caught before handling it: `e instanceof X`, `e.name === …`,
94
+ * `"status" in e`, or a type guard `isApiError(e)`, in an `if`, a `switch` or a `?:`. Such a catch
95
+ * says which errors it expects; `errors/catch-hides-unexpected` checks what it does with the rest.
96
+ */
97
+ export function testsCaught(clause) {
98
+ const declaration = clause.variableDeclaration;
99
+ if (declaration === undefined || !ts.isIdentifier(declaration.name))
100
+ return false;
101
+ const variable = declaration.name.text;
102
+ const isVariable = (node) => {
103
+ const inner = stripParentheses(node);
104
+ return ts.isIdentifier(inner) && inner.text === variable;
105
+ };
106
+ const tests = (condition) => {
107
+ if (ts.isBinaryExpression(condition)) {
108
+ const operator = condition.operatorToken.kind;
109
+ if (operator === ts.SyntaxKind.InstanceOfKeyword && isVariable(condition.left))
110
+ return true;
111
+ if (operator === ts.SyntaxKind.InKeyword && isVariable(condition.right))
112
+ return true;
113
+ }
114
+ if (ts.isPropertyAccessExpression(condition) && isVariable(condition.expression))
115
+ return true;
116
+ if (ts.isCallExpression(condition) && condition.arguments.some(isVariable))
117
+ return true;
118
+ return ts.forEachChild(condition, (child) => (tests(child) ? true : undefined)) === true;
119
+ };
120
+ let found = false;
121
+ const walk = (node) => {
122
+ if (found || isFunctionBoundary(node))
123
+ return;
124
+ if (ts.isIfStatement(node) && tests(node.expression))
125
+ found = true;
126
+ if (ts.isConditionalExpression(node) && tests(node.condition))
127
+ found = true;
128
+ if (ts.isSwitchStatement(node)) {
129
+ found ||=
130
+ tests(node.expression) ||
131
+ node.caseBlock.clauses.some((one) => ts.isCaseClause(one) && tests(one.expression));
132
+ }
133
+ ts.forEachChild(node, walk);
134
+ };
135
+ walk(clause.block);
136
+ return found;
137
+ }
138
+ /** A parameter of a function around `node` with this name: `reject`, `next`, `onError`. */
139
+ function isParameterOfEnclosing(name, node) {
140
+ for (let parent = node.parent; !ts.isSourceFile(parent); parent = parent.parent) {
141
+ if (ts.isFunctionLike(parent) &&
142
+ parent.parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === name)) {
143
+ return true;
144
+ }
145
+ }
146
+ return false;
147
+ }
148
+ /**
149
+ * Whether `call`, in `clause`, hands the caught error to a callback the function around it was
150
+ * given (`reject(e)`, `next(e)`, `onError(e)`): the caller gets it, as with a rethrow.
151
+ */
152
+ export function handsOnCaught(call, clause) {
153
+ const declaration = clause.variableDeclaration;
154
+ if (declaration === undefined || !ts.isIdentifier(declaration.name))
155
+ return false;
156
+ const variable = declaration.name.text;
157
+ const callee = stripParentheses(call.expression);
158
+ return (ts.isIdentifier(callee) &&
159
+ isParameterOfEnclosing(callee.text, clause) &&
160
+ call.arguments.some((argument) => {
161
+ const inner = stripParentheses(argument);
162
+ return ts.isIdentifier(inner) && inner.text === variable;
163
+ }));
164
+ }
165
+ /** Whether `test` holds for `root` or a node under it, without entering nested functions or classes. */
166
+ export function someRunning(root, test) {
167
+ const walk = (node) => (node === root || !isFunctionBoundary(node)) &&
168
+ (test(node) || ts.forEachChild(node, (child) => (walk(child) ? true : undefined)) === true);
169
+ return walk(root);
170
+ }
171
+ //# sourceMappingURL=syntax.js.map
package/dist/text.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /** A headline is at most this long. */
2
+ export declare const HEADLINE_MAX = 100;
3
+ /** The first line of a fix is at most this long. */
4
+ export declare const HINT_MAX = 140;
5
+ /** The text on one line: all whitespace becomes a single space. */
6
+ export declare function oneLine(text: string): string;
7
+ /** The text as it is, or its first `max - 1` characters and `…` when it is longer than `max`. */
8
+ export declare function shorten(text: string, max: number): string;
9
+ /**
10
+ * Builds a message whose inserted names are shown whole, on one line: a developer's name is never
11
+ * cut. A headline that is longer than the limit because of its names is allowed to be.
12
+ * `build` applies the given `cut` to every name it inserts.
13
+ */
14
+ export declare function fitted(build: (cut: (text: string) => string) => string): string;
15
+ /**
16
+ * `` `A` ``, `` `A` and `B` ``, `` `A`, `B` and `C` ``, or the first three and `and N more`. Each
17
+ * name is passed through `cut` and put in backticks.
18
+ */
19
+ export declare function codeList(names: readonly string[], cut: (text: string) => string): string;
20
+ /** A fix line followed by a code example, each example line indented by 4 spaces. */
21
+ export declare function withExample(line: string, example: readonly string[]): string;
22
+ //# sourceMappingURL=text.d.ts.map
package/dist/text.js ADDED
@@ -0,0 +1,43 @@
1
+ /** A headline is at most this long. */
2
+ export const HEADLINE_MAX = 100;
3
+ /** The first line of a fix is at most this long. */
4
+ export const HINT_MAX = 140;
5
+ /** The text on one line: all whitespace becomes a single space. */
6
+ export function oneLine(text) {
7
+ return text.replace(/\s+/g, " ").trim();
8
+ }
9
+ /** The text as it is, or its first `max - 1` characters and `…` when it is longer than `max`. */
10
+ export function shorten(text, max) {
11
+ if (text.length <= max)
12
+ return text;
13
+ return `${text.slice(0, max - 1)}…`;
14
+ }
15
+ /**
16
+ * Builds a message whose inserted names are shown whole, on one line: a developer's name is never
17
+ * cut. A headline that is longer than the limit because of its names is allowed to be.
18
+ * `build` applies the given `cut` to every name it inserts.
19
+ */
20
+ export function fitted(build) {
21
+ return build(oneLine);
22
+ }
23
+ /** The names that a list shows, at most this many; the rest are counted. */
24
+ const MAX_LISTED = 3;
25
+ /**
26
+ * `` `A` ``, `` `A` and `B` ``, `` `A`, `B` and `C` ``, or the first three and `and N more`. Each
27
+ * name is passed through `cut` and put in backticks.
28
+ */
29
+ export function codeList(names, cut) {
30
+ const shown = names.slice(0, MAX_LISTED).map((name) => `\`${cut(name)}\``);
31
+ const hidden = names.length - shown.length;
32
+ if (hidden > 0)
33
+ return `${shown.join(", ")} and ${String(hidden)} more`;
34
+ const last = shown.at(-1);
35
+ if (last === undefined || shown.length === 1)
36
+ return last ?? "";
37
+ return `${shown.slice(0, -1).join(", ")} and ${last}`;
38
+ }
39
+ /** A fix line followed by a code example, each example line indented by 4 spaces. */
40
+ export function withExample(line, example) {
41
+ return [line, ...example.map((text) => ` ${text}`)].join("\n");
42
+ }
43
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1,8 @@
1
+ /** The group the rule belongs to in the docs and in `stateward status`. */
2
+ export declare const GROUP = "react";
3
+ export declare const throwInHook: import("@stateward/core").FileRule<{
4
+ skipContextGuards: import("@stateward/core").BooleanOption;
5
+ skipInputGuards: import("@stateward/core").BooleanOption;
6
+ ignore: import("@stateward/core").StringsOption;
7
+ }, "react">;
8
+ //# sourceMappingURL=throw-in-hook.d.ts.map