fix: exclude non-types from C# type resolution

(cherry picked from commit dfe5a220b2)
This commit is contained in:
himanshupatro-334 authored and safishamsi committed 2026-09-25 16:01:47 +01:00
1 parent cac084cf3d
commit 769851dfae
2 files changed
+157 -3

No files matched your search

+20 -3
View File
@@ -16,8 +16,23 @@ from pathlib import Path
from graphify.extractors.base import _make_id
def _build_csharp_type_def_index(all_nodes: list[dict]) -> dict[tuple[str, str], str]:
def _build_csharp_type_def_index(
all_nodes: list[dict],
all_edges: list[dict] | None = None,
) -> dict[tuple[str, str], str]:
"""Return deterministic ``(namespace, name) -> node_id`` C# type definitions."""
contained: set[str] = set()
non_type_ids: set[str] = set()
if all_edges is not None:
for edge in all_edges:
rel = edge.get("relation")
tgt = edge.get("target")
if isinstance(tgt, str):
if rel == "contains":
contained.add(tgt)
elif rel in ("case_of", "defines", "method"):
non_type_ids.add(tgt)
candidates: dict[tuple[str, str], list[dict]] = {}
for node in all_nodes:
if node.get("type") == "namespace":
@@ -31,6 +46,8 @@ def _build_csharp_type_def_index(all_nodes: list[dict]) -> dict[tuple[str, str],
label = node.get("label")
if not (isinstance(nid, str) and nid and isinstance(label, str) and label):
continue
if all_edges is not None and (nid not in contained or nid in non_type_ids):
continue
source_file = node.get("source_file")
if (
not isinstance(source_file, str)
@@ -105,7 +122,7 @@ def _resolve_cross_file_csharp_imports(
if isinstance(label, str) and label and isinstance(nid, str) and nid:
namespace_id_by_label.setdefault(label, nid)
type_def_index = _build_csharp_type_def_index(all_nodes)
type_def_index = _build_csharp_type_def_index(all_nodes, all_edges)
if not namespace_id_by_label and not type_def_index:
return
@@ -184,7 +201,7 @@ class CsharpNameResolver:
for node in all_nodes
if isinstance(node.get("id"), str) and node.get("id")
}
self.type_def_index = _build_csharp_type_def_index(all_nodes)
self.type_def_index = _build_csharp_type_def_index(all_nodes, all_edges)
self.known_namespaces = {
node.get("label")
for node in all_nodes
+137
View File
@@ -567,3 +567,140 @@ def test_csharp_alias_using_scoped_to_its_block(tmp_path: Path):
inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
assert (good["id"], n_t["id"]) in inh, "Good must bind N.T via the in-block alias"
assert (bad["id"], n_t["id"]) not in inh, "Bad (sibling block) must NOT see the alias"
def test_csharp_same_file_enum_member_does_not_preempt_class_inheritance(tmp_path: Path):
# Repro A (#3795): enum member Follow must not take the inheritance edge from class Follow.
f = _write(
tmp_path / "effects.cs",
"namespace Demo {\n"
" public enum EffectKind { Tween, Follow }\n"
" public abstract class Follow { }\n"
" public sealed class FollowFloat : Follow { }\n"
"}\n",
)
result = extract([f], cache_root=tmp_path)
follow_class = next(
n for n in result["nodes"]
if n.get("label") == "Follow" and "effectkind" not in str(n.get("id", "")).lower()
)
follow_member = next(
n for n in result["nodes"]
if n.get("label") == "Follow" and "effectkind" in str(n.get("id", "")).lower()
)
followfloat = next(n for n in result["nodes"] if n.get("label") == "FollowFloat")
inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
assert (followfloat["id"], follow_class["id"]) in inh, \
"FollowFloat must inherit from class Follow"
assert (followfloat["id"], follow_member["id"]) not in inh, \
"FollowFloat must NOT inherit from enum member EffectKind.Follow"
def test_csharp_enum_member_cannot_steal_external_parameter_type(tmp_path: Path):
# Repro B (#3795): Vector2 parameter must not bind to Channel.Vector2 enum member.
kinds = _write(
tmp_path / "Kinds.cs",
"namespace Demo {\n"
" public enum Channel { Vector2 }\n"
"}\n",
)
mover = _write(
tmp_path / "Mover.cs",
"using UnityEngine;\n"
"namespace Demo {\n"
" public class Mover {\n"
" public void Move(Vector2 delta) { }\n"
" }\n"
"}\n",
)
result = extract([kinds, mover], cache_root=tmp_path)
channel_v2 = next(
n for n in result["nodes"]
if n.get("label") == "Vector2" and n.get("source_file")
)
move_refs = [
e["target"] for e in result["edges"]
if e.get("relation") == "references"
and e.get("context") == "parameter_type"
and "move" in str(e.get("source", "")).lower()
]
assert channel_v2["id"] not in move_refs, \
"Mover.Move(Vector2) must NOT resolve to Channel.Vector2 enum member"
by_id = {n["id"]: n for n in result["nodes"]}
assert any(
by_id[tgt].get("label") == "Vector2" and not by_id[tgt].get("source_file")
for tgt in move_refs
), f"Vector2 parameter must remain on an external stub: {move_refs}"
def test_csharp_property_cannot_steal_external_parameter_type(tmp_path: Path):
# Repro B (#3795): System.Type parameter must not bind to Node.Type property.
node = _write(
tmp_path / "Node.cs",
"namespace Demo {\n"
" public class Node {\n"
" public System.Type Type { get; set; }\n"
" }\n"
"}\n",
)
mover = _write(
tmp_path / "Mover.cs",
"using System;\n"
"namespace Demo {\n"
" public class Mover {\n"
" public string Describe(Type t) => t.Name;\n"
" }\n"
"}\n",
)
result = extract([node, mover], cache_root=tmp_path)
node_prop_type = next(
n for n in result["nodes"]
if n.get("label") == "Type" and n.get("source_file")
)
describe_refs = [
e["target"] for e in result["edges"]
if e.get("relation") == "references"
and e.get("context") == "parameter_type"
and "describe" in str(e.get("source", "")).lower()
]
assert node_prop_type["id"] not in describe_refs, \
"Mover.Describe(Type) must NOT resolve to Node.Type property"
by_id = {n["id"]: n for n in result["nodes"]}
assert any(
by_id[tgt].get("label") == "Type" and not by_id[tgt].get("source_file")
for tgt in describe_refs
), f"Type parameter must remain on an external stub: {describe_refs}"
def test_csharp_cross_file_collision_resolved_by_kind_not_filename_order(tmp_path: Path):
# Enum member in A_Enum.cs (alphabetically first) must not steal inheritance from class in Follow.cs.
a_enum = _write(
tmp_path / "A_Enum.cs",
"namespace Demo {\n"
" public enum EffectKind { Tween, Follow }\n"
"}\n",
)
follow = _write(
tmp_path / "Follow.cs",
"namespace Demo {\n"
" public abstract class Follow { }\n"
" public sealed class FollowFloat : Follow { }\n"
"}\n",
)
result = extract([a_enum, follow], cache_root=tmp_path)
follow_class = next(
n for n in result["nodes"]
if n.get("label") == "Follow" and "follow.cs" in str(n.get("source_file", "")).lower()
)
follow_member = next(
n for n in result["nodes"]
if n.get("label") == "Follow" and "a_enum.cs" in str(n.get("source_file", "")).lower()
)
followfloat = next(n for n in result["nodes"] if n.get("label") == "FollowFloat")
inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"}
assert (followfloat["id"], follow_class["id"]) in inh, \
"FollowFloat must inherit from class Follow despite A_Enum.cs sorting first"
assert (followfloat["id"], follow_member["id"]) not in inh, \
"FollowFloat must NOT inherit from enum member EffectKind.Follow in A_Enum.cs"