Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
167 changes: 155 additions & 12 deletions __tests__/jsx-child-disambiguation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']);
});
});

Expand Down
Loading