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