fix(react): a typed styled component is a component, not a constant (#2429)

`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>
This commit is contained in:
Colby Mchenry
2026-10-07 15:48:58 +00:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 7a79654157
commit 78f5c8893a
5 changed files with 193 additions and 1 deletions
+32
View File
@@ -103,6 +103,9 @@ impl<'t> Walker<'t> {
/// inner is the inline render function, or None for `styled.x`/`memo(Ref)`.
/// Outer None = not a component wrapper.
fn react_component_hoc(&self, value: Node<'t>) -> Option<Option<Node<'t>>> {
if self.is_typed_styled_template(value) {
return Some(None);
}
if value.kind() != "call_expression" {
return None;
}
@@ -128,6 +131,35 @@ impl<'t> Walker<'t> {
Some(inner)
}
/// A typed styled tag, `styled.div<Props>` before a template literal, which
/// parses as comparisons: `(styled.div < Props) > template`. The leftmost
/// operand is the `styled` tag against a `<`; the rightmost is the
/// template_string against a `>` (`>>` when the type argument ends in its
/// own `<…>`). Mirrors TreeSitterExtractor.isTypedStyledTemplate.
fn is_typed_styled_template(&self, value: Node<'t>) -> bool {
if value.kind() != "binary_expression" {
return false;
}
let (mut tag_parent, mut tag) = (value, value.child_by_field_name("left"));
while let Some(t) = tag.filter(|t| t.kind() == "binary_expression") {
(tag_parent, tag) = (t, t.child_by_field_name("left"));
}
let (mut template_parent, mut template) = (value, value.child_by_field_name("right"));
while let Some(t) = template.filter(|t| t.kind() == "binary_expression") {
(template_parent, template) = (t, t.child_by_field_name("right"));
}
let (Some(tag), Some(template)) = (tag, template) else {
return false;
};
let operator = |n: Node<'t>| n.child_by_field_name("operator").map(|o| o.kind()).unwrap_or("");
let closing = operator(template_parent);
template.kind() == "template_string"
&& operator(tag_parent) == "<"
&& !closing.is_empty()
&& closing.bytes().all(|b| b == b'>')
&& util::styled_callee().is_match(self.text(tag))
}
fn extract_react_component_node(
&mut self,
name: &str,