Repository navigation
fix(react): a name the file destructures at its top renders no JSX child - #2677
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
On main
24ea3c54there are 177 such jsx-render edges across 101 files, all in refine-examples, all wrong. 94 land onexamples/app-crm-minimal/.../gh-banner/index.tsx'sTextand 83 onexamples/finefoods-antd/.../title/index.tsx'sTitle. 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 }) => …)).jsxCodereturns 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'sbindingElementAt. So a rename's key, a default value and arequire(…)/import(…)declaration bind nothing, as inside a function.declaredInside), else nothing. So a component with its ownfunction Text()inside still renders it.jsxCodeview 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 withscripts/dump-graph.mjs. The two arms differed only incallback-synthesizer.js.Tests
Two new tests in
__tests__/jsx-child-disambiguation.test.ts:function Text()still renders it. This test fails on main.requiredestructuring, a top-level callback's destructured parameter, and another function's local destructuring.Mutations:
const { Chart } = propsread as top-level)declaredInsideSummaryFull 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
jsxEdgespass (hrtime around the two new functions):topLevelPatternspatternsBindThe 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 calldestructuredAtTop(name)too, for exampleown ? 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'sButtonwould 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