From 1990b02b0056e99f8075ecd6dc3efec3832fee52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sinan=20G=C3=BCm=C3=BC=C5=9Fkabak?= <256841712+sinangumuskabak-sys@users.noreply.github.com> Date: Sat, 26 Sep 2026 17:58:15 +0300 Subject: [PATCH] fix(extract): skip JSX member tags With a lowercase receiver, and 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 aa4be46dace8a329bc8947e6e8517c7534fa9739) --- graphify/extractors/engine.py | 18 ++++++++---------- tests/test_jsx_component_usage_calls.py | 23 +++++++++++++++++++++-- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/graphify/extractors/engine.py b/graphify/extractors/engine.py index 7470bbb29..1b5484f60 100644 --- a/graphify/extractors/engine.py +++ b/graphify/extractors/engine.py @@ -6459,18 +6459,16 @@ def _extract_generic( # `` / `` renders Comp; the tag is the `name` field. # Same rule as the JSX transform: a tag starting with a lowercase # letter (`
`) 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 (`` is not a - # local `div`). Fragments and namespaced tags (``) are - # skipped. + # so it must not bind by name to a same-named function. + # Only bare identifiers are handled. Member tags (``, + # ``, ``) 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 (``) 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": diff --git a/tests/test_jsx_component_usage_calls.py b/tests/test_jsx_component_usage_calls.py index a6c563ba2..9d8083c28 100644 --- a/tests/test_jsx_component_usage_calls.py +++ b/tests/test_jsx_component_usage_calls.py @@ -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 (`
`) is an intrinsic -DOM element and must not bind to a same-named function; for a member tag the last -segment decides (``). +DOM element and must not bind to a same-named function. Member tags +(``, ``) 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): + # `` / `` 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
;\n" + "}\n" + ), + }) + assert ("App()", "Close()") not in calls + assert ("App()", "Comp()") not in calls