fix(react): a name a component destructures, or a function around it binds, renders no JSX child (#2676)

#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 `<Sidebar>` edge used to
hide. No other edge, node, ref or file changes; 29 repos are
byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-10-19 19:38:59 +00:00
committed by GitHub
parent bb8a0f732b
commit 24ea3c54b2
3 changed files with 412 additions and 42 deletions
+1
View File
@@ -164,6 +164,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 `<PaginatedList<Document> items={…} />`, `User` in `useState<User>()` or `Entry` in a generic `<Entry extends BaseEntity>(…) =>` component, is no longer read as a tag. A tag naming a variable or parameter the component sets itself, like `<Content>` after `const Content = isDropdown ? DropdownMenu.SubContent : ContextMenu.SubContent` or `<Widget>` in `widgets.map((Widget) => <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 `{/* <Progress /> */}`, or an error message like react-router's "You cannot render a <Router> inside another <Router>" or shadcn's "useCarousel must be used within a <Carousel />". 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 `<p>Don't have an account? <Link to="/signup">`, 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.
+155 -12
View File
@@ -139,12 +139,15 @@ export function Header() { return <h1><Badge /></h1>; }
* 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 <div />;\n}\n'
);
write('examples/app/sidebar/ExampleSidebar.tsx', 'export function Sidebar() {\n return <aside />;\n}\n');
write('examples/blog/header.tsx', 'export function Header() {\n return <header />;\n}\n');
write('examples/blog/footer.tsx', 'export function Footer() {\n return <footer />;\n}\n');
write('shared/glyph.tsx', 'export function Glyph() {\n return <i />;\n}\n');
write('shared/icon.tsx', 'export function Icon() {\n return <svg />;\n}\n');
write('shared/panel.tsx', 'export function Panel() {\n return <section />;\n}\n');
// excalidraw's example app takes the library as a prop and renders its
// parts, in the component and in a function nested in it.
write(
'examples/app/ExampleApp.tsx',
`export default function ExampleApp({ excalidrawLib }) {
const { WelcomeScreen, exportToSvg } = excalidrawLib;
return <div><WelcomeScreen /></div>;
const { WelcomeScreen, exportToSvg, Sidebar } = excalidrawLib;
const renderSidebar = () => {
return <Sidebar name="custom" />;
};
return <div><WelcomeScreen />{renderSidebar()}</div>;
}
`
);
// refine's layout renders the parts its props hand in.
write(
'examples/app/Layout.tsx',
`export const Layout = ({ children, Header, Footer }) => {
return (
<div>
{Header && <Header />}
{children}
{Footer && <Footer />}
</div>
);
};
`
);
// A rename's target, a pattern nested in a parameter's, an array pattern.
write(
'examples/app/Nav.tsx',
`import * as React from 'react';
export function Nav({ items, slots: { Panel } }) {
const [Icon] = React.useState(null);
return (
<ul>
{items.map(({ icon: Glyph, label }) => <li key={label}><Glyph /><Icon /></li>)}
<Panel />
</ul>
);
}
`
);
await index();
expect(renders('ExampleApp')).toEqual(['WelcomeScreen packages/excalidraw/components/WelcomeScreen.tsx:1']);
expect(renders('ExampleApp')).toEqual([]);
expect(renders('renderSidebar')).toEqual([]);
expect(renders('Layout')).toEqual([]);
expect(renders('Nav')).toEqual([]);
});
it('renders nothing for a name a function around the parent binds, unless that function declares it', async () => {
write('a-decoy/icon.tsx', 'export function Icon() {\n return <svg />;\n}\n');
write('a-decoy/item.tsx', 'export function StyledItem() {\n return <li />;\n}\n');
write('a-decoy/cell.tsx', 'export function Cell() {\n return <td />;\n}\n');
write(
'app/menu.tsx',
`export function Menu({ items }) {
const Icon = items[0].icon;
const renderItem = (item) => <li key={item.id}><Icon /></li>;
return <ul>{items.map(renderItem)}</ul>;
}
export function Tree({ nodes }) {
const StyledItem = ({ label }) => <li>{label}</li>;
const renderNode = (node) => <StyledItem key={node.id} label={node.label} />;
return <ul>{nodes.map(renderNode)}</ul>;
}
`
);
// The callback's `Cell` is its own; the tag below it renders the
// component the table declares.
write(
'app/table.tsx',
`export function Table({ rows, columns }) {
function Cell({ value }) {
return <td>{value}</td>;
}
const titles = columns.map(({ Cell, title }) => title);
return <table>{rows.map((row) => <tr key={row.id}><Cell value={row.value} /></tr>)}</table>;
}
`
);
await index();
expect(renders('renderItem')).toEqual([]);
expect(renders('renderNode')).toEqual(['StyledItem app/menu.tsx:8']);
expect(renders('Table')).toEqual(['Cell app/table.tsx:2']);
});
it('still renders the component for a rename’s key, a default value, a type literal, an object argument or a require', async () => {
write('app/footer.tsx', 'export function Footer() {\n return <footer />;\n}\n');
write('app/fallback.tsx', 'export function Fallback() {\n return <div />;\n}\n');
write('app/badge.tsx', 'export function Badge() {\n return <b />;\n}\n');
write('app/legacy.tsx', 'export function Legacy() {\n return <div />;\n}\n');
write('app/chart.tsx', 'export function Chart() {\n return <canvas />;\n}\n');
write(
'app/Card.tsx',
`import { Footer } from './footer';
import { Fallback } from './fallback';
import { Badge } from './badge';
export function Card({ parts, Slot = Fallback }) {
const { Footer: CustomFooter } = parts;
return (
<div>
<Footer />
<Fallback />
<Slot />
</div>
);
}
export function Tag({ label }: { label: string, badge: Badge }) {
return <Badge>{label}</Badge>;
}
export function Note(props: { badge: Badge }) {
return <Badge />;
}
export function Shell({ render }) {
return render({ Footer }) ?? <Footer />;
}
`
);
// A destructured require or import binds the module's own export.
write(
'app/legacy-page.jsx',
`function LegacyPage() {
const { Legacy } = require('./legacy');
return <Legacy />;
}
async function renderChart() {
const { Chart } = await import('./chart');
return <Chart />;
}
module.exports = { LegacyPage, renderChart };
`
);
await index();
expect(renders('Card')).toEqual(['Fallback app/fallback.tsx:1', 'Footer app/footer.tsx:1']);
expect(renders('Tag')).toEqual(['Badge app/badge.tsx:1']);
expect(renders('Note')).toEqual(['Badge app/badge.tsx:1']);
expect(renders('Shell')).toEqual(['Footer app/footer.tsx:1']);
expect(renders('LegacyPage')).toEqual(['Legacy app/legacy.tsx:1']);
expect(renders('renderChart')).toEqual(['Chart app/chart.tsx:1']);
});
});
+256 -30
View File
@@ -1930,6 +1930,221 @@ function declaredInside(ctx: ResolutionContext, name: string, parent: Node, chil
return ctx.getNodesByName(name).find((n) => JSX_CHILD_KINDS.has(n.kind) && inside(n));
}
/**
* What a function's span holds: each tag's name and the line of its first tag
* (0 while only type argument or parameter lists name it, `opensTag`), the
* names it also writes outside a tag (`writtenBare`), its code, and whether
* that code destructures a name by a line (`destructuredBy`).
*/
type SpanTags = { tags: Map<string, number>; bare: Set<string>; src: string; destructured: Map<string, boolean> };
/** The tags `src`, a function's lines from `startLine`, holds. */
function spanTags(src: string, startLine: number): SpanTags {
const names: SpanTags = { tags: new Map<string, number>(), bare: new Set<string>(), src, destructured: new Map<string, boolean>() };
if (src.includes('</') || src.includes('/>')) {
let line = startLine;
let counted = 0;
JSX_TAG_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = JSX_TAG_RE.exec(src))) {
for (let nl = src.indexOf('\n', counted); nl !== -1 && nl < m.index; nl = src.indexOf('\n', nl + 1)) line++;
counted = m.index;
if (!names.tags.get(m[1]!)) names.tags.set(m[1]!, opensTag(src, m.index, m[1]!) ? line : 0);
}
for (const [name, first] of names.tags) if (first && writtenBare(src, name)) names.bare.add(name);
}
return names;
}
/**
* Whether `fn`'s code (`names`, its span) binds `name` by `line`: a `const`,
* a parameter (`jsCodeBindsName`) or a destructured name (`destructures`).
* Only a name it also writes outside a tag can be bound there.
*/
function scopeBinds(names: SpanTags, fn: Node, name: string, line: number, file: string, ctx: ResolutionContext): boolean {
return names.bare.has(name) &&
(jsCodeBindsName(name, fn, file, line, ctx) || destructuredBy(names, name, line - fn.startLine));
}
/** The functions among `parents` whose lines hold `fn`'s, innermost first. */
function functionsAround(parents: Node[], fn: Node): Node[] {
const around: Node[] = [];
for (const n of parents) {
if (n.startLine <= fn.startLine && n.endLine >= fn.endLine &&
(n.startLine !== fn.startLine || n.endLine !== fn.endLine)) around.push(n);
}
return around.sort((a, b) => b.startLine - a.startLine || a.endLine - b.endLine);
}
/** Whether a span's code destructures `name` through its `index`th line (`destructures`), once per span, line and name. */
function destructuredBy(names: SpanTags, name: string, index: number): boolean {
const key = `${index}\0${name}`;
let binds = names.destructured.get(key);
if (binds === undefined) names.destructured.set(key, (binds = destructures(names.src, name, lineEnd(names.src, index))));
return binds;
}
/** Where the `index`th line of `src`, counted from 0, ends. */
function lineEnd(src: string, index: number): number {
let at = -1;
for (let i = 0; i <= index; i++) {
at = src.indexOf('\n', at + 1);
if (at === -1) return src.length;
}
return at;
}
const ID_CHAR = /[\w$]/;
const SPACE = /\s/;
/** How far a bracket walk reads: no pattern or parameter list runs longer. */
const PATTERN_REACH = 10_000;
const CONTROL_HEADS = new Set(['if', 'while', 'for', 'switch', 'with']);
/** After a parameter list: a return type on its line, then `=>` or a body. */
const PARAMETERS_END = /\s*(?::[^=;{}()\n]*)?(?:=>|\{)/y;
/** After a declaration's pattern: its type, then a `require(…)` or `import(…)` value. */
const IMPORTED_VALUE = /\s*(?::[^=;]*)?=\s*(?:await\s+)?(?:require|import)\s*\(/y;
/**
* Whether `src`, before `end`, binds `name` in a destructuring pattern: a
* declaration's (excalidraw's example app: `const { Sidebar, Footer,
* WelcomeScreen } = excalidrawLib`; `const [Panel] = useState(…)`) or a
* parameter's (refine's `({ children, Header, Footer, Sider }) =>`, `({ icon:
* Icon }) =>`), nested patterns included. A key a pattern renames binds
* nothing (`{ footer: Footer }` binds `Footer`), nor does a default value
* (`{ Icon = DefaultIcon }`) or a type literal after the pattern (`}: {
* footer: Footer }`), and a declaration of `require(…)` or `import(…)` binds
* the module's own export, which links as an import does. `src` is code
* (`jsxCode`), so a bracket in a string is blank.
*/
function destructures(src: string, name: string, end: number): boolean {
for (let at = src.indexOf(name); at !== -1 && at < end; at = src.indexOf(name, at + name.length)) {
if (bindingElementAt(src, at, name.length)) return true;
}
return false;
}
/**
* Whether the name at `at` is an element of a pattern that binds it — `{
* Name }`, `{ ...Name }`, `{ key: Name }`, `[Name]` — followed by the next
* element, the pattern's end or a default.
*/
function bindingElementAt(code: string, at: number, length: number): boolean {
if (ID_CHAR.test(code[at - 1] ?? '') || ID_CHAR.test(code[at + length] ?? '')) return false;
const next = skipSpace(code, at + length, 1);
const after = code[next];
if (after !== ',' && after !== '}' && after !== ']' &&
(after !== '=' || code[next + 1] === '=' || code[next + 1] === '>')) return false;
let prev = skipSpace(code, at - 1, -1);
// A rest element; any other `.` reads a member.
if (code[prev] === '.') {
if (code[prev - 1] !== '.' || code[prev - 2] !== '.') return false;
prev = skipSpace(code, prev - 3, -1);
}
let opener = prev;
if (code[prev] === ',') {
opener = enclosingOpener(code, prev - 1);
} else if (code[prev] === ':') {
// A rename's target: its key opens an element of an object pattern.
if (!keyOpensElement(code, prev)) return false;
opener = enclosingOpener(code, prev - 1);
if (code[opener] !== '{') return false;
}
return (code[opener] === '{' || code[opener] === '[') && bindsPattern(code, opener, 0);
}
/**
* Whether the `{` or `[` at `opener` opens a pattern that binds its names: a
* declaration's, unless it declares an import, or a parameter's — itself or
* through the patterns it nests in.
*/
function bindsPattern(code: string, opener: number, depth: number): boolean {
if (depth > 8) return false;
const p = skipSpace(code, opener - 1, -1);
const c = code[p];
if (c === '(') return opensParameters(code, p);
if (c === '[') return bindsPattern(code, p, depth + 1);
if (c === ',') {
const q = enclosingOpener(code, p - 1);
if (code[q] === '(') return opensParameters(code, q);
return code[q] === '[' && bindsPattern(code, q, depth + 1);
}
if (c === ':') {
// `{ key: { … } }` nests a pattern; `props: {` and `}: {` annotate a type.
if (!keyOpensElement(code, p)) return false;
const q = enclosingOpener(code, p - 1);
return code[q] === '{' && bindsPattern(code, q, depth + 1);
}
if (!ID_CHAR.test(c ?? '')) return false;
const word = wordEndingAt(code, p);
if (word !== 'const' && word !== 'let' && word !== 'var') return false;
const close = matchingCloser(code, opener);
if (close < 0) return false;
IMPORTED_VALUE.lastIndex = close + 1;
return !IMPORTED_VALUE.test(code);
}
/**
* Whether the `(` at `paren` opens a parameter list: an arrow or a body
* follows it, and no control-flow keyword precedes it (`if ([a, B].includes(x)) {`).
*/
function opensParameters(code: string, paren: number): boolean {
const p = skipSpace(code, paren - 1, -1);
if (ID_CHAR.test(code[p] ?? '') && CONTROL_HEADS.has(wordEndingAt(code, p))) return false;
const close = matchingCloser(code, paren);
if (close < 0) return false;
PARAMETERS_END.lastIndex = close + 1;
return PARAMETERS_END.test(code);
}
/** Whether a property key ends at the `:` at `colon` and opens its element: `{ key:`, `, 'key':`, `, [key]:`. */
function keyOpensElement(code: string, colon: number): boolean {
let k = skipSpace(code, colon - 1, -1);
const c = code[k];
if (c === ']') k = enclosingOpener(code, k - 1) - 1;
else if (c === '"' || c === "'") k = code.lastIndexOf(c, k - 1) - 1;
else if (ID_CHAR.test(c ?? '')) while (k >= 0 && ID_CHAR.test(code[k]!)) k--;
else return false;
const b = code[skipSpace(code, k, -1)];
return b === '{' || b === ',';
}
/** The `(`, `[` or `{` around `from`, past balanced brackets; -1 when none is near. */
function enclosingOpener(code: string, from: number): number {
let depth = 0;
for (let i = from, stop = Math.max(0, from - PATTERN_REACH); i >= stop; i--) {
const c = code[i];
if (c === ')' || c === ']' || c === '}') depth++;
else if (c === '(' || c === '[' || c === '{') {
if (depth === 0) return i;
depth--;
}
}
return -1;
}
/** The bracket that closes the one at `open`; -1 when none is near. */
function matchingCloser(code: string, open: number): number {
let depth = 0;
for (let i = open, stop = Math.min(code.length, open + PATTERN_REACH); i < stop; i++) {
const c = code[i];
if (c === '(' || c === '[' || c === '{') depth++;
else if ((c === ')' || c === ']' || c === '}') && --depth === 0) return i;
}
return -1;
}
/** The identifier that ends at `end`. */
function wordEndingAt(code: string, end: number): string {
let start = end;
while (start > 0 && ID_CHAR.test(code[start - 1]!)) start--;
return code.slice(start, end + 1);
}
function skipSpace(code: string, at: number, step: 1 | -1): number {
while (at >= 0 && at < code.length && SPACE.test(code[at]!)) at += step;
return at;
}
/**
* Phase 5: React JSX child rendering. A component that returns `<Child .../>`
* mounts Child — React calls it — but JSX instantiation isn't a static call edge,
@@ -1942,12 +2157,17 @@ function declaredInside(ctx: ResolutionContext, name: string, parent: Node, chil
* `<Entry extends BaseEntity>(…) =>`; outline's `<PaginatedList<Document>`
* read its `Document` model class as a child), and the name must resolve to a
* component/function/class node, or to a value the file declares itself (see
* `jsxChild`). A name the parent binds itself by its first
* tag — a parameter, or a `const` like outline's `const Content = variant ===
* "dropdown" ? DropdownMenu.SubContent : ContextMenu.SubContent` — is that
* local, and renders only a component declared inside the parent
* (`declaredInside`). A destructured name still links by name, as a
* destructured function call does.
* `jsxChild`). A name the parent, or a function around it, binds by the
* parent's first tag — a parameter, or a `const` like outline's `const Content
* = variant === "dropdown" ? DropdownMenu.SubContent : ContextMenu.SubContent`
* — is that local, and renders only a component declared inside that function
* (`declaredInside`). So is a name it destructures (`destructures`): refine's
* `Layout({ children, Header, Footer, Sider })` rendered another example's
* `Footer`, and excalidraw's example app rendered the `Sidebar` of its own
* `ExampleSidebar.tsx` for the library's, out of `const { Sidebar, Footer } =
* excalidrawLib`. A destructured name keeps linking for a bare call — `const
* { t } = useI18n()` is that function more often than not — but a tag's is a
* component the props or a library hand in, which is no node here.
*/
async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield): Promise<Edge[]> {
let scannedFiles = 0;
@@ -1998,40 +2218,46 @@ async function reactJsxChildEdges(ctx: ResolutionContext, onYield: MaybeYield):
// file is split once, not once per function, and read as code once: a
// tag in a comment or a string is text (`jsxCode`).
let lines: string[] | null = null;
// Each name a span holds, and the line of its first tag: 0 while only type
// argument or parameter lists name it (`opensTag`). `bare` holds the
// names it also writes outside a tag (`writtenBare`).
const tagsBySpan = new Map<string, { tags: Map<string, number>; bare: Set<string> }>();
for (const parent of parents) {
if (!parent.startLine || !parent.endLine) continue;
const span = `${parent.startLine}:${parent.endLine}`;
// Each name a span holds (`spanTags`), read when a function among the
// parents is, or a function around one.
const tagsBySpan = new Map<string, SpanTags>();
const spanOf = (fn: Node): SpanTags => {
const span = `${fn.startLine}:${fn.endLine}`;
let names = tagsBySpan.get(span);
if (!names) {
names = { tags: new Map<string, number>(), bare: new Set<string>() };
lines ??= (jsxCode(content) ?? content).split('\n');
const src = lines.slice(parent.startLine - 1, parent.endLine).join('\n');
if (src.includes('</') || src.includes('/>')) {
let line = parent.startLine;
let counted = 0;
JSX_TAG_RE.lastIndex = 0;
let m: RegExpExecArray | null;
while ((m = JSX_TAG_RE.exec(src))) {
for (let nl = src.indexOf('\n', counted); nl !== -1 && nl < m.index; nl = src.indexOf('\n', nl + 1)) line++;
counted = m.index;
if (!names.tags.get(m[1]!)) names.tags.set(m[1]!, opensTag(src, m.index, m[1]!) ? line : 0);
}
for (const [name, first] of names.tags) if (first && writtenBare(src, name)) names.bare.add(name);
}
names = spanTags(lines.slice(fn.startLine - 1, fn.endLine).join('\n'), fn.startLine);
tagsBySpan.set(span, names);
}
return names;
};
for (const parent of parents) {
if (!parent.startLine || !parent.endLine) continue;
const names = spanOf(parent);
// The functions around the parent, innermost first, once a tag needs them.
let around: Node[] | null = null;
let added = 0;
for (const [name, line] of names.tags) {
if (added >= MAX_JSX_CHILDREN) break;
if (!line) continue;
let child = jsxChild(ctx, name, file, importsOf, ownValueOf, exportOf);
// A name the parent binds itself by its first tag is that local.
if (child && names.bare.has(name) && jsCodeBindsName(name, parent, file, line, ctx)) {
child = declaredInside(ctx, name, parent, child);
if (child) {
// A name the parent, or a function around it, binds by the parent's
// first tag is that local: excalidraw's example app destructures
// `Sidebar` and `Footer` once and renders them in nested `render…`
// functions too. It renders a component that function declares, if
// any (`declaredInside`).
let binder = scopeBinds(names, parent, name, line, file, ctx) ? parent : undefined;
if (!binder) {
around ??= functionsAround(parents, parent);
for (const fn of around) {
if (scopeBinds(spanOf(fn), fn, name, line, file, ctx)) {
binder = fn;
break;
}
}
}
if (binder) child = declaredInside(ctx, name, binder, child);
}
if (!child || child.id === parent.id || crossesCodeBoundary(parent.language, child.language)) continue;
const key = `${parent.id}>${child.id}`;