Repository navigation
fix(react): a typed styled component is a component, not a constant - #2429
Merged
Merged
Conversation
`const Wrapper = styled.div<WrapperProps>` + a template literal came out as a
`constant`, so the JSX-render synthesizer skipped it: `<Wrapper>` got no
render edge, or bound to a same-named component in another file.
tree-sitter's tagged-template call takes no type arguments, so when the type
argument also reads as an expression the initializer parses as comparisons,
`(styled.div < WrapperProps) > template`, not as a call. (A type argument
that can't be an expression, `<{ open: boolean }>`, error-recovers into a
call on the tag, which #841 already took.)
reactComponentHoc now also takes that comparison chain: the leftmost operand
is the `styled` tag against a `<`, the rightmost is the template against a
`>` (`>>` when the type argument ends in its own `<...>`), and operators in
between belong to the type argument (`<A & B>`). Mirrored in the kernel's
TS/JS walker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-156108 # Conflicts: # CHANGELOG.md
…-156108 # Conflicts: # CHANGELOG.md
…-156108 # Conflicts: # CHANGELOG.md
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
const Wrapper = styled.div<WrapperProps>+ a template literal was indexed as aconstant, whilestyled.div+ a template andstyled.div<{ open: boolean }>+ a template were alreadycomponents (#841). A constant is skipped by the JSX-render synthesizer, so<Wrapper>got no render edge, or got bound to a same-named component in another file. This PR recognizes the typed form as a component too, in both the TS extractor and the kernel.Why it happened
tree-sitter-typescript's tagged-template call (a
call_expressionwhoseargumentsis atemplate_string) has notype_argumentsfield. When the type argument also reads as an expression, the initializer parses as comparisons instead (same tree on the tsx and typescript grammars, and no parse error):A type argument that can't be an expression (
<{ open: boolean }>in most contexts,<{ a?: T }>,<keyof T>) error-recovers intocall_expression(non_null_expression(instantiation_expression …) MISSING "!"). Its callee text starts withstyled, which is why that form already worked. The recovery makes the file erroring, so the kernel defers it to wasm.Change
reactComponentHoc(src/extraction/tree-sitter.ts) andreact_component_hoc(codegraph-kernel/src/tsjs/extractors.rs) also take that comparison chain:styled…tag, against a<;>(>>/>>>when the type argument ends in its own<…>);The PascalCase gate is unchanged.
css<P>,createGlobalStyle<P>,keyframes<P>,styled.length < LIMIT > 2and lowercase consts stay constants.Validation
Tests
react-hoc-component.test.ts: 3 new tests..attrs(…)<P>,.withConfig().attrs<P>()<P>,<A | B>,<A & {…}>and<Partial<…>>, are components.<Wrapper>gets jsx-render callers from its own file and from an importer.kernel-tsjs-parity.test.ts: kernel == wasm for these shapes, in ts and tsx.Full suite: the parallel run on a saturated box (100% CPU, 70 to 200 node processes) failed in 47 files, every failure a 5 s/10 s timeout or
EBUSY. Rerun serially with long timeouts, those files passed except 14 tests in 7 daemon/MCP-lifecycle, writer-lock, query-pool and worktree files. Those 7 files were then run back to back on main's source (61f8b09) and on this branch under the same load. main failed 4. This branch failed 3, all of which also fail on main:daemon-pid-reuse(EBUSY), the #1820 function-ref case (60 s timeout) andmcp-writer-lock. None of these touch TSX extraction.Kernel parity (
scripts/kernel-parity.mjs): 0 diffs on every sweep, and the deferral counts are identical to main's.src/+__tests__/A/B on real repos. Each arm ran a fresh
codegraph init -yon the kernel path: main 9a1d4bd (with #2442 and #2433) against this branch. Dumps come fromscripts/dump-graph.mjs. Edges are compared by kind-free endpoint labels, because a node id embeds its kind and every edge touching a reclassified node is re-keyed.failedfrontend/src)failedEvery changed jsx-render edge was checked against the source. The tag must sit inside the rendering function, and the target must be the file's own module-level declaration of the name or what its import names (tsconfig aliases,
baseUrlpaths, index files and one re-export hop followed).New callers on outline:
NudeButton22,HStack18,VStack8,Key2, and one each forRealButton,Main,GroupWrapper,FieldSet,ContentsContainer,CloseActionandAvatarPresence.Corrected on outline:
<Text>tags imported from@shared/components/Texthad been bound toEditableTitle.tsx's localText.<Emoji>tags had been bound to theEmojimodel class.<Wrapper>,<Background>,<Container>,<Content>,<Title>,<StyledContent>and<EditorContainer>in files that declare them had gone to another file's namesake.<MenuButton>and theEditorContainerimported from@shared/editor/components/Styleshad gone to the wrong file.The first run, on main 61f8b09, gave the same numbers plus 3 edges that moved between two wrong targets: a function-local
const Content = …alias inMenu/index.tsx(2) and the type argument inSuggestionsMenuProps<Emoji>(1). #2442 has since removed those edges from both arms.Dead code: the report is identical on outline (47) and SigNoz (10). The reclassified consts become candidates, and every one of them is excluded as exported or mentioned.
Known side effects
s()andborderRadius(); 4 function refs to the wrapped base, such asNudeButton → ActionButton; 6 value refs to CSS mixin constants; 2 exact-match calls and 1 import ref.styled(...)components (about 776 on outline) never had these edges.<Modal>tags imported fromantd, plus 1 renamed default import ofcomponents/Modal.tsx, now bind to the project's onlyModal(styled(ModalComponent)<Props>).jsxChildreturns a lone namesake without checking that the file imports the name from a package.<Button>tags bound to an untypedstyled(Link)namedButton.<Link>from@mui/material.EmojiMenu.tsx'sSuggestionsMenuProps<Emoji>names that file's owntype Emoji. It now resolves through React Pattern 1's component-directory preference to the newEmojicomponent, where exact-match had bound it to the local type alias. Pattern 1 never looks at the referencing file's own type declarations; on main only the ambiguity guard hid this. Follow-up filed.Overlap
#2420 (open) changes what the jsx-render synthesizer and Pattern 1 bind a name to: the referencing file's own value declarations. It doesn't touch extraction, so there is no textual overlap. Once both land, a typed styled const is a component the file declares itself. Whichever lands second should re-run the outline A/B.
🤖 Generated with Claude Code