From 22502f02946034e1b99f6d1a362b2b465e1824fe Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Mon, 19 Oct 2026 13:52:40 -0500 Subject: [PATCH] fix(react): a name a component destructures, or a function around it binds, renders no JSX child MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #2442 made a tag the parent binds itself, as a `const` or a parameter, render only a component the parent declares inside itself. Destructuring was left out, as name-matcher leaves it out for a bare call (`const { t } = useI18n()` is that function more often than not), so a destructured tag still linked any same-named component in the repository. For a tag that is the wrong default: what a component destructures is a component its props or a library hand in, which is no node here. - refine's layouts, `({ children, Header, Footer, Sider, OffLayoutArea }) =>`, rendered other examples' `Header`, `Footer`, `Sider` and `OffLayoutArea`; `const { Title } = Typography` (antd) and `const { Link } = useRouterContext()` rendered other examples' `Title` and `Link`. - excalidraw's example app, `const { Sidebar, Footer, WelcomeScreen, … } = excalidrawLib`, rendered its own `ExampleSidebar.tsx` for the library's `Sidebar`, and `Footer.tsx` though the library exports `FooterCenter` as `Footer`. Its other tags landed on the library's components by luck. A tag now checks the parent's code through its first tag for a destructuring pattern that binds the name (`destructures`, read off the code-only view): a declaration's or a parameter's, nested patterns, rest elements, rename targets and array patterns included. A rename's key, a default value and a type literal after a pattern bind nothing, nor does an object argument or a control-flow head, and a declaration of `require(…)` or `import(…)` binds the module's own export, which links as before. The same check, and #2442's `const`/parameter check, now also reads the functions around the parent: excalidraw's `renderExcalidraw` and `renderTopRightUI` render what `ExampleApp` destructures. The function that binds the name is the one whose own components a tag may still render (`declaredInside`), so a helper rendering a component its enclosing function declares (binner's `recursiveTreeItem` → `StyledTreeItem`) keeps its edge. Fresh before/after indexes on 31 repos (main a50104c8): 24 jsx-render edges removed (excalidraw 14, refine-examples 10), every one graded all-local by the TypeScript checker (each tag a destructured local); 15 targets were wrong, 9 right by luck. One of them, ExampleApp → ExampleSidebar's `Sidebar`, comes back with `via` ExampleSidebar, the default import that renders it, which the wrong `` edge used to hide. No other edge, node, ref or file changes; 29 repos are byte-identical. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/jsx-child-disambiguation.test.ts | 167 +++++++++++- src/resolution/callback-synthesizer.ts | 286 ++++++++++++++++++--- 3 files changed, 412 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fbf0745e3..111929ca28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - React Router routes are now read from every table a route-building function returns, an early `if (!user) return [];` and each branch alike, and never from a list that a function written inside it returns, like a `.forEach` callback's `return []` or a helper's menu. A function that returns another's table, as in `return buildRoutes()`, is followed too, and so is a table behind `useMemo`, like `useMemo(() => getRoutes(user), [user])` or `useMemo(getRoutes, [])`. Before, the first `return [` anywhere in the function was taken for the table, so an empty guard or a callback's list could hide an app's routes entirely, or a menu could be read in their place. Re-index React projects after upgrading. - In JavaScript and TypeScript, an import written `import { default as AppRoot } from './routes/app/root'`, as in bulletproof-react's router, now counts as the module's default import, the same as `import AppRoot from './routes/app/root'`. Before, it was read as an import of an export named `default`, which no module has, so a call, a route or a JSX attribute that used `AppRoot` was matched by its name alone: it could link to another file's `AppRoot`, or to nothing when the default export has a name of its own. Svelte, Vue and Astro script blocks are read the same way. - In React, a component's JSX no longer links it to a class or component that only shares a name with one of its type arguments or with a variable of its own. A type in angle brackets, like `Document` in ` items={…} />`, `User` in `useState()` or `Entry` in a generic `(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `` in `widgets.map((Widget) => )`, now links to nothing, unless the component declares a component of that name inside itself. Before, these linked to an unrelated class or component elsewhere in the repository: outline's document lists showed up among the callers of its `Document` model class, and its menus among the callers of the command bar's `Content`, so `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading. +- In React, a tag for a component a function destructures from its props or from an object, like `({ Header, Footer, children }) =>` in refine's layouts or `const { Sidebar, Footer } = excalidrawLib` in excalidraw's example app, now links to nothing, as a tag naming a variable the function sets itself already did, unless the function declares a component of that name inside itself. A function nested in the one that sets or destructures the name, like a `renderSidebar` helper inside the component, reads the tag the same way. A name destructured from `require(…)` or `import(…)` still links to that module's export. Before, these tags were matched by their name alone, so refine's layouts showed up among the callers of other examples' `Header` and `Footer` components, and `codegraph_explore`, callers and impact followed renders that never happen. Re-index React projects after upgrading. - In React, a tag a component only mentions in a comment or a string no longer links it to that component: a JSDoc example, markup commented out like `{/* */}`, or an error message like react-router's "You cannot render a inside another " or shadcn's "useCarousel must be used within a ". Nothing in a TypeScript `.ts` file, which can't hold JSX, is read as a tag either, like the XML in outline's OpenSearch template. Before, these counted as renders, so react-router's hooks showed up among the callers of `Router`, `Routes` and `Route`, and `codegraph_explore`, callers and impact followed renders that never happen. A tag after an apostrophe, a URL or a backtick in a component's text, like `

