fix(extract): skip JSX member tags

With a lowercase receiver, <icons.Close /> and <props.Comp /> resolved by the
property name and bound to unrelated local Close() / Comp() functions. None
of the member tags tried produced a correct edge, so only bare identifiers
are handled now.

(cherry picked from commit aa4be46dac)
This commit is contained in:
Sinan Gümüşkabak authored and safishamsi committed 2026-09-27 18:39:52 +01:00
1 parent a3559c2541
commit 1990b02b00
2 files changed
+29 -12

No files matched your search

+8 -10
View File
@@ -6459,18 +6459,16 @@ def _extract_generic(
# `<Comp />` / `<Comp>` renders Comp; the tag is the `name` field.
# Same rule as the JSX transform: a tag starting with a lowercase
# letter (`<div>`) is an intrinsic element, not a symbol in scope,
# so it must not bind by name to a same-named function. For a
# member tag the last segment decides (`<motion.div>` is not a
# local `div`). Fragments and namespaced tags (`<svg:rect>`) are
# skipped.
# so it must not bind by name to a same-named function.
# Only bare identifiers are handled. Member tags (`<icons.Close>`,
# `<props.Comp>`, `<Ctx.Provider>`) are skipped: with a lowercase
# receiver the member path falls back to the bare property name and
# binds to an unrelated local function. Fragments and namespaced
# tags (`<svg:rect>`) have no identifier name either.
func_node = node.child_by_field_name("name")
tag = func_node
if tag is not None and tag.type == "member_expression":
tag = tag.child_by_field_name("property")
if (func_node is None
or func_node.type not in ("identifier", "member_expression")
or tag is None
or _read_text(tag, source)[:1].islower()):
or func_node.type != "identifier"
or _read_text(func_node, source)[:1].islower()):
func_node = None
if func_node:
if func_node.type == "identifier":
+21 -2
View File
@@ -5,8 +5,9 @@ A JSX element renders its component, but tree-sitter models it as
field, not as a `call_expression`. Those node types were never in `call_types`,
so a component that is only ever rendered (the normal case in React) had no
incoming edge and looked unused. A lowercase bare tag (`<div>`) is an intrinsic
DOM element and must not bind to a same-named function; for a member tag the last
segment decides (`<motion.div>`).
DOM element and must not bind to a same-named function. Member tags
(`<icons.Close>`, `<motion.div>`) are not handled: resolving them by the property
name binds to unrelated local functions.
"""
from __future__ import annotations
@@ -120,3 +121,21 @@ def test_member_tag_does_not_bind_to_local_function_by_last_segment(tmp_path: Pa
),
})
assert ("App()", "Provider()") not in calls
def test_member_tag_with_lowercase_receiver_does_not_bind_to_local_function(tmp_path: Path):
# `<icons.Close>` / `<props.Comp>` are members, not the local Close() / Comp().
calls, _ = _calls(tmp_path, {
"icons.tsx": "export function X() { return null; }\n",
"app.tsx": (
"import * as icons from './icons';\n"
"function Close() { return null; }\n"
"function Comp() { return null; }\n"
"export function App(props: any) {\n"
" return <div><icons.Close /><props.Comp /></div>;\n"
"}\n"
),
})
assert ("App()", "Close()") not in calls
assert ("App()", "Comp()") not in calls