Repository navigation
fix(react): a tag written in a comment or a string renders no JSX child - #2564
Merged
Merged
Conversation
The jsx-render pass read tag names off each parent's raw lines, so text
that only mentions a tag rendered it: react-router's hooks rendered the
`Router`, `Routes` and `Route` their JSDoc, comments and invariant
messages name ("You cannot render a <Router> inside another <Router>"),
shadcn's `useCarousel` the `Carousel` its error names, and layouts the
markup commented out of them (`{/* <Progress /> */}`).
The pass now scans a code-only view of each file (`jsxCode`, new in
src/resolution/jsx-code.ts): comments and the contents of string,
template and regex literals and of JSX attribute strings are blanked,
offsets and line breaks kept. The lexer follows JSX elements, so a quote,
slash or backtick in JSX text (`Don't`, `https://…`) stays text, and it
re-reads a `<` that opens a type parameter list (`<T,>(x: T) =>`) as an
operator. A file it can't read as one program (an unbalanced brace, a
string open at a line's end, a mismatched closing tag) is scanned as it
is, as before. TypeScript `.ts` files are skipped outright: they can't
hold JSX.
Checked against TypeScript's parser on 36 React repos (7,649 files,
61,668 `<Name` matches): no real tag blanked, no file given up on, 612
non-JSX matches removed. Graph A/B on 12 repos: -36 jsx-render edges,
+0, nothing else changed; the TypeScript oracle grades all 36 as tags in
comments or strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…X child Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jsxCode made its helpers as closures on every call. Each file's closures were new call targets to V8's optimizer, which threw the function's optimized code away and compiled it again for file after file, at 40 to 90 ms a time: 15 times over excalidraw, about 0.9 s of CPU. The state now lives in a class whose methods every file shares, the pure helpers are module functions, and the main loop is split by mode. Output is identical to the closure version on all 9,482 candidate files of the 68 validation repos; excalidraw's jsxEdges pass is back to within the lexer's own ~0.1 s of main. 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.
Summary
The
jsx-rendersynthesizer (reactJsxChildEdges) read tag names off each parent function's raw lines withJSX_TAG_RE, so text that only mentions a tag rendered it:Router,RoutesandRoutetheir JSDoc, line comments and invariant messages name ("You cannot render a<Router>inside another<Router>");useCarouselrendered theCarouselits error names ("useCarousel must be used within a<Carousel />");{/* <Progress /> */});opensearchResponse, in a.tsfile, rendered the<Image>and<Description>of its XML template.Main still scanned raw text: no merged or open PR covers this. #2442 and #2476 handle type arguments, and #2475, #2420 and #2563 handle what a tag binds to.
What changed
src/resolution/jsx-code.ts(new):jsxCode(source)returns the file with comments blanked, along with the contents of string, template and regex literals and of JSX attribute strings. Offsets and line breaks are kept, so the scan's line counting is unchanged.<p>Don't have an account? <Link to="/signup">,https://…).<that opens a type parameter list where an expression can start (<T,>(x: T) =>,type Fn = <T>(x: T) => T,{ <T>(x: T): T }) is read again as an operator once it fails to read as an element.nulland the pass scans the raw text as before. That covers a string still open at the end of its line, braces that don't balance, a closing tag naming another element, a</or/>among code, or the re-read or step budget running out.reactJsxChildEdges:jsxCode(content) ?? content, still split once per file and memoized per span..tsfiles, which can't hold JSX. There a<Name>is a type assertion or text.Validation
Occurrence check against TypeScript's parser
For each
<Namethe pass's own pattern (main's, with #2476's lookahead) finds in the raw source, does TypeScript parse a JSX element whose<is there, and does the pattern still match overjsxCode's output?The check ran on 36 React repos: 7,649 JSX-capable files holding
</or/>, with 62,377 matches.<T extends …>,styled(X)<Props>,<Item = MenuItem,>), which aren't comments or strings and stay withopensTag..tsfiles: 322 with</or/>, 0 given up on.graphiql.min.js, qwik, hugo, bootstrap 4 and several RN/Expo libraries.Graph A/B
Fresh indexes per arm, compared through
scripts/dump-graph.mjs, main3f714bccvs this branch:Nodes, refs, files and every other edge are unchanged. A TypeScript-checker oracle graded all 36 removed edges "no JSX tag of that name in the parent (comment/string)". The same 36 edges came out on three bases (
883504d2,cd97bb37and3f714bcc).Examples of removed edges:
useRoutesImpl→Route/Routes/Router/Outlet/Blog(comment examples, template invariant messages)createRouter→Form/ScrollRestoration(JSDoc inrouter.ts)getChildren→Route/RoutesuseCarousel→CarouselanduseChart→ChartContainer, from error stringsShipping/PaymentContainer→RadioAuthenticatedLayout→ThemedLayoutToggleTheme→ dev-docs'Excalidraw, from an error string holding backticksgetLatestValue, in a.tsfileLayout→ProgressTeamMembers→AvatarImageInventory→MatchingPartsMemoizedEditorSidebar→ threeNew…ButtonsSearchedEventsList→EventExcerptTests
jsx-child-disambiguation.test.ts:.tstemplate, plus a.tstype assertion in a file the lexer gives up on;jsxCodeunit cases..tsgate fails the.tscase.tscandnpm run buildpass.Cost
jsxCodetakes about 16 ms on go-ethereum's 980 KBgraphiql.min.js(the same with--no-regexp-optimization) and about 5–7 ms on excalidraw's 381 KBApp.tsx. The per-span memo is untouched, andjsx-render-work.test.tspasses.jsxEdgespass CPU, median of 3 interleaved runs: excalidraw 501 → 656 ms, outline 1376 → 1360 ms (within noise).Not in this PR
importedTagModules, sync's list of the modules a file's tags are read through, still scans raw text. A tag in a comment there only adds a spare read dependency, never an edge.if (…)reads as a division to the lexer, so that file falls back to the raw scan. That happened in 0 of 9,482 files.Overlap
const languageline.🤖 Generated with Claude Code