Don't have an account? `, still links as before. Re-index React projects after upgrading. - In JavaScript and TypeScript, a name imported from a package your `package.json` lists no longer links to a project symbol that only shares its name when `tsconfig.json` or `jsconfig.json` has a catch-all path alias, like `"*": ["./typings/*"]` or `"*": ["src/*", "node_modules/*"]`, or an alias that points the package at a file in `node_modules`, like `"lit/decorators": ["./node_modules/lit/decorators.js"]`. Such an alias made every package look like part of the project, so `import { Typography } from '@mui/material'` was linked to the project's own `Typography` and every lit `@property()` decorator to an unrelated class's `property` field, and `codegraph callers`, impact and `codegraph affected` listed code that never used them. An import the alias does map to a file of your project, like a `.d.ts` you keep for an untyped package or `components/Button` through `"*": ["src/*"]`, links as before. Re-index affected projects after upgrading. - In JavaScript and TypeScript, a function the file imports from a package and passes along as a value, like react-router's `Link` in `styled(Link)`, lodash's `noop` in `createContext({ onClose: noop })` or chart.js's `Legend` in `Chart.register(Legend)`, no longer links to a project function that only shares its name. Before, whenever the project had exactly one function of that name, every such value linked to it: outline's breadcrumbs, menus and document lists showed up among the callers of an email template's `Link`, and a test's own `noop` stub in cord-field among the code the app's contexts use, so `codegraph callers`, impact and `codegraph_explore` followed references that never happen. A function the file declares itself, including one that hides the import inside a function, and one imported from a file of your project link as before. Re-index JavaScript and TypeScript projects after upgrading. diff --git a/__tests__/jsx-child-disambiguation.test.ts b/__tests__/jsx-child-disambiguation.test.ts index a5d669e7b6..4791a7db72 100644 --- a/__tests__/jsx-child-disambiguation.test.ts +++ b/__tests__/jsx-child-disambiguation.test.ts @@ -139,12 +139,15 @@ export function Header() { return

; } * writes a type's name right after another name. outline's document list * rendered its `Document` model class, its settings tables the `User` and * `Group` classes. - * - A name the parent binds itself, as a `const` or a parameter, is that local - * wherever its tags sit. outline's menus pick a Radix part into `const - * Content = variant === 'dropdown' ? DropdownMenu.SubContent : + * - A name the parent binds itself, as a `const`, a parameter or a name it + * destructures, is that local wherever its tags sit, and so is a name a + * function around the parent binds. outline's menus pick a Radix part into + * `const Content = variant === 'dropdown' ? DropdownMenu.SubContent : * ContextMenu.SubContent` and rendered the command bar's styled `Content` - * instead. A local is no node, so it renders nothing, unless the parent - * declares a component of that name inside itself. + * instead; refine's layout renders the `Header` and `Footer` its props hand + * in, and rendered other examples' components of those names. A local is no + * node, so it renders nothing, unless the function that binds it declares a + * component of that name inside itself. */ describe('JSX child: a name the parent writes only as a type, or binds itself', () => { let dir: string; @@ -322,24 +325,164 @@ export function Toolbar({ actions }) { expect(renders('Toolbar')).toEqual(['Icon app/icon.tsx:1']); }); - it('keeps a destructured name on the same-named component, as a destructured call does', async () => { - // excalidraw's example app: `const { Sidebar, Footer, … } = excalidrawLib`. - // Destructuring is not the parent's own binding here, as it is not for a - // bare call (`const { t } = useI18n()`), so the name links by name. + it('renders nothing for a name the parent destructures from a value or its props', async () => { write( 'packages/excalidraw/components/WelcomeScreen.tsx', 'export default function WelcomeScreen() {\n return
;\n}\n' ); + write('examples/app/sidebar/ExampleSidebar.tsx', 'export function Sidebar() {\n return