From 58efcebd83df56ea2ae3ea14d556f0eb2937fb70 Mon Sep 17 00:00:00 2001 From: Skyler Southern <274284809+PowPickles@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:14:28 -0400 Subject: [PATCH] fix: resolve_seed matches bare names against ()-decorated callable labels Callable nodes are labeled with a trailing "()" (e.g. classifyProperty()), so a bare-name query like "classifyProperty" falls through the exact-label pass and then ties with any prefix sibling (classifyPropertySafe()) in the contains pass, returning "No unique node match" even though exactly one callable with that name exists. Add a bare-name pass between exact-label and exact-source matching that compares query and label with the trailing "()" stripped from each. Bare queries now resolve decorated labels and vice versa; genuine duplicates still return None. Co-Authored-By: Claude Fable 5 --- graphify/affected.py | 17 +++++++++++++++++ tests/test_affected_cli.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/graphify/affected.py b/graphify/affected.py index 10c63187..80bdc204 100644 --- a/graphify/affected.py +++ b/graphify/affected.py @@ -43,6 +43,12 @@ def _format_location(data: dict) -> str: return str(source_file) +def _bare_name(label: str) -> str: + """Lowercased label with the callable decoration (trailing "()") removed.""" + label = label.lower() + return label[:-2] if label.endswith("()") else label + + def resolve_seed(graph: nx.Graph, query: str) -> str | None: if query in graph: return query @@ -54,6 +60,17 @@ def resolve_seed(graph: nx.Graph, query: str) -> str | None: ] if len(exact_label_matches) == 1: return exact_label_matches[0] + # Callable labels are decorated ("name()"), so a bare "name" query falls + # through exact matching and then ties with any "name*" sibling in the + # contains pass. Match on the undecorated name before giving up. + query_bare = _bare_name(query_lower) + bare_name_matches = [ + str(node_id) + for node_id, data in graph.nodes(data=True) + if _bare_name(str(data.get("label", ""))) == query_bare + ] + if len(bare_name_matches) == 1: + return bare_name_matches[0] exact_source_matches = [ str(node_id) for node_id, data in graph.nodes(data=True) diff --git a/tests/test_affected_cli.py b/tests/test_affected_cli.py index f2a4e554..40e0d68e 100644 --- a/tests/test_affected_cli.py +++ b/tests/test_affected_cli.py @@ -92,3 +92,34 @@ def test_affected_cli_forces_directed_on_undirected_graph(monkeypatch, tmp_path, assert "calls" in out # B is the query node, not an affected node, and the result is not empty. assert "No affected nodes found." not in out + + +def test_resolve_seed_bare_name_matches_callable_label(): + from graphify.affected import resolve_seed + + graph = nx.DiGraph() + graph.add_node("a", label="classifyProperty()", source_file="pkg/entity.py") + graph.add_node("b", label="classifyPropertySafe()", source_file="app/context.py") + + assert resolve_seed(graph, "classifyProperty") == "a" + assert resolve_seed(graph, "classifyPropertySafe") == "b" + + +def test_resolve_seed_decorated_query_matches_bare_label(): + from graphify.affected import resolve_seed + + graph = nx.DiGraph() + graph.add_node("a", label="Foo", source_file="pkg/foo.py") + graph.add_node("b", label="FooBar", source_file="pkg/foobar.py") + + assert resolve_seed(graph, "Foo()") == "a" + + +def test_resolve_seed_bare_name_tie_still_returns_none(): + from graphify.affected import resolve_seed + + graph = nx.DiGraph() + graph.add_node("a", label="dup()", source_file="pkg/one.py") + graph.add_node("b", label="dup()", source_file="pkg/two.py") + + assert resolve_seed(graph, "dup") is None