Skip to content

fix(react): a name the file destructures at its top renders no JSX child - #2677

Merged
colbymchenry merged 1 commit into
mainfrom
claude/dreamy-khorana-module-destructuring
Oct 19, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/dreamy-khorana-module-destructuring

Conversation

@colbymchenry

Copy link
Copy Markdown
Owner

Follow-up to #2676, which made a JSX tag whose name the parent, or a function around it, destructures render only a component that function declares inside itself.

The gap

#2676's survey also counted tags whose name the file destructures at its top, outside every function. No function's code holds that binding, so those tags still linked any same-named component in the repository. refine's pages take antd's typography parts once, above their components:

import { Typography } from "antd";

const { Title, Text } = Typography;

export const PostShow = () => (
  <Show>
    <Title level={5}>Title</Title>
    <Text>{record?.title}</Text>
  </Show>
);

On main 24ea3c54 there are 177 such jsx-render edges across 101 files, all in refine-examples, all wrong. 94 land on examples/app-crm-minimal/.../gh-banner/index.tsx's Text and 83 on examples/finefoods-antd/.../title/index.tsx's Title. The TypeScript checker binds every one of their 575 tag sites to a module-level destructured local (module:short-var). No module-level destructuring in the other 30 repos produced an edge.

What changes

In reactJsxChildEdges (callback-synthesizer.ts):

  • topLevelPatterns(code) reads the file's code-only view (jsxCode) and returns the patterns of destructuring declarations (const/let/var {/[) at brace depth 0. That is a declaration in no function, class or block, and never a parameter of a top-level callback (items.forEach(({ Badge }) => …)).
    • Only braces are counted. JSX text may hold a stray parenthesis or bracket, but never a brace that opens no expression.
    • jsxCode returns null for a file whose braces don't balance, and such a file binds nothing here.
  • patternsBind(code, patterns, name) checks each occurrence with fix(react): a name a component destructures, or a function around it binds, renders no JSX child #2676's bindingElementAt. So a rename's key, a default value and a require(…) / import(…) declaration bind nothing, as inside a function.
  • When it applies: only to a tag that no function in its scope chain binds, as the lexical order requires. The tag then renders only a component the outermost function around it declares inside itself (declaredInside), else nothing. So a component with its own function Text() inside still renders it.
  • The file's jsxCode view is now read once per file and shared by the span scan and this check.

Validation

Fresh before/after indexes on #2676's 31 survey repos, on main 24ea3c54, compared with scripts/dump-graph.mjs. The two arms differed only in callback-synthesizer.js.

  • refine-examples: −177 / +0. It is exactly the survey's set, and every removed edge's tags are all module-level destructured locals by the TypeScript checker.
  • The other 30 repos are byte-identical: excalidraw, bulletproof, matx, mantis, minimal-kit, proshop, next-saas, t3-turbo, outline, crwn-clothing, binner, rr-examples, vispana, hathor-explorer, berry, cboard, chickadee, chushi, clean-arch, eshop-ddd, paydirt, persist-login, proshop-v2, ranking-app, react-boilerplate, redux-realworld, rr-auth-v6, takenote, vanlife and volun-mern.
  • No node, ref, file or non-jsx-render edge changes.

Tests

Two new tests in __tests__/jsx-child-disambiguation.test.ts:

  1. Renders nothing for a name the file destructures at its top, unless a function declares it inside itself. refine's shape renders nothing. A sibling component with its own function Text() still renders it. This test fails on main.
  2. Still renders the component for: a top-level require destructuring, a top-level callback's destructured parameter, and another function's local destructuring.

Mutations:

Mutation Fails
the brace-depth check off test 2 (a helper's local const { Chart } = props read as top-level)
top-level names dropped without declaredInside test 1's Summary

Full suite on main 24ea3c54 + this branch, Windows 11 x64, Node 22, kernel built, CODEGRAPH_KERNEL_EXPECT=1, --maxWorkers=4 --minWorkers=1: 618 files passed, 6 skipped; 7,501 tests passed, 80 skipped, 0 failed.

Cost

An instrumented jsxEdges pass (hrtime around the two new functions):

Repo topLevelPatterns patternsBind
excalidraw 6–7 ms (137 files) 0.08 ms
outline 7 ms (564 files) —
refine-examples 6–8 ms (762 files) 1.3–1.5 ms (193 names)
binner 3 ms (57 files) 0.07 ms

The pass itself takes 0.7–3.7 s of CPU on these repos, so that is under 1%. One outline run read 14 ms during a load spike on this shared machine, with a wall time of 11.5 s.

Overlap: #2611 (open, @inth3shadows)

#2611 gives tags outside every function a parent: the file or a top-level value. Its "Known limits" lists module-level destructuring (const { Excalidraw } = window.ExcalidrawLib) linking by name. When it lands, its owning parents can call destructuredAtTop(name) too, for example own ? ownLinesBind(...) || destructuredAtTop(name) : …. That closes the limit for tags at the top as well.

Not handled

A name destructured at the top from a project module's namespace, like import * as UI from './ui'; const { Button } = UI;, now renders nothing, where ./ui's Button would be exact. None of the 31 repos has an edge of that shape, so it is left for when one turns up.

🤖 Generated with Claude Code

Since PR 2676, a tag whose name the parent, or a function around it,
destructures renders only a component that function declares inside
itself. A name the file destructures at its top was left out, because no
function's code holds it: refine's pages take `const { Title, Text } =
Typography;` from antd once, above their components, and every `<Title>`
and `<Text>` in them linked to another example's `Title` or `Text`
component by name.

The pass now reads the destructuring patterns the file declares outside
every brace (`topLevelPatterns`, on the code-only view), so in no
function, class or block, and never a top-level callback's parameter.
Only braces are counted: JSX text may hold a stray parenthesis or
bracket, never a brace that opens no expression, and `jsxCode` returns
null for a file whose braces don't balance, which then binds nothing
here. A tag that no function in its scope chain binds, but that such a
pattern binds (`bindingElementAt`, so not a `require(...)` or `import(...)`
declaration), renders only a component the outermost of those functions
declares inside itself, else nothing.

Fresh before/after indexes on 31 repos (main 24ea3c5): 177 jsx-render
edges removed, all in refine-examples (94 to one example's `Text`, 83 to
another's `Title`), and every tag of each is a module-level destructured
local by the TypeScript checker. Nothing else changes; 30 repos are
byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry merged commit 22d420e into main Oct 19, 2026
@colbymchenry
colbymchenry deleted the claude/dreamy-khorana-module-destructuring branch October 19, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant