@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,863 @@
1
+ import ts from "@stateward/core/typescript";
2
+ import { classIdOf, packageNameOf } from "../inference/summarize.js";
3
+ import { each, fieldsOf, isString, readStrings, spanOf } from "../inference/summary.js";
4
+ import { scanRunning, stripParentheses } from "../syntax.js";
5
+ import { ALL_GUARDS, guardThrows } from "./render-guards.js";
6
+ export const FALLBACK_KINDS = [
7
+ "FallbackComponent",
8
+ "fallbackRender",
9
+ "onError",
10
+ "getDerivedStateFromError",
11
+ ];
12
+ const EFFECT_HOOKS = new Set([
13
+ "useEffect",
14
+ "useLayoutEffect",
15
+ "useInsertionEffect",
16
+ ]);
17
+ /** The class methods React runs while rendering or committing: their errors reach a boundary. */
18
+ const CLASS_METHODS = new Set([
19
+ "render",
20
+ "componentDidMount",
21
+ "componentDidUpdate",
22
+ "constructor",
23
+ ]);
24
+ /** The error boundaries from packages that are known without a config: package to components. */
25
+ export const LIBRARY_BOUNDARIES = { "react-error-boundary": ["ErrorBoundary"] };
26
+ function isHookName(name) {
27
+ return /^use[A-Z]/.test(name);
28
+ }
29
+ function isJsx(node) {
30
+ return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
31
+ }
32
+ /** Has JSX, or calls a hook, outside nested functions: the body of a component. */
33
+ function looksLikeComponent(body) {
34
+ let found = false;
35
+ scanRunning(body, (node) => {
36
+ if (isJsx(node))
37
+ found = true;
38
+ else if (ts.isCallExpression(node)) {
39
+ const callee = stripParentheses(node.expression);
40
+ if (ts.isIdentifier(callee) && isHookName(callee.text))
41
+ found = true;
42
+ }
43
+ });
44
+ return found;
45
+ }
46
+ function renderFunctionOf(node) {
47
+ if (ts.isClassDeclaration(node)) {
48
+ const extendsSomething = node.heritageClauses?.some((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
49
+ const renders = node.members.some((member) => ts.isMethodDeclaration(member) &&
50
+ ts.isIdentifier(member.name) &&
51
+ member.name.text === "render");
52
+ return node.name !== undefined && extendsSomething === true && renders
53
+ ? { node, name: node.name, kind: "class" }
54
+ : undefined;
55
+ }
56
+ let fn;
57
+ let name;
58
+ if (ts.isFunctionDeclaration(node)) {
59
+ fn = node;
60
+ name = node.name;
61
+ }
62
+ else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
63
+ const value = stripParentheses(node.initializer);
64
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) {
65
+ fn = value;
66
+ name = node.name;
67
+ }
68
+ }
69
+ if (fn === undefined || name === undefined || fn.body === undefined)
70
+ return undefined;
71
+ if (isHookName(name.text))
72
+ return { node: fn, name, kind: "hook" };
73
+ return /^[A-Z]/.test(name.text) && looksLikeComponent(fn.body)
74
+ ? { node: fn, name, kind: "component" }
75
+ : undefined;
76
+ }
77
+ /** The node of a tag that names what it renders: `Card` in `<UI.Card>`. */
78
+ function tagName(tag) {
79
+ if (ts.isIdentifier(tag))
80
+ return /^[a-z]/.test(tag.text) ? undefined : tag;
81
+ return ts.isPropertyAccessExpression(tag) ? tag.name : undefined;
82
+ }
83
+ function openingOf(element) {
84
+ return ts.isJsxElement(element) ? element.openingElement : element;
85
+ }
86
+ function isBoundaryClass(node) {
87
+ return ((ts.isClassDeclaration(node) || ts.isClassExpression(node)) &&
88
+ node.members.some((member) => member.name !== undefined &&
89
+ ts.isIdentifier(member.name) &&
90
+ (member.name.text === "getDerivedStateFromError" ||
91
+ member.name.text === "componentDidCatch")));
92
+ }
93
+ function classifyTag(types, tag, libraryBoundaries) {
94
+ const name = tagName(tag);
95
+ const symbol = name === undefined ? undefined : types.symbolAt(name);
96
+ const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
97
+ if (symbol === undefined || declaration === undefined) {
98
+ return { target: undefined, boundary: undefined };
99
+ }
100
+ if (!types.isOwnFile(declaration.file)) {
101
+ const library = packageNameOf(types, declaration.file);
102
+ const isLibraryBoundary = (libraryBoundaries[library] ?? []).includes(symbol.name);
103
+ return { target: undefined, boundary: isLibraryBoundary ? { own: null } : undefined };
104
+ }
105
+ return {
106
+ target: symbol.id,
107
+ boundary: isBoundaryClass(declaration.node) ? { own: declaration.node } : undefined,
108
+ };
109
+ }
110
+ function refersToError(expression, ref) {
111
+ const e = stripParentheses(expression);
112
+ if (ts.isIdentifier(e))
113
+ return ref.names.has(e.text);
114
+ return (ts.isPropertyAccessExpression(e) &&
115
+ e.name.text === "error" &&
116
+ ts.isIdentifier(e.expression) &&
117
+ ref.props.has(e.expression.text));
118
+ }
119
+ /** The names `{ error }` binds from a pattern. */
120
+ function errorBindings(pattern, into) {
121
+ if (!ts.isObjectBindingPattern(pattern))
122
+ return;
123
+ for (const element of pattern.elements) {
124
+ const property = element.propertyName ?? element.name;
125
+ if (ts.isIdentifier(property) && property.text === "error" && ts.isIdentifier(element.name)) {
126
+ into.add(element.name.text);
127
+ }
128
+ }
129
+ }
130
+ /** Where the error is in a fallback component's props: `{ error }`, `props.error`. */
131
+ function propsRef(fn) {
132
+ const ref = { names: new Set(), props: new Set() };
133
+ const parameter = fn.parameters[0];
134
+ if (parameter === undefined)
135
+ return ref;
136
+ if (ts.isIdentifier(parameter.name))
137
+ ref.props.add(parameter.name.text);
138
+ else
139
+ errorBindings(parameter.name, ref.names);
140
+ return ref;
141
+ }
142
+ /** The error is the first parameter (`onError`, `getDerivedStateFromError`). */
143
+ function parameterRef(fn) {
144
+ const parameter = fn.parameters[0];
145
+ const names = new Set();
146
+ if (parameter !== undefined && ts.isIdentifier(parameter.name))
147
+ names.add(parameter.name.text);
148
+ return { names, props: new Set() };
149
+ }
150
+ /** The classes `error instanceof A || error instanceof B` names; undefined for a test of something else. */
151
+ function instanceofTest(condition, ref) {
152
+ const e = stripParentheses(condition);
153
+ if (!ts.isBinaryExpression(e))
154
+ return undefined;
155
+ const operator = e.operatorToken.kind;
156
+ if (operator === ts.SyntaxKind.InstanceOfKeyword) {
157
+ return refersToError(e.left, ref)
158
+ ? { classes: [e.right], exact: true, variable: stripParentheses(e.left).getText() }
159
+ : undefined;
160
+ }
161
+ if (operator !== ts.SyntaxKind.BarBarToken &&
162
+ operator !== ts.SyntaxKind.AmpersandAmpersandToken) {
163
+ return undefined;
164
+ }
165
+ const left = instanceofTest(e.left, ref);
166
+ const right = instanceofTest(e.right, ref);
167
+ const found = left ?? right;
168
+ if (found === undefined)
169
+ return undefined;
170
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken)
171
+ return { ...found, exact: false };
172
+ // `a || other`: what `other` lets through is not known.
173
+ if (left === undefined || right === undefined)
174
+ return { ...found, exact: false };
175
+ return {
176
+ classes: [...left.classes, ...right.classes],
177
+ exact: left.exact && right.exact,
178
+ variable: left.variable,
179
+ };
180
+ }
181
+ function isNothing(expression) {
182
+ if (expression === undefined)
183
+ return true;
184
+ const e = stripParentheses(expression);
185
+ return e.kind === ts.SyntaxKind.NullKeyword || (ts.isIdentifier(e) && e.text === "undefined");
186
+ }
187
+ /** Whether these statements do something for an error no branch named: not just `return null`. */
188
+ function isGeneric(statements) {
189
+ return statements.some((statement) => {
190
+ if (ts.isReturnStatement(statement))
191
+ return !isNothing(statement.expression);
192
+ if (ts.isBlock(statement))
193
+ return isGeneric(statement.statements);
194
+ return !ts.isEmptyStatement(statement);
195
+ });
196
+ }
197
+ function statementsOf(statement) {
198
+ return ts.isBlock(statement) ? statement.statements : [statement];
199
+ }
200
+ function exits(statement) {
201
+ const last = statementsOf(statement).at(-1);
202
+ return last !== undefined && (ts.isReturnStatement(last) || ts.isThrowStatement(last));
203
+ }
204
+ /** `a instanceof X ? … : b instanceof Y ? … : other`. */
205
+ function ternaryChain(expression, ref) {
206
+ let e = stripParentheses(expression);
207
+ if (!ts.isConditionalExpression(e))
208
+ return undefined;
209
+ const start = e;
210
+ const classes = [];
211
+ let exact = true;
212
+ let variable;
213
+ for (;;) {
214
+ const test = ts.isConditionalExpression(e) ? instanceofTest(e.condition, ref) : undefined;
215
+ if (!ts.isConditionalExpression(e) || test === undefined)
216
+ break;
217
+ classes.push(test.classes);
218
+ exact &&= test.exact;
219
+ variable ??= test.variable;
220
+ e = stripParentheses(e.whenFalse);
221
+ }
222
+ if (variable === undefined)
223
+ return undefined;
224
+ return {
225
+ at: [start.getStart(), start.getEnd()],
226
+ variable,
227
+ dispatch: "if",
228
+ classes,
229
+ exact,
230
+ generic: !isNothing(e),
231
+ };
232
+ }
233
+ /** The `instanceof` chain of a function body, from its first statement that branches on the error. */
234
+ function blockChain(statements, ref) {
235
+ for (const [index, statement] of statements.entries()) {
236
+ if (ts.isReturnStatement(statement) && statement.expression !== undefined) {
237
+ const found = ternaryChain(statement.expression, ref);
238
+ if (found !== undefined)
239
+ return found;
240
+ }
241
+ if (ts.isSwitchStatement(statement)) {
242
+ if (statement.expression.kind !== ts.SyntaxKind.TrueKeyword)
243
+ continue;
244
+ const classes = [];
245
+ let exact = true;
246
+ let variable;
247
+ let generic;
248
+ for (const clause of statement.caseBlock.clauses) {
249
+ if (ts.isDefaultClause(clause)) {
250
+ generic = isGeneric(clause.statements);
251
+ continue;
252
+ }
253
+ const test = instanceofTest(clause.expression, ref);
254
+ if (test === undefined) {
255
+ exact = false;
256
+ continue;
257
+ }
258
+ classes.push(test.classes);
259
+ exact &&= test.exact;
260
+ variable ??= test.variable;
261
+ }
262
+ if (variable === undefined)
263
+ continue;
264
+ return {
265
+ at: [statement.getStart(), statement.getEnd()],
266
+ variable,
267
+ dispatch: "switch",
268
+ classes,
269
+ exact,
270
+ generic: generic ?? isGeneric(statements.slice(index + 1)),
271
+ };
272
+ }
273
+ if (!ts.isIfStatement(statement) || instanceofTest(statement.expression, ref) === undefined) {
274
+ continue;
275
+ }
276
+ return ifChain(statements, index, ref);
277
+ }
278
+ return undefined;
279
+ }
280
+ /** An `if` / `else if` chain, or `if (…) return …;` statements one after another, then the rest. */
281
+ function ifChain(statements, from, ref) {
282
+ const first = statements[from];
283
+ const classes = [];
284
+ let exact = true;
285
+ let variable = "";
286
+ let index = from;
287
+ for (;;) {
288
+ const statement = statements[index];
289
+ const test = statement !== undefined && ts.isIfStatement(statement)
290
+ ? instanceofTest(statement.expression, ref)
291
+ : undefined;
292
+ if (statement === undefined || !ts.isIfStatement(statement) || test === undefined)
293
+ break;
294
+ let link = statement;
295
+ let linkTest = test;
296
+ let closed;
297
+ for (;;) {
298
+ classes.push(linkTest.classes);
299
+ exact &&= linkTest.exact;
300
+ if (variable === "")
301
+ variable = linkTest.variable;
302
+ const otherwise = link.elseStatement;
303
+ if (otherwise === undefined)
304
+ break;
305
+ const next = ts.isIfStatement(otherwise)
306
+ ? instanceofTest(otherwise.expression, ref)
307
+ : undefined;
308
+ if (ts.isIfStatement(otherwise) && next !== undefined) {
309
+ link = otherwise;
310
+ linkTest = next;
311
+ continue;
312
+ }
313
+ closed = isGeneric(statementsOf(otherwise));
314
+ break;
315
+ }
316
+ if (closed !== undefined) {
317
+ return { at: span(first), variable, dispatch: "if", classes, exact, generic: closed };
318
+ }
319
+ index += 1;
320
+ if (!exits(link.thenStatement))
321
+ break;
322
+ }
323
+ return {
324
+ at: span(first),
325
+ variable,
326
+ dispatch: "if",
327
+ classes,
328
+ exact,
329
+ generic: isGeneric(statements.slice(index)),
330
+ };
331
+ }
332
+ function span(node) {
333
+ return node === undefined ? [0, 0] : [node.getStart(), node.getEnd()];
334
+ }
335
+ /** How a fallback function branches on the error; null when it does not. */
336
+ function chainOf(types, fn, ref) {
337
+ const body = fn.body;
338
+ if (body === undefined)
339
+ return null;
340
+ let found;
341
+ if (ts.isBlock(body)) {
342
+ // `const { error } = props;` at the top binds the error too.
343
+ for (const statement of body.statements) {
344
+ if (!ts.isVariableStatement(statement))
345
+ continue;
346
+ for (const declaration of statement.declarationList.declarations) {
347
+ const value = declaration.initializer;
348
+ if (value !== undefined && ts.isIdentifier(value) && ref.props.has(value.text)) {
349
+ errorBindings(declaration.name, ref.names);
350
+ }
351
+ }
352
+ }
353
+ found = blockChain(body.statements, ref);
354
+ }
355
+ else {
356
+ found = ternaryChain(body, ref);
357
+ }
358
+ if (found === undefined)
359
+ return null;
360
+ const { classes, ...rest } = found;
361
+ const branches = classes.map((names) => {
362
+ const ids = names.map((name) => classIdOf(types, ts.isPropertyAccessExpression(name) ? name.name : name));
363
+ return ids.every((id) => id !== undefined) ? ids : null;
364
+ });
365
+ return { ...rest, file: fn.getSourceFile().fileName, branches };
366
+ }
367
+ /** The function a prop's value is: written in place, or a name of an own function. */
368
+ function functionOfValue(types, value) {
369
+ const e = stripParentheses(value);
370
+ if (ts.isArrowFunction(e) || ts.isFunctionExpression(e))
371
+ return e;
372
+ if (!ts.isIdentifier(e))
373
+ return undefined;
374
+ const symbol = types.symbolAt(e);
375
+ const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
376
+ if (declaration === undefined || !types.isOwnFile(declaration.file))
377
+ return undefined;
378
+ const node = declaration.node;
379
+ if (ts.isFunctionDeclaration(node))
380
+ return node;
381
+ if (ts.isVariableDeclaration(node) && node.initializer !== undefined) {
382
+ const init = stripParentheses(node.initializer);
383
+ if (ts.isArrowFunction(init) || ts.isFunctionExpression(init))
384
+ return init;
385
+ }
386
+ return undefined;
387
+ }
388
+ function fallbacksOf(types, opening, tag, sourceFile) {
389
+ const own = tag.boundary?.own;
390
+ if (own !== undefined && own !== null) {
391
+ const method = own.members.find((member) => ts.isMethodDeclaration(member) &&
392
+ ts.isIdentifier(member.name) &&
393
+ member.name.text === "getDerivedStateFromError");
394
+ if (method === undefined)
395
+ return [];
396
+ return [
397
+ {
398
+ kind: "getDerivedStateFromError",
399
+ at: [opening.tagName.getStart(sourceFile), opening.tagName.getEnd()],
400
+ name: `${own.name?.text ?? "boundary"}.getDerivedStateFromError`,
401
+ chain: chainOf(types, method, parameterRef(method)),
402
+ },
403
+ ];
404
+ }
405
+ const found = [];
406
+ for (const attribute of opening.attributes.properties) {
407
+ if (!ts.isJsxAttribute(attribute) || !ts.isIdentifier(attribute.name))
408
+ continue;
409
+ const kind = FALLBACK_KINDS.find((one) => one === attribute.name.getText(sourceFile));
410
+ const value = attribute.initializer !== undefined && ts.isJsxExpression(attribute.initializer)
411
+ ? attribute.initializer.expression
412
+ : undefined;
413
+ if (kind === undefined || value === undefined)
414
+ continue;
415
+ const fn = functionOfValue(types, value);
416
+ const valueText = stripParentheses(value);
417
+ found.push({
418
+ kind,
419
+ at: [attribute.getStart(sourceFile), attribute.getEnd()],
420
+ name: ts.isIdentifier(valueText) ? valueText.text : kind,
421
+ chain: fn === undefined
422
+ ? null
423
+ : chainOf(types, fn, kind === "onError" ? parameterRef(fn) : propsRef(fn)),
424
+ });
425
+ }
426
+ return found;
427
+ }
428
+ // ---- the file -----------------------------------------------------------------------------------
429
+ /** Whether `call` renders an app: `createRoot(el).render(<App/>)`, `hydrateRoot(el, <App/>)`. */
430
+ function entryArgument(types, call) {
431
+ const fromReactDom = (expression, name) => {
432
+ const e = stripParentheses(expression);
433
+ const symbol = ts.isIdentifier(e) ? types.symbolAt(e) : undefined;
434
+ const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
435
+ return (symbol?.name === name &&
436
+ declaration !== undefined &&
437
+ !types.isOwnFile(declaration.file) &&
438
+ packageNameOf(types, declaration.file) === "react-dom");
439
+ };
440
+ const isRoot = (expression) => {
441
+ const e = stripParentheses(expression);
442
+ if (ts.isCallExpression(e))
443
+ return fromReactDom(e.expression, "createRoot");
444
+ if (!ts.isIdentifier(e))
445
+ return false;
446
+ const symbol = types.symbolAt(e);
447
+ const declaration = symbol === undefined ? undefined : types.declarationOf(symbol);
448
+ const node = declaration?.node;
449
+ return (node !== undefined &&
450
+ ts.isVariableDeclaration(node) &&
451
+ node.initializer !== undefined &&
452
+ ts.isCallExpression(stripParentheses(node.initializer)) &&
453
+ isRoot(node.initializer));
454
+ };
455
+ const callee = stripParentheses(call.expression);
456
+ if (fromReactDom(callee, "hydrateRoot"))
457
+ return call.arguments[1];
458
+ if (ts.isPropertyAccessExpression(callee) && callee.name.text === "render") {
459
+ return isRoot(callee.expression) ? call.arguments[0] : undefined;
460
+ }
461
+ return undefined;
462
+ }
463
+ /** How many variables deep JSX is followed (`const page = <Layout>{content}</Layout>`). */
464
+ const MAX_HOLDERS = 5;
465
+ /**
466
+ * The local variable `child` is stored in: `const content = <A/>`, or `content = <A/>` for a
467
+ * `let`. Undefined for anything else.
468
+ */
469
+ function heldIn(parent, child) {
470
+ if (ts.isVariableDeclaration(parent) && parent.initializer === child) {
471
+ return ts.isIdentifier(parent.name) ? parent.name : undefined;
472
+ }
473
+ if (ts.isBinaryExpression(parent) &&
474
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
475
+ parent.right === child &&
476
+ ts.isIdentifier(parent.left)) {
477
+ return parent.left;
478
+ }
479
+ return undefined;
480
+ }
481
+ /** An identifier that is written, not read: `content = …`. */
482
+ function isWrite(node) {
483
+ const parent = node.parent;
484
+ return ((ts.isBinaryExpression(parent) &&
485
+ parent.left === node &&
486
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) ||
487
+ (ts.isVariableDeclaration(parent) && parent.name === node));
488
+ }
489
+ /**
490
+ * Where the value stops being followed and is rendered where it is: returned (from the
491
+ * component, or a callback such as `items.map(…)`), or the body of an arrow function.
492
+ */
493
+ function isFunctionBoundaryOrStatement(parent, child) {
494
+ return (ts.isReturnStatement(parent) ||
495
+ ((ts.isArrowFunction(parent) || ts.isFunctionExpression(parent)) && parent.body === child) ||
496
+ ts.isStatement(parent) ||
497
+ ts.isBlock(parent));
498
+ }
499
+ /** Nodes the JSX passes through unchanged on its way to where it is rendered. */
500
+ function passesValue(parent, child) {
501
+ if (ts.isParenthesizedExpression(parent) || ts.isJsxExpression(parent))
502
+ return true;
503
+ if (ts.isJsxAttribute(parent) || ts.isJsxAttributes(parent))
504
+ return true;
505
+ if (ts.isJsxElement(parent) || ts.isJsxSelfClosingElement(parent) || ts.isJsxFragment(parent)) {
506
+ return true;
507
+ }
508
+ if (ts.isJsxOpeningElement(parent))
509
+ return true;
510
+ if (ts.isConditionalExpression(parent))
511
+ return parent.condition !== child;
512
+ if (ts.isBinaryExpression(parent)) {
513
+ const operator = parent.operatorToken.kind;
514
+ return (parent.right === child &&
515
+ (operator === ts.SyntaxKind.AmpersandAmpersandToken ||
516
+ operator === ts.SyntaxKind.BarBarToken ||
517
+ operator === ts.SyntaxKind.QuestionQuestionToken));
518
+ }
519
+ return false;
520
+ }
521
+ export function summarizeTree(ctx, libraryBoundaries = LIBRARY_BOUNDARIES, skips = ALL_GUARDS) {
522
+ const { sourceFile, types } = ctx;
523
+ const file = sourceFile.fileName;
524
+ const spanOf = (node) => [node.getStart(sourceFile), node.getEnd()];
525
+ const tags = new Map();
526
+ const tagOf = (opening) => {
527
+ let tag = tags.get(opening);
528
+ if (tag === undefined) {
529
+ tag = classifyTag(types, opening.tagName, libraryBoundaries);
530
+ tags.set(opening, tag);
531
+ }
532
+ return tag;
533
+ };
534
+ const boundaries = [];
535
+ const keyOf = (element) => `${file}:${String(element.getStart(sourceFile))}`;
536
+ /**
537
+ * Where `node` is rendered, up to `stop`: for each place, the boundaries and own components
538
+ * around it, innermost first; null for a place it cannot be followed to. JSX held in a local
539
+ * variable is placed where the variable is used, as React renders it there.
540
+ */
541
+ const placementsOf = (node, stop, depth = 0) => {
542
+ const around = [];
543
+ // While true, `child` is the JSX value itself (or what it flows into unchanged).
544
+ let tracking = true;
545
+ let child = node;
546
+ for (let parent = node.parent; parent !== stop && !ts.isSourceFile(parent);) {
547
+ if (ts.isJsxElement(parent) &&
548
+ child !== parent.openingElement &&
549
+ child !== parent.closingElement) {
550
+ const tag = tagOf(parent.openingElement);
551
+ if (tag.boundary !== undefined)
552
+ around.push({ boundary: keyOf(parent) });
553
+ else if (tag.target !== undefined)
554
+ around.push({ via: tag.target });
555
+ }
556
+ else if (tracking) {
557
+ const held = heldIn(parent, child);
558
+ if (held !== undefined) {
559
+ return depth >= MAX_HOLDERS ? [null] : usesOf(held, stop, around, depth);
560
+ }
561
+ if (isFunctionBoundaryOrStatement(parent, child))
562
+ tracking = false;
563
+ else if (!passesValue(parent, child))
564
+ return [null];
565
+ }
566
+ child = parent;
567
+ parent = parent.parent;
568
+ }
569
+ return [around];
570
+ };
571
+ /** Each place the variable `held` is read, below `stop`, with `inner` around it first. */
572
+ const usesOf = (held, stop, inner, depth) => {
573
+ const id = types.symbolAt(held)?.id;
574
+ if (id === undefined)
575
+ return [null];
576
+ const places = [];
577
+ const walk = (node) => {
578
+ if (ts.isIdentifier(node) &&
579
+ node.text === held.text &&
580
+ node !== held &&
581
+ !isWrite(node) &&
582
+ types.symbolAt(node)?.id === id) {
583
+ for (const place of placementsOf(node, stop, depth + 1)) {
584
+ places.push(place === null ? null : [...inner, ...place]);
585
+ }
586
+ }
587
+ ts.forEachChild(node, walk);
588
+ };
589
+ walk(stop);
590
+ return places;
591
+ };
592
+ const functionNodes = new Set();
593
+ const found = [];
594
+ const findFunctions = (node) => {
595
+ const fn = renderFunctionOf(node);
596
+ if (fn !== undefined) {
597
+ found.push(fn);
598
+ functionNodes.add(fn.node);
599
+ }
600
+ ts.forEachChild(node, findFunctions);
601
+ };
602
+ findFunctions(sourceFile);
603
+ /** Each JSX element under `root` (not in another render function), with what it renders. */
604
+ const elementsIn = (root, visit) => {
605
+ const walk = (node) => {
606
+ if (node !== root && functionNodes.has(node))
607
+ return;
608
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node))
609
+ visit(node);
610
+ ts.forEachChild(node, walk);
611
+ };
612
+ walk(root);
613
+ };
614
+ const rendersOf = (element, stop) => {
615
+ const opening = openingOf(element);
616
+ const tag = tagOf(opening);
617
+ if (tag.boundary !== undefined) {
618
+ boundaries.push({
619
+ key: keyOf(element),
620
+ name: opening.tagName.getText(sourceFile),
621
+ at: spanOf(opening.tagName),
622
+ fallbacks: fallbacksOf(types, opening, tag, sourceFile),
623
+ });
624
+ }
625
+ const target = tag.target;
626
+ if (target === undefined)
627
+ return [];
628
+ const seen = new Set();
629
+ return placementsOf(element, stop).flatMap((around) => {
630
+ const key = JSON.stringify(around);
631
+ if (seen.has(key))
632
+ return [];
633
+ seen.add(key);
634
+ return [
635
+ {
636
+ target,
637
+ name: opening.tagName.getText(sourceFile),
638
+ at: spanOf(opening),
639
+ around: around ?? [],
640
+ unknown: around === null,
641
+ },
642
+ ];
643
+ });
644
+ };
645
+ const functions = [];
646
+ for (const fn of found) {
647
+ const symbol = types.symbolAt(fn.name);
648
+ if (symbol === undefined)
649
+ continue;
650
+ const renders = [];
651
+ elementsIn(fn.node, (element) => {
652
+ renders.push(...rendersOf(element, fn.node));
653
+ });
654
+ const hooks = [];
655
+ const effects = [];
656
+ const methods = [];
657
+ let children = null;
658
+ if (fn.kind === "class" && ts.isClassDeclaration(fn.node)) {
659
+ for (const member of fn.node.members) {
660
+ const name = ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) ? member.name : undefined;
661
+ const id = name === undefined || !CLASS_METHODS.has(name.text)
662
+ ? undefined
663
+ : types.symbolAt(name)?.id;
664
+ if (id !== undefined)
665
+ methods.push(id);
666
+ }
667
+ }
668
+ else if (!ts.isClassDeclaration(fn.node) && fn.node.body !== undefined) {
669
+ scanRunning(fn.node.body, (node) => {
670
+ if (!ts.isCallExpression(node))
671
+ return;
672
+ const callee = stripParentheses(node.expression);
673
+ const name = ts.isIdentifier(callee)
674
+ ? callee
675
+ : ts.isPropertyAccessExpression(callee)
676
+ ? callee.name
677
+ : undefined;
678
+ const called = name === undefined ? undefined : types.symbolAt(name);
679
+ const declaration = called === undefined ? undefined : types.declarationOf(called);
680
+ if (called === undefined || declaration === undefined)
681
+ return;
682
+ if (types.isOwnFile(declaration.file)) {
683
+ if (isHookName(called.name))
684
+ hooks.push(called.id);
685
+ return;
686
+ }
687
+ const effect = node.arguments[0];
688
+ if (EFFECT_HOOKS.has(called.name) &&
689
+ packageNameOf(types, declaration.file) === "react" &&
690
+ effect !== undefined) {
691
+ const callback = stripParentheses(effect);
692
+ if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) {
693
+ effects.push(callback.getStart(sourceFile));
694
+ }
695
+ }
696
+ });
697
+ if (fn.kind === "component") {
698
+ elementsInExpressions(fn.node, functionNodes, (expression) => {
699
+ const value = stripParentheses(expression);
700
+ const isChildren = (ts.isIdentifier(value) && value.text === "children") ||
701
+ (ts.isPropertyAccessExpression(value) && value.name.text === "children");
702
+ if (!isChildren)
703
+ return;
704
+ for (const place of placementsOf(value, fn.node)) {
705
+ const boundaryKeys = (place ?? []).flatMap((one) => "boundary" in one ? [one.boundary] : []);
706
+ children =
707
+ children === null
708
+ ? boundaryKeys
709
+ : children.filter((key) => boundaryKeys.includes(key));
710
+ }
711
+ });
712
+ }
713
+ }
714
+ functions.push({
715
+ id: symbol.id,
716
+ // `export default function App` is bound as `default`; the developer knows it as `App`.
717
+ name: symbol.name === "default" ? fn.name.text : symbol.name,
718
+ at: spanOf(fn.name),
719
+ kind: fn.kind,
720
+ renders,
721
+ hooks: [...new Set(hooks)],
722
+ effects,
723
+ methods,
724
+ children,
725
+ guards: ts.isClassDeclaration(fn.node) ? [] : guardThrows(fn.node, ctx, skips),
726
+ });
727
+ }
728
+ // Boundaries outside render functions (at an entry) are found while reading the entries.
729
+ const entries = [];
730
+ const findEntries = (node) => {
731
+ if (ts.isCallExpression(node)) {
732
+ const argument = entryArgument(types, node);
733
+ if (argument !== undefined) {
734
+ elementsIn(argument, (element) => {
735
+ entries.push(...rendersOf(element, node));
736
+ });
737
+ }
738
+ }
739
+ ts.forEachChild(node, findEntries);
740
+ };
741
+ findEntries(sourceFile);
742
+ return {
743
+ functions,
744
+ entries,
745
+ boundaries: [...new Map(boundaries.map((one) => [one.key, one])).values()],
746
+ };
747
+ }
748
+ /** The expressions in JSX braces (`{children}`) under `root`, not in another render function. */
749
+ function elementsInExpressions(root, functionNodes, visit) {
750
+ const walk = (node) => {
751
+ if (node !== root && functionNodes.has(node))
752
+ return;
753
+ if (ts.isJsxExpression(node) && node.expression !== undefined)
754
+ visit(node.expression);
755
+ ts.forEachChild(node, walk);
756
+ };
757
+ walk(root);
758
+ }
759
+ // ---- reading it back ----------------------------------------------------------------------------
760
+ function readAround(value) {
761
+ const f = fieldsOf(value);
762
+ if (isString(f?.boundary))
763
+ return { boundary: f.boundary };
764
+ return isString(f?.via) ? { via: f.via } : undefined;
765
+ }
766
+ function readRender(value) {
767
+ const f = fieldsOf(value);
768
+ const at = spanOf(f?.at);
769
+ const around = each(f?.around, readAround);
770
+ if (f === undefined || at === undefined || around === undefined)
771
+ return undefined;
772
+ if (typeof f.unknown !== "boolean")
773
+ return undefined;
774
+ return isString(f.target) && isString(f.name)
775
+ ? { target: f.target, name: f.name, at, around, unknown: f.unknown }
776
+ : undefined;
777
+ }
778
+ function readChain(value) {
779
+ if (value === null)
780
+ return null;
781
+ const f = value === undefined ? undefined : fieldsOf(value);
782
+ const at = spanOf(f?.at);
783
+ const branches = each(f?.branches, (item) => item === null ? null : readStrings(item));
784
+ if (f === undefined || at === undefined || branches === undefined)
785
+ return undefined;
786
+ if (!isString(f.file) || !isString(f.variable))
787
+ return undefined;
788
+ if (f.dispatch !== "if" && f.dispatch !== "switch")
789
+ return undefined;
790
+ if (typeof f.exact !== "boolean" || typeof f.generic !== "boolean")
791
+ return undefined;
792
+ return {
793
+ file: f.file,
794
+ at,
795
+ variable: f.variable,
796
+ dispatch: f.dispatch,
797
+ branches,
798
+ exact: f.exact,
799
+ generic: f.generic,
800
+ };
801
+ }
802
+ function readFallback(value) {
803
+ const f = fieldsOf(value);
804
+ const at = spanOf(f?.at);
805
+ const kind = FALLBACK_KINDS.find((one) => one === f?.kind);
806
+ const chain = readChain(f?.chain);
807
+ if (f === undefined || at === undefined || kind === undefined || chain === undefined) {
808
+ return undefined;
809
+ }
810
+ return isString(f.name) ? { kind, at, name: f.name, chain } : undefined;
811
+ }
812
+ function readBoundary(value) {
813
+ const f = fieldsOf(value);
814
+ const at = spanOf(f?.at);
815
+ const fallbacks = each(f?.fallbacks, readFallback);
816
+ if (f === undefined || at === undefined || fallbacks === undefined)
817
+ return undefined;
818
+ return isString(f.key) && isString(f.name)
819
+ ? { key: f.key, name: f.name, at, fallbacks }
820
+ : undefined;
821
+ }
822
+ const KINDS = ["component", "hook", "class"];
823
+ function readFunction(value) {
824
+ const f = fieldsOf(value);
825
+ const at = spanOf(f?.at);
826
+ const kind = KINDS.find((one) => one === f?.kind);
827
+ const renders = each(f?.renders, readRender);
828
+ const hooks = readStrings(f?.hooks);
829
+ const methods = readStrings(f?.methods);
830
+ const effects = each(f?.effects, (item) => (typeof item === "number" ? item : undefined));
831
+ const children = f?.children === null ? null : readStrings(f?.children);
832
+ const guards = each(f?.guards, (item) => (typeof item === "number" ? item : undefined));
833
+ if (f === undefined || at === undefined || kind === undefined || renders === undefined) {
834
+ return undefined;
835
+ }
836
+ if (hooks === undefined || methods === undefined || effects === undefined)
837
+ return undefined;
838
+ if (children === undefined || guards === undefined)
839
+ return undefined;
840
+ if (!isString(f.id) || !isString(f.name))
841
+ return undefined;
842
+ return { id: f.id, name: f.name, at, kind, renders, hooks, effects, methods, children, guards };
843
+ }
844
+ /** Read once per summary object: the engine hands out the same object while it is unchanged. */
845
+ const read = new WeakMap();
846
+ /** The tree as `summarizeTree` wrote it, or undefined for anything else (an old cache file). */
847
+ export function readTree(value) {
848
+ const f = value === undefined ? undefined : fieldsOf(value);
849
+ if (f === undefined)
850
+ return undefined;
851
+ const known = read.get(f);
852
+ if (known !== undefined)
853
+ return known ?? undefined;
854
+ const functions = each(f.functions, readFunction);
855
+ const entries = each(f.entries, readRender);
856
+ const boundaries = each(f.boundaries, readBoundary);
857
+ const tree = functions === undefined || entries === undefined || boundaries === undefined
858
+ ? undefined
859
+ : { functions, entries, boundaries };
860
+ read.set(f, tree ?? null);
861
+ return tree;
862
+ }
863
+ //# sourceMappingURL=tree.js.map