Preserve case-only interface signature owners

(cherry picked from commit 72d5914a10)
This commit is contained in:
oleksii-tumanov authored and safishamsi committed 2026-09-22 14:58:12 +01:00
1 parent bda82f9ded
commit f7bc904bda
2 files changed
+75

No files matched your search

+15
View File
@@ -251,6 +251,21 @@ def extract_go(path: Path) -> dict:
return _make_id(base, name), name
def _scan_declarations(node) -> None:
if node.type == "type_spec":
name_node = node.child_by_field_name("name")
if name_node is not None:
owner_nid = _make_id(pkg_scope, _read_text(name_node, source))
for body in node.children:
if body.type != "interface_type":
continue
for elem in body.children:
if elem.type != "method_elem":
continue
method_name = elem.child_by_field_name("name")
if method_name is not None:
name = _read_text(method_name, source)
plain_nid = _make_id(owner_nid, name)
case_groups.setdefault(plain_nid, set()).add(name)
if node.type in ("function_declaration", "method_declaration"):
found = _plain_symbol_nid(node)
if found:
+60
View File
@@ -165,3 +165,63 @@ def test_interface_method_signature_keeps_type_references(tmp_path: Path, signat
warm = _extract(tmp_path)
assert _signature_refs(warm, "Service") == (interface_id, interface_refs)
assert _signature_refs(warm, "Impl") == (concrete_id, concrete_refs)
@pytest.mark.parametrize("names", [("Run", "run"), ("run", "Run"), ("Run", "RUN"), ("RUN", "Run")])
def test_case_only_interface_methods_keep_separate_signature_owners(tmp_path: Path, names):
path = tmp_path / "contract.go"
path.write_text(
"package contracts\n\n"
"type InputA struct{}\n"
"type InputB struct{}\n"
"type Service interface {\n"
f" {names[0]}(InputA)\n"
f" {names[1]}(InputB)\n"
"}\n"
"type Other interface { Run(InputA) }\n",
encoding="utf-8",
)
def owned_refs(result):
by_id = {node["id"]: node for node in result["nodes"]}
methods = {
by_id[edge["target"]]["label"]: edge["target"]
for edge in result["edges"]
if edge.get("relation") == "method"
and by_id[edge["source"]].get("label") == "Service"
}
assert set(methods) == {f".{name}()" for name in names}
assert len(set(methods.values())) == 2
for name, expected in zip(names, ("InputA", "InputB")):
refs = [
edge for edge in result["edges"]
if edge.get("relation") == "references"
and edge["source"] == methods[f".{name}()"]
]
assert len(refs) == 1
assert by_id[refs[0]["target"]]["label"] == expected
assert refs[0]["context"] == "parameter_type"
other_id, other_refs = _signature_refs(result, "Other")
assert other_id not in methods.values()
assert [edge["target_label"] for edge in other_refs] == ["InputA"]
return methods, other_id
result = _extract(tmp_path)
methods, other_id = owned_refs(result)
assert owned_refs(_extract(tmp_path)) == (methods, other_id)
# Declaration order cannot choose the canonical owner of a collision.
path.write_text(path.read_text().replace(
f" {names[0]}(InputA)\n {names[1]}(InputB)\n",
f" {names[1]}(InputB)\n {names[0]}(InputA)\n",
))
assert owned_refs(_extract(tmp_path)) == (methods, other_id)
# Removing the private sibling preserves the exported ID and other owners.
if "run" in names:
source = path.read_text()
path.write_text("\n".join(line for line in source.splitlines() if " run(" not in line))
without_private = _extract(tmp_path)
exported_id, _ = _signature_refs(without_private, "Service")
assert exported_id == methods[".Run()"]
assert _signature_refs(without_private, "Other")[0] == other_id