diff --git a/CHANGELOG.md b/CHANGELOG.md index f3388d034..ccd5890c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -142,6 +142,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - `codegraph sync` and the file watcher now link a lazily loaded React Router route to its page when the page appears, or gains the export the route reads, in a folder below the one the route loads, like `src/pages/Home/index.tsx` behind `import('@/pages')`. Before, such a route kept linking nothing until the folder's own `index.ts` changed or the project was re-indexed, although a fresh index linked it. - 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 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. - `codegraph_explore`'s Flow now goes through the implementation your query names when an interface method has several that lead to the same place. Before, it took whichever one came first in the index: asking about prometheus's `Engine.execEvalStmt Queryable.Querier fanout.Querier NewMergeQuerier` traced the call through the TSDB's `DB.Querier` and left the `fanout.Querier` you named off the Flow. Any two routes of the same length are now settled this way, in favor of the one that passes through more of the symbols you named. - In C and C++, a struct, class, union or enum defined in a variable declaration, like `struct Foo { … } foo;`, is now indexed with its methods and enum values, next to the variable. Before, at most the variable was, so the type was missing from search and `codegraph_explore`, a class deriving from it had no base, and calls to its methods linked to nothing: in redis, the Sentinel's `sentinelState` and `redis-cli`'s `config` struct were missing. A struct, union or enum with no name of its own now takes the name of the variable it declares, so redis's `static struct { … } SPT;` is found as `SPT`, and one declared inside a function is named after its variable instead of ``. The comment above the declaration becomes the type's description. Re-index C and C++ projects after upgrading. diff --git a/__tests__/jsx-child-disambiguation.test.ts b/__tests__/jsx-child-disambiguation.test.ts index 3a718b9a1..a5d669e7b 100644 --- a/__tests__/jsx-child-disambiguation.test.ts +++ b/__tests__/jsx-child-disambiguation.test.ts @@ -3,6 +3,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { CodeGraph } from '../src'; +import { jsxCode } from '../src/resolution/jsx-code'; /** * A JSX tag names ONE component, and the file it is written in says which: @@ -1002,3 +1003,232 @@ export default React.forwardRef(Button); expect(renders('Deck')).toEqual(['Card components/Card.js:3']); }); }); + +/** + * The pass reads tag names off a parent's source with a pattern, so it also + * read the tags that text only mentions. react-router's hooks rendered the + * `Router`, `Routes` and `Route` their invariant messages and comments name + * ("You cannot render a inside another "), shadcn's + * `useCarousel` the `Carousel` its error names, and a layout the `` + * commented out of it. A tag in a comment, or in a string, template, regex or + * attribute value, is text; so is anything in a TypeScript `.ts` file, which + * holds no JSX at all. + */ +describe('JSX child: a tag written in a comment or a string', () => { + let dir: string; + let cg: any; + + beforeEach(() => { + dir = fs.mkdtempSync(path.join(os.tmpdir(), 'jsx-text-')); + fs.writeFileSync(path.join(dir, 'package.json'), '{"dependencies":{"react":"^18.0.0"}}'); + }); + + afterEach(() => { + cg?.close?.(); + fs.rmSync(dir, { recursive: true, force: true }); + }); + + const write = (rel: string, lines: string[]) => { + const p = path.join(dir, rel); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, lines.join('\n') + '\n'); + }; + const component = (name: string, tag = 'div') => [`export function ${name}() {`, ` return <${tag} />;`, '}']; + + async function index(): Promise { + cg = await CodeGraph.init(dir, { silent: true }); + await cg.indexAll(); + } + + /** The names a jsx-render edge out of `parent` points at. */ + const renders = (parent: string): string[] => + cg.db.db + .prepare( + `SELECT t.name AS r FROM edges e + JOIN nodes s ON s.id = e.source + JOIN nodes t ON t.id = e.target + WHERE s.name = ? AND json_extract(e.metadata, '$.synthesizedBy') = 'jsx-render' + ORDER BY r` + ) + .all(parent) + .map((row: any) => row.r); + + it('renders nothing for a tag in a comment, a string, a template or a regex', async () => { + write('router/components.tsx', ['Router', 'Routes', 'Route', 'Outlet', 'Blog'].flatMap((n) => component(n))); + write('router/hooks.tsx', [ + "import { Outlet } from './components';", + '', + 'export function useRoutesImpl(routes, parentPath) {', + ' invariant(', + ' useInRouterContext(),', + ' `useRoutes() may be used only in the context of a component.`', + ' );', + " // You won't get a warning about 2 different under a :", + ' //', + ' // } />', + ' warning(', + ' !parentPath,', + ' `You rendered descendant (under ) ` +', + ' "but the parent route path has no trailing \\"*\\"."', + ' );', + ' const selfClosing = //;', + ' return ;', + '}', + ]); + // shadcn's carousel hook, and excalidraw's docs theme toggle. + write('components/ui/carousel.tsx', [ + "import * as React from 'react';", + 'const CarouselContext = React.createContext(null);', + 'export function useCarousel() {', + ' const context = React.useContext(CarouselContext);', + " if (!context) throw new Error('useCarousel must be used within a ');", + ' return context;', + '}', + 'export function Carousel({ children }) {', + ' return {children};', + '}', + ]); + write('docs/Excalidraw.tsx', component('Excalidraw')); + write('docs/ToggleTheme.tsx', [ + 'export function ToggleTheme({ onThemeChange }) {', + ' if (!onThemeChange) {', + ' throw new Error("` props.onThemeChange` must be defined when `theme` is set");', + ' }', + ' return