|
1 | 1 | # error-boundaries IMPL–SPEC Diff Report |
2 | 2 |
|
3 | | -**IMPL**: `error-boundaries.ts` (ESLint rule)\ |
4 | | -**SPEC**: `error-boundaries.spec.md` (React Compiler `ValidateNoJSXInTryStatement`) |
| 3 | +## Verification metadata |
5 | 4 |
|
6 | | ---- |
| 5 | +- **IMPL**: `error-boundaries.ts` (ESLint rule) |
| 6 | +- **SPEC**: `error-boundaries.spec.md` (React Compiler `ValidateNoJSXInTryStatement`) |
| 7 | +- **Implementation commit**: `55c10db7bae04d49606792767530cc1e786dd5a0` |
| 8 | +- **React commit**: `c0c39a6b3907eaab35f43074949e2957a2a734c1` |
| 9 | +- **Last verified**: `2026-07-14` |
| 10 | +- **React package**: `compiler/packages/babel-plugin-react-compiler` |
| 11 | +- **Implementation sources/tests**: |
| 12 | + - `error-boundaries.ts` |
| 13 | + - `error-boundaries.spec.ts` |
| 14 | +- **React sources/fixtures**: |
| 15 | + - `src/Validation/ValidateNoJSXInTryStatement.ts` |
| 16 | + - `src/__tests__/fixtures/compiler/invalid-jsx-in-try-with-catch.js` |
| 17 | + - `src/__tests__/fixtures/compiler/invalid-jsx-in-catch-in-outer-try-with-catch.js` |
| 18 | + - `src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-try-with-finally.js` |
| 19 | + - `src/__tests__/fixtures/compiler/error.todo-invalid-jsx-in-catch-in-outer-try-with-finally.js` |
7 | 20 |
|
8 | | -## 1. Underlying Mechanism |
| 21 | +“Source” below means behavior directly established by the files above; “fixture” means an explicit reviewed case; “inference” is kept separate. |
9 | 22 |
|
10 | | -The SPEC operates on the React Compiler's High-level IR (HIR), using block-based CFG traversal with an `activeTryBlocks` stack. It tracks `JsxExpression` and `JsxFragment` instructions and operates on `HIRFunction` scope with `outputMode === 'lint'`. |
| 23 | +## 1. Detection mechanism and breadth |
11 | 24 |
|
12 | | -The IMPL operates on the ESLint AST with parent-node traversal (`Traverse.findParent`). It detects JSX-like return values via `isJsxLike(context, ret, hint)` and is explicitly scoped to function components and hooks via collectors. |
| 25 | +**Source — React pass.** `validateNoJSXInTryStatement(fn)` iterates `fn.body.blocks`, maintains an `activeTryBlocks` stack, and reports every `JsxExpression` or `JsxFragment` instruction encountered while that stack is non-empty. |
13 | 26 |
|
14 | | ---- |
| 27 | +**Source — IMPL.** The rule collects function components and hooks, then checks JSX-like values from their collected `rets`. It does not visit every JSX creation site. Separately, it collects `use()` calls and reports those that are in a matching try body belonging to a collected component or hook. |
15 | 28 |
|
16 | | -## 2. Rule: No JSX in Try Block |
| 29 | +**Fixture.** React's `invalid-jsx-in-try-with-catch.js` assigns `<div />` to a variable inside a try body. This is an explicit compiler error case; it is not the return-value shape inspected by the IMPL. |
17 | 30 |
|
18 | | -### Detection Target |
| 31 | +**Inference.** The IMPL's JSX candidate set is narrower than the pass's instruction-level JSX scan. The IMPL also adds `tryCatchWithUse`, which has no counterpart in this React pass. |
19 | 32 |
|
20 | | -The SPEC flags any `JsxExpression` or `JsxFragment` instruction appearing inside a try block, including JSX assigned to variables (e.g., `el = <div />`). |
| 33 | +## 2. Try ancestry, catch, and finally |
21 | 34 |
|
22 | | -The IMPL only inspects `ReturnStatement` nodes with a `TryStatement` ancestor. Intermediate variable assignments inside try blocks are missed. |
| 35 | +**Source — IMPL traversal.** `getEnclosingTryBlock` uses a custom `while` parent-chain, not repeated `Traverse.findParent` calls. For each ancestor `TryStatement`, it walks from the original node toward that try and matches only if the path passes through `TryStatement.block`. |
23 | 36 |
|
24 | | -**Verdict**: IMPL detection is narrower. It only catches JSX returned directly from a try/catch scope. |
| 37 | +Consequently: |
25 | 38 |
|
26 | | -### Catch Block Handling |
| 39 | +- a node in a try body matches that try; |
| 40 | +- a node in that try's own `catch` or `finally` does not match that try; |
| 41 | +- traversal continues outward, so the same node can match an outer try when its path passes through the outer `TryStatement.block`. |
27 | 42 |
|
28 | | -The SPEC maintains an `activeTryBlocks` stack and explicitly removes catch handler blocks from the stack at block start. JSX inside a catch block is outside the try scope when there is no enclosing outer try. |
| 43 | +The IMPL's remaining `Traverse.findParent` use checks whether a matched `use()` try belongs to a collected component or hook; it is not the try-body ancestry algorithm. |
29 | 44 |
|
30 | | -The IMPL now uses repeated `Traverse.findParent` calls to determine whether a `ReturnStatement` sits inside the `try` block or inside the `catch` / `finally` block of its nearest ancestor `TryStatement`. If the node is in `catch` / `finally`, the traversal continues upward to check for an enclosing outer `TryStatement`. |
| 45 | +**Source — React pass.** At each HIR block, the pass removes that block's ID from `activeTryBlocks` before inspecting instructions. This excludes a current try's handler while retaining any still-active outer try. |
31 | 46 |
|
32 | | -Key behavioral difference: |
| 47 | +**Fixtures.** The reviewed React fixtures explicitly cover JSX in a try body with catch and JSX in an inner catch that remains inside an outer try. Together with the reviewed IMPL tests, these support alignment for those two cases. |
33 | 48 |
|
34 | | -- JSX in catch (no outer try): **Allowed** by both. |
35 | | -- JSX in catch (nested in outer try): **Error** for both. |
| 49 | +**Source/fixtures — Finally.** The IMPL does not treat a current try's own `finally` as its try body; it can only match an enclosing outer try as described above. The React pass has no explicit finally branch, and the two reviewed finally scenarios are TODO fixtures. |
36 | 50 |
|
37 | | -**Verdict**: Both now align on catch-block exemptions. |
| 51 | +**Inference.** Current source and fixtures do not establish general IMPL/compiler parity for finally-related control flow. |
38 | 52 |
|
39 | | -### Nested Try/Catch |
| 53 | +## 3. Function scope and nested callbacks |
40 | 54 |
|
41 | | -The SPEC's `activeTryBlocks` stack naturally handles nesting depth. The IMPL now resolves nested structures by walking up through successive `TryStatement` ancestors, skipping those whose `catch` / `finally` blocks contain the node, until it finds one whose `try` block does. |
| 55 | +**Source — IMPL.** The rule's JSX checks are driven by `rets` supplied by the function-component and hook collectors, rather than by a recursive scan of all nested function bodies. |
42 | 56 |
|
43 | | -**Verdict**: Both handle nested structures correctly and align on catch-block exemptions. |
| 57 | +**Source — React pass.** One invocation scans only the blocks of the supplied `HIRFunction`; this pass does not recursively traverse lowered nested functions. |
44 | 58 |
|
45 | | ---- |
| 59 | +**Fixture boundary.** The reviewed React fixtures cover try/catch, catch inside an outer try, and the two finally TODOs. None contains a nested callback. |
46 | 60 |
|
47 | | -## 3. Rule: No `use` Hook in Try/Catch |
| 61 | +**Inference boundary.** Current pass source and fixtures do not prove how the compiler pipeline handles JSX inside a nested callback. In particular, this report does not assert that the compiler must report such a case; that would require pipeline/lowering evidence or a dedicated fixture. |
48 | 62 |
|
49 | | -The SPEC does not include this check. The IMPL adds an extra `tryCatchWithUse` rule that reports `use()` calls inside try blocks. |
| 63 | +## 4. Message mapping |
50 | 64 |
|
51 | | -Rationale: the `use` hook suspends the component; its errors can only be caught by Error Boundaries, not try/catch. |
52 | | - |
53 | | -**Verdict**: IMPL extends the compiler SPEC with an extra rule that has no SPEC counterpart. |
54 | | - |
55 | | ---- |
56 | | - |
57 | | -## 4. Finally Blocks |
58 | | - |
59 | | -The SPEC explicitly marks finally blocks as TODO / unsupported (`error.todo-invalid-jsx-in-try-with-finally.js`). The IMPL provides no special handling; AST traversal treats finally as a normal descendant of `TryStatement`. |
60 | | - |
61 | | -**Verdict**: IMPL provides basic coverage for finally via AST ancestry, while the SPEC documents known unsupported cases. |
62 | | - |
63 | | ---- |
64 | | - |
65 | | -## 5. Non-Component / Non-Hook Functions |
66 | | - |
67 | | -The SPEC implicitly limits scope via `HIRFunction`. The IMPL explicitly filters — only function components and hooks are collected. Utility functions (e.g., lowercase `processItems`, `fetchData`) are ignored. |
68 | | - |
69 | | -**Verdict**: Both are effectively limited to component-like functions, but the IMPL achieves this explicitly via collectors. |
70 | | - |
71 | | -### Nested Functions and Hook Callbacks |
72 | | - |
73 | | -A corollary of the above: the IMPL does **not** inspect `ReturnStatement` nodes inside nested function expressions, arrow functions, object methods, or hook callbacks (e.g., `useMemo(() => { try { return <div />; } catch {} })`). Because these nested functions are not themselves components or hooks, their returns are never examined for JSX-in-try violations. |
74 | | - |
75 | | -The SPEC, operating on the full HIR of the component, would still flag JSX constructed inside a try block regardless of whether it appears in a nested callback. [NEEDS VERIFICATION] |
76 | | - |
77 | | -**Verdict**: IMPL misses try/catch JSX inside nested callbacks; SPEC catches them uniformly. |
78 | | - |
79 | | ---- |
80 | | - |
81 | | -## 6. Message ID Mapping |
82 | | - |
83 | | -The SPEC defines a single error message. The IMPL splits the concept into two distinct MessageIDs: |
84 | | - |
85 | | -- `tryCatchWithJsx` — "Avoid constructing JSX within try/catch"\ |
86 | | - Maps to the SPEC's single error. |
87 | | -- `tryCatchWithUse` — **IMPL-only**\ |
88 | | - Reports `use()` hook calls inside a `TryStatement`. |
89 | | - |
90 | | -Error text is aligned in intent (recommending Error Boundaries) but the IMPL adds hook-specific wording for the `use` case. |
91 | | - |
92 | | ---- |
93 | | - |
94 | | -## 7. Key Gaps and Deviations |
95 | | - |
96 | | -1. **Narrower JSX Detection**: The IMPL only catches JSX in `ReturnStatement` nodes, missing intermediate assignments such as `el = <div />` inside try blocks. |
97 | | -2. **`use` Hook Extension**: The IMPL adds `tryCatchWithUse`, a rule with no equivalent in the compiler SPEC. |
98 | | -3. **Finally Handling**: The SPEC documents TODOs for finally blocks; the IMPL provides implicit coverage via AST ancestry but does not document any gaps. |
| 65 | +- `tryCatchWithJsx` corresponds in intent to the pass diagnostic, “Avoid constructing JSX within try/catch,” though the exact text differs. |
| 66 | +- `tryCatchWithUse` is IMPL-only and advises using an Error Boundary around `use()` rather than try/catch. |
0 commit comments