from __future__ import annotations from pathlib import Path from graphify.extract import extract def _write(path: Path, text: str) -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text, encoding="utf-8") return path def _node_by_id(result: dict, nid: str) -> dict | None: return next((n for n in result["nodes"] if n.get("id") == nid), None) def _targets(result: dict, relation: str, label: str) -> list[dict]: out = [] for e in result["edges"]: if e.get("relation") != relation: continue n = _node_by_id(result, e.get("target")) if n is not None and n.get("label") == label: out.append(n) return out def _defs(result: dict, label: str) -> list[dict]: return [ n for n in result["nodes"] if n.get("label") == label and n.get("source_file") ] def test_csharp_declaration_nodes_carry_enclosing_namespace(tmp_path: Path): block = _write( tmp_path / "block.cs", "namespace Game.Core { public class Damage {} }\n", ) nested = _write( tmp_path / "nested.cs", "namespace Outer { namespace Inner { public class NestedDamage {} } }\n", ) file_scoped = _write( tmp_path / "file_scoped.cs", "namespace FileScoped.Core;\npublic class FileScopedDamage {}\n", ) result = extract([block, nested, file_scoped], cache_root=tmp_path) assert _defs(result, "Damage")[0].get("metadata", {}).get("namespace") == "Game.Core" assert _defs(result, "NestedDamage")[0].get("metadata", {}).get("namespace") == "Outer.Inner" assert _defs(result, "FileScopedDamage")[0].get("metadata", {}).get("namespace") == "FileScoped.Core" assert _defs(result, "Damage")[0]["metadata"].get("scope_chain"), "lexical scope_chain must be stamped" def test_csharp_cross_file_inherits_resolves_to_real_def(tmp_path: Path): core = _write(tmp_path / "core.cs", "namespace Game.Core { public class Damage { public int Calc() { return 1; } } }\n") combat = _write(tmp_path / "combat.cs", "using Game.Core;\nnamespace Game.Combat { public class Weapon : Damage {} }\n") result = extract([core, combat], cache_root=tmp_path) damage = _targets(result, "inherits", "Damage") assert damage, "expected an inherits edge to Damage" assert all(d.get("source_file") for d in damage), \ "Weapon : Damage must resolve to the real Damage def, not a shadow stub" def test_csharp_collision_disambiguated_by_using(tmp_path: Path): core = _write(tmp_path / "core.cs", "namespace Game.Core { public class WeaponData { public int Number; } }\n") ui = _write(tmp_path / "ui.cs", "namespace Game.UI { public class WeaponData { public int Width; } }\n") combat = _write(tmp_path / "combat.cs", "using Game.Core;\nnamespace Game.Combat { public class Holder { public WeaponData data; } }\n") result = extract([core, ui, combat], cache_root=tmp_path) shadow = [n for n in result["nodes"] if n.get("label") == "WeaponData" and not n.get("source_file")] assert not shadow, f"orphan WeaponData shadow node(s) remain: {[n['id'] for n in shadow]}" resolved = [w for w in _targets(result, "references", "WeaponData") if w.get("source_file")] assert resolved, "WeaponData reference should resolve to a real def" assert all("core.cs" in w["source_file"] for w in resolved), \ "must disambiguate to Game.Core.WeaponData via `using Game.Core;`, not Game.UI" def test_csharp_global_using_and_global_namespace(tmp_path: Path): gadget = _write(tmp_path / "gadget.cs", "public class Gadget {}\n") user = _write(tmp_path / "user.cs", "global using System;\npublic class Widget : Gadget {}\n") result = extract([gadget, user], cache_root=tmp_path) g = _targets(result, "inherits", "Gadget") assert g, "expected an inherits edge to Gadget" assert all(x.get("source_file") for x in g), \ "Widget : Gadget (both global namespace) must resolve; `global using` must not break parsing" def test_csharp_cross_namespace_enum_reference_resolves_to_real_def(tmp_path: Path): core = _write( tmp_path / "core.cs", "namespace Game.Core { public enum Element { Fire, Ice } public class Damage {} }\n", ) combat = _write( tmp_path / "combat.cs", "using Game.Core;\n" "namespace Game.Combat { public class Spell { Element element; Damage dmg; } }\n", ) result = extract([core, combat], cache_root=tmp_path) element_defs = _defs(result, "Element") assert element_defs, "enum Element should be emitted as a real type definition node" assert all("core.cs" in n["source_file"] for n in element_defs) element_refs = [n for n in _targets(result, "references", "Element") if n.get("source_file")] assert element_refs, "Element field reference should resolve to the enum definition" assert all("core.cs" in n["source_file"] for n in element_refs) def test_csharp_cross_namespace_struct_and_record_references_resolve(tmp_path: Path): core = _write( tmp_path / "core.cs", "namespace Game.Core { " "public struct Coord { public int X; } " "public record Player(string Name); " "}\n", ) combat = _write( tmp_path / "combat.cs", "using Game.Core;\n" "namespace Game.Combat { public class Spell { Coord coord; Player player; } }\n", ) result = extract([core, combat], cache_root=tmp_path) for label in ("Coord", "Player"): assert _defs(result, label), f"{label} should be emitted as a real type definition node" resolved = [n for n in _targets(result, "references", label) if n.get("source_file")] assert resolved, f"{label} field reference should resolve to the real definition" assert all("core.cs" in n["source_file"] for n in resolved) def test_csharp_ambiguous_using_does_not_resolve(tmp_path: Path): # WeaponData is defined in BOTH Game.Core and Game.UI, and the referrer opens # BOTH namespaces. With two candidates the resolver must REFUSE (accept only a # unique hit) and leave the reference dangling on a shadow stub, rather than # fabricate an edge to an arbitrary, possibly-wrong definition. core = _write( tmp_path / "core.cs", "namespace Game.Core { public class WeaponData { public int Number; } }\n", ) ui = _write( tmp_path / "ui.cs", "namespace Game.UI { public class WeaponData { public int Width; } }\n", ) holder = _write( tmp_path / "holder.cs", "using Game.Core;\n" "using Game.UI;\n" "namespace Game.Combat { public class Holder { public WeaponData data; } }\n", ) result = extract([core, ui, holder], cache_root=tmp_path) wd_refs = _targets(result, "references", "WeaponData") assert wd_refs, "expected a WeaponData reference edge (otherwise the test is vacuous)" resolved = [n for n in wd_refs if n.get("source_file")] assert not resolved, ( "ambiguous WeaponData (Game.Core vs Game.UI, both opened) must NOT resolve to " f"either def; got wrong resolution(s): {[n.get('source_file') for n in resolved]}" ) def test_csharp_using_alias_resolves_to_aliased_type(tmp_path: Path): # `using Dmg = Game.Core.Damage;` is a single-type alias. A base type written as # `Dmg` has no other resolution route, so it must resolve to the real # Game.Core.Damage definition via the alias map -- not stay on a `Dmg` stub. core = _write( tmp_path / "core.cs", "namespace Game.Core { public class Damage {} }\n", ) combat = _write( tmp_path / "combat.cs", "using Dmg = Game.Core.Damage;\n" "namespace Game.Combat { public class Weapon : Dmg {} }\n", ) result = extract([core, combat], cache_root=tmp_path) damage = _targets(result, "inherits", "Damage") assert damage, "Weapon : Dmg must resolve (via the `using Dmg = ...` alias) to Damage" assert all("core.cs" in d["source_file"] for d in damage), ( "the alias `Dmg` must resolve to the real Game.Core.Damage def, not a shadow stub" ) def test_csharp_namespace_nodes_canonical_and_discriminated(tmp_path: Path): a = _write(tmp_path / "a.cs", "namespace N { class A {} }\n") b = _write(tmp_path / "b.cs", "namespace N { class B {} }\n") nested = _write(tmp_path / "n.cs", "namespace Outer { namespace Inner { class C {} } }\n") result = extract([a, b, nested], cache_root=tmp_path) ns = [n for n in result["nodes"] if n.get("type") == "namespace"] by_label = {} for n in ns: by_label.setdefault(n["label"], []).append(n) assert len(by_label.get("N", [])) == 1, "namespace N must be one canonical node across files" assert "Outer.Inner" in by_label, sorted(by_label) assert all(n["id"].startswith("csharp_namespace:") for n in ns), [n["id"] for n in ns] def test_csharp_import_edges_carry_using_kind(tmp_path: Path): f = _write( tmp_path / "a.cs", "using Game.Core;\nusing static System.Math;\nglobal using System;\n" "using X = Game.Core.Damage;\nclass Z {}\n", ) result = extract([f], cache_root=tmp_path) imports = { (e["metadata"].get("using_kind"), e["metadata"].get("target_fqn"), e["metadata"].get("alias")) for e in result["edges"] if e.get("relation") == "imports" and e.get("metadata") } assert ("namespace", "Game.Core", None) in imports, imports assert ("namespace", "System", None) in imports, imports assert ("static", "System.Math", None) in imports, imports assert ("alias", "Game.Core.Damage", "X") in imports, imports def test_csharp_import_edges_resolve_internal_namespace_and_alias(tmp_path: Path): core = _write( tmp_path / "core.cs", "namespace Game.Core { public class Damage {} }\n", ) user = _write( tmp_path / "u.cs", "using Game.Core;\n" "using UnityEngine;\n" "using Dmg = Game.Core.Damage;\n" "using DMath = System.Math;\n" "using static Game.Core.Damage;\n" "class Z {}\n", ) result = extract([core, user], cache_root=tmp_path) by_id = {n["id"]: n for n in result["nodes"]} imports = [ (e["metadata"]["using_kind"], e["metadata"].get("target_fqn"), by_id.get(e["target"])) for e in result["edges"] if e.get("relation") == "imports" and (e.get("metadata") or {}).get("using_kind") ] assert ("namespace", "Game.Core", "namespace") in [ (kind, fqn, target.get("type") if target else None) for kind, fqn, target in imports ] assert ("namespace", "UnityEngine", None) in [ (kind, fqn, target.get("type") if target else None) for kind, fqn, target in imports ] assert ("alias", "Game.Core.Damage", "Damage") in [ (kind, fqn, target.get("label") if target else None) for kind, fqn, target in imports ] assert ("alias", "System.Math", None) in [ (kind, fqn, target.get("label") if target else None) for kind, fqn, target in imports ] assert ("static", "Game.Core.Damage", None) in [ (kind, fqn, target.get("label") if target else None) for kind, fqn, target in imports ] assert not [ n for n in result["nodes"] if not n.get("source_file") and n.get("label") in {"Game.Core", "Game.Core.Damage"} ] def test_csharp_qualified_base_ref_is_flagged(tmp_path: Path): f = _write(tmp_path / "a.cs", "namespace N { class T {} class Use : B.T {} }\n") result = extract([f], cache_root=tmp_path) assert any((e.get("metadata") or {}).get("qualified") for e in result["edges"]), \ "the qualified base ref B.T must carry metadata.qualified" def test_csharp_one_file_same_name_no_collision_flag(tmp_path: Path): # ns_collision is gone: A.T and B.T are distinct nodes with no ns_collision metadata. dup = _write(tmp_path / "dup.cs", "namespace A { class T {} } namespace B { class T {} }\n") result = extract([dup], cache_root=tmp_path) tnodes = [n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")] assert len({n["id"] for n in tnodes}) == 2, tnodes assert not any((n.get("metadata") or {}).get("ns_collision") for n in tnodes), \ "ns_collision must no longer be stamped" def test_csharp_type_parameter_emits_no_reference(tmp_path: Path): f = _write(tmp_path / "a.cs", "namespace N { class T {} class Box { T value; } }\n") result = extract([f], cache_root=tmp_path) real_t = {n["id"] for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")} box_to_t = [ e for e in result["edges"] if e.get("relation") in ("references", "inherits", "implements") and e.get("target") in real_t and "box" in str(e.get("source", "")).lower() ] assert not box_to_t, f"type parameter T must not produce a ref to the real N.T: {box_to_t}" def test_csharp_nested_type_carries_metadata(tmp_path: Path): f = _write(tmp_path / "a.cs", "namespace N { class Outer { class Inner {} } }\n") result = extract([f], cache_root=tmp_path) inner = [n for n in result["nodes"] if n.get("label") == "Inner"] assert inner and inner[0].get("metadata", {}).get("is_nested_type") is True, inner def test_csharp_cross_namespace_ref_not_misbound(tmp_path: Path): # Use in namespace B must NOT bind to C.T (B never opens C) - even though T is globally unique. f = _write(tmp_path / "x.cs", "namespace B { class Use : T {} } namespace C { class T {} }\n") result = extract([f], cache_root=tmp_path) resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")] assert not resolved, f"Use:T in B must not bind C.T: {resolved}" def test_csharp_same_file_cross_namespace_ref_not_misbound(tmp_path: Path): # Same file, T defined in B, Use in C : T - must NOT bind B.T (the eager same-file binding case). f = _write(tmp_path / "x.cs", "namespace B { class T {} } namespace C { class Use : T {} }\n") result = extract([f], cache_root=tmp_path) resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")] assert not resolved, f"same-file Use:T in C must not bind B.T: {resolved}" def test_csharp_inherits_does_not_bind_namespace_node(tmp_path: Path): # class Use : Game where Game is a namespace - must NOT bind the namespace node (Chunk-1 review B1). f = _write(tmp_path / "y.cs", "namespace Game { class Damage {} class Use : Game {} }\n") result = extract([f], cache_root=tmp_path) nsids = {n["id"] for n in result["nodes"] if n.get("type") == "namespace"} bad = [e for e in result["edges"] if e.get("relation") == "inherits" and e.get("target") in nsids] assert not bad, f"inherits must not target a namespace node: {bad}" def test_csharp_qualified_ref_unknown_qualifier_dangles(tmp_path: Path): # B.T where B is neither a known namespace nor an alias -> must NOT bind A.T (sound dangle). f = _write(tmp_path / "a.cs", "namespace A { class T {} class Use : B.T {} }\n") result = extract([f], cache_root=tmp_path) resolved = [t for t in _targets(result, "inherits", "T") if t.get("source_file")] assert not resolved, f"unknown-qualifier B.T must not bind A.T: {resolved}" def test_csharp_qualified_ref_known_namespace_resolves(tmp_path: Path): a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n") b = _write(tmp_path / "m.cs", "namespace M { class Use : N.T {} }\n") result = extract([a, b], cache_root=tmp_path) n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")) use = next(n for n in result["nodes"] if n.get("label") == "Use") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (use["id"], n_t["id"]) in inh, "M.Use : N.T must bind N.T" def test_csharp_qualified_generic_resolves_to_real_def(tmp_path: Path): # N.Box previously emitted a junk 'B'-style label; it must resolve to the real N.Box def. f = _write(tmp_path / "g.cs", "namespace N { class Box {} class Use { N.Box b; } }\n") result = extract([f], cache_root=tmp_path) box = next(n for n in result["nodes"] if n.get("label") == "Box" and n.get("source_file")) use = next(n for n in result["nodes"] if n.get("label") == "Use") refs = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "references"} assert (use["id"], box["id"]) in refs, "N.Box field must resolve to the real N.Box def" assert not any("<" in (n.get("label") or "") for n in result["nodes"]), \ "no node should carry a junk generic label" def test_csharp_qualified_alias_namespace_resolves(tmp_path: Path): # using B = X.Y (namespace alias) then B.T -> resolves the type T in namespace X.Y. a = _write(tmp_path / "n.cs", "namespace X.Y { class T {} }\n") b = _write(tmp_path / "m.cs", "using B = X.Y;\nnamespace M { class Use : B.T {} }\n") result = extract([a, b], cache_root=tmp_path) t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")) use = next(n for n in result["nodes"] if n.get("label") == "Use") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (use["id"], t["id"]) in inh, "B.T with `using B = X.Y;` must resolve to X.Y.T" def test_csharp_qualified_out_of_scope_alias_falls_through_to_namespace(tmp_path: Path): # B is a real namespace AND an out-of-scope alias (declared in A, used in M): # B.T in M must resolve to namespace B's T, not dangle. a = _write(tmp_path / "b.cs", "namespace B { class T {} }\n") c = _write(tmp_path / "m.cs", "namespace A { using B = X.Y; }\nnamespace M { class Use : B.T {} }\n") result = extract([a, c], cache_root=tmp_path) b_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")) use = next(n for n in result["nodes"] if n.get("label") == "Use") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (use["id"], b_t["id"]) in inh, "out-of-scope alias B must fall through to namespace B" def test_csharp_qualified_in_scope_alias_shadows_namespace(tmp_path: Path): # B is both a real namespace AND an in-scope alias (B = X.Y) in A's block; a later out-of-scope # alias (B = Z.Q in C) must not overwrite it. Good : B.T -> X.Y.T, not namespace B's T. a = _write(tmp_path / "xy.cs", "namespace X.Y { class T {} }\n") b = _write(tmp_path / "b.cs", "namespace B { class T {} }\n") c = _write(tmp_path / "use.cs", "namespace A { using B = X.Y; class Good : B.T {} }\nnamespace C { using B = Z.Q; }\n") result = extract([a, b, c], cache_root=tmp_path) xy_t = next(n for n in result["nodes"] if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "X.Y") b_t = next(n for n in result["nodes"] if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "B") good = next(n for n in result["nodes"] if n.get("label") == "Good") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (good["id"], xy_t["id"]) in inh, "in-scope alias B=X.Y must resolve B.T to X.Y.T" assert (good["id"], b_t["id"]) not in inh, "must NOT bind namespace B's T" def test_csharp_one_file_same_name_binds_own_namespace(tmp_path: Path): # T in both A and B of one file; Use:T in B must bind B.T (its own namespace), not A.T. f = _write( tmp_path / "c.cs", "namespace A { class T {} } namespace B { class T {} class Use : T {} }\n", ) result = extract([f], cache_root=tmp_path) b_t = next(n for n in result["nodes"] if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "B") a_t = next(n for n in result["nodes"] if n.get("label") == "T" and (n.get("metadata") or {}).get("namespace") == "A") use = next(n for n in result["nodes"] if n.get("label") == "Use") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (use["id"], b_t["id"]) in inh, "Use:T in B must bind B.T" assert (use["id"], a_t["id"]) not in inh, "Use:T must NOT bind A.T" def test_csharp_nested_type_not_importable_via_using(tmp_path: Path): # Inner is nested in Outer; `using N;` does not bring Inner into scope as a bare member. a = _write(tmp_path / "a.cs", "namespace N { class Outer { class Inner {} } }\n") b = _write(tmp_path / "b.cs", "using N;\nnamespace M { class Use { Inner x; } }\n") result = extract([a, b], cache_root=tmp_path) resolved = [t for t in _targets(result, "references", "Inner") if t.get("source_file")] assert not resolved, f"nested Inner must not resolve via `using N;`: {resolved}" def test_csharp_generic_alias_resolves_to_base_type(tmp_path: Path): core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n") use = _write(tmp_path / "use.cs", "using Bx = N.Box;\nclass Use : Bx {}\n") result = extract([core, use], cache_root=tmp_path) resolved = [t for t in _targets(result, "inherits", "Box") if t.get("source_file")] assert resolved, "generic alias `using Bx = N.Box;` must resolve to the real Box def" def test_csharp_type_ref_never_targets_a_file_label(tmp_path: Path): core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n") b = _write(tmp_path / "b.cs", "using B = N.Box;\nclass Use : B {}\n") result = extract([core, b], cache_root=tmp_path) bad = [ e for e in result["edges"] if e.get("relation") in ("inherits", "implements", "references") and str(_node_by_id(result, e.get("target")).get("label", "") if _node_by_id(result, e.get("target")) else "").endswith(".cs") ] assert not bad, f"a C# type ref must not target a .cs file-labeled node: {bad}" def test_csharp_type_ref_edges_carry_ref_token(tmp_path: Path): core = _write(tmp_path / "core.cs", "namespace N { class Base {} }\n") use = _write(tmp_path / "use.cs", "using N;\nnamespace M { class Use : Base {} }\n") result = extract([core, use], cache_root=tmp_path) inh = [ e for e in result["edges"] if e.get("relation") == "inherits" and "use" in str(e.get("source", "")).lower() ] assert inh, "expected the Use : Base inherits edge" assert any((e.get("metadata") or {}).get("ref_token") == "Base" for e in inh), \ "the inherits edge must carry metadata.ref_token == 'Base'" def test_csharp_alias_matching_file_stem_resolves_via_token(tmp_path: Path): # alias name == file stem (B in b.cs) used to corrupt the target label; the # ref token makes the arbiter resolve it correctly regardless. core = _write(tmp_path / "core.cs", "namespace N { class Box {} }\n") b = _write(tmp_path / "b.cs", "using B = N.Box;\nclass Use : B {}\n") result = extract([core, b], cache_root=tmp_path) resolved = [t for t in _targets(result, "inherits", "Box") if t.get("source_file")] assert resolved, "Use : B (alias B == file stem) must resolve to the real Box def" def test_csharp_same_name_diff_namespace_have_distinct_ids(tmp_path: Path): # The id now carries the namespace, so A.T and B.T are distinct nodes (resolution unchanged here). f = _write(tmp_path / "x.cs", "namespace A { class T {} } namespace B { class T {} }\n") result = extract([f], cache_root=tmp_path) ids = {n["id"] for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")} assert len(ids) == 2, f"A.T and B.T must be distinct nodes: {ids}" def test_csharp_global_scope_id_unchanged(tmp_path: Path): # A C# type at global scope (no namespace) keeps the bare stem+name id (empty namespace dropped by make_id). from graphify.extractors.base import _make_id, _file_stem f = _write(tmp_path / "g.cs", "class Glob {}\n") result = extract([f], cache_root=tmp_path) glob = next(n for n in result["nodes"] if n.get("label") == "Glob") stem = _file_stem(tmp_path / "g.cs") if "/" in stem: stem = stem.rsplit("/", 1)[-1] assert glob["id"] == _make_id(stem, "Glob"), glob assert "namespace" not in (glob.get("metadata") or {}) def test_csharp_namespaced_id_carries_namespace_segment(tmp_path: Path): f = _write(tmp_path / "n.cs", "namespace Game.Core { class Order {} }\n") result = extract([f], cache_root=tmp_path) order = next(n for n in result["nodes"] if n.get("label") == "Order") assert order["id"].endswith("order") and "game_core" in order["id"], order["id"] assert (order.get("metadata") or {}).get("namespace") == "Game.Core" def test_csharp_two_namespaces_each_resolve_own_type(tmp_path: Path): f = _write( tmp_path / "two.cs", "namespace A { class T {} class UseA : T {} } namespace B { class T {} class UseB : T {} }\n", ) result = extract([f], cache_root=tmp_path) def _n(label, ns): return next(x for x in result["nodes"] if x.get("label") == label and (x.get("metadata") or {}).get("namespace") == ns) a_t, b_t, use_a, use_b = _n("T", "A"), _n("T", "B"), _n("UseA", "A"), _n("UseB", "B") inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (use_a["id"], a_t["id"]) in inh and (use_b["id"], b_t["id"]) in inh assert (use_a["id"], b_t["id"]) not in inh and (use_b["id"], a_t["id"]) not in inh def test_csharp_file_level_using_applies_across_blocks(tmp_path: Path): a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n") b = _write(tmp_path / "u.cs", "using N;\nnamespace A { class X : T {} } namespace B { class Y : T {} }\n") result = extract([a, b], cache_root=tmp_path) resolved = [t["id"] for t in _targets(result, "inherits", "T") if t.get("source_file")] assert len(resolved) >= 2, f"file-level using N must reach both A.X and B.Y: {resolved}" def test_csharp_namespace_scoped_using_isolated_to_sibling_block(tmp_path: Path): a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n") b = _write( tmp_path / "u.cs", "namespace A { using N; class Good : T {} }\nnamespace A { class Bad : T {} }\n", ) result = extract([a, b], cache_root=tmp_path) good = next(n for n in result["nodes"] if n.get("label") == "Good") bad = next(n for n in result["nodes"] if n.get("label") == "Bad") n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")) inh = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "inherits"} assert (good["id"], n_t["id"]) in inh, "Good (same block as using N) must bind N.T" assert (bad["id"], n_t["id"]) not in inh, "Bad (sibling block, no using) must NOT bind N.T" def test_csharp_using_flows_into_nested_block(tmp_path: Path): a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n") b = _write(tmp_path / "u.cs", "namespace A { using N; namespace B { class Inner : T {} } }\n") result = extract([a, b], cache_root=tmp_path) resolved = [t["id"] for t in _targets(result, "inherits", "T") if t.get("source_file")] assert resolved, "using N in outer block A must flow into nested block B" def test_csharp_alias_using_scoped_to_its_block(tmp_path: Path): a = _write(tmp_path / "n.cs", "namespace N { class T {} }\n") b = _write( tmp_path / "u.cs", "namespace A { using AliasT = N.T; class Good : AliasT {} }\nnamespace A { class Bad : AliasT {} }\n", ) result = extract([a, b], cache_root=tmp_path) good = next(n for n in result["nodes"] if n.get("label") == "Good") bad = next(n for n in result["nodes"] if n.get("label") == "Bad") n_t = next(n for n in result["nodes"] if n.get("label") == "T" and n.get("source_file")) 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" def test_csharp_name_resolver_resolves_type_in_partial_graph_without_contains(): # A partial or cross-repo graph intentionally lacks structural `contains` edges # from file nodes. CsharpNameResolver must still resolve valid types while # excluding member nodes (methods, properties, enum members). from graphify.extractors.csharp import CsharpNameResolver nodes = [ { "id": "app::file", "label": "OrderService.cs", "source_file": "src/OrderService.cs", "file_type": "code", "repo": "app", }, { "id": "app::run", "label": ".Run()", "source_file": "src/OrderService.cs", "file_type": "code", "repo": "app", }, { "id": "lib::type", "label": "IValidator", "source_file": "src/IValidator.cs", "file_type": "code", "repo": "lib", "metadata": {"namespace": "Lib.Domain.Interfaces"}, }, { "id": "lib::method", "label": ".ValidateAsync()", "source_file": "src/IValidator.cs", "file_type": "code", "repo": "lib", }, ] edges = [ { "source": "app::file", "target": "app::using", "relation": "imports", "metadata": {"using_kind": "namespace", "target_fqn": "Lib.Domain.Interfaces"}, }, { "source": "lib::type", "target": "lib::method", "relation": "method", }, ] resolver = CsharpNameResolver(nodes, edges) resolved, decisive = resolver.resolve_type_name( "IValidator", nodes[1], "src/OrderService.cs", ) assert resolved == "lib::type", "Valid C# type must resolve even without `contains` edges" assert decisive is True def _crlf_vs_lf(tmp_path: Path, files: dict[str, str]) -> tuple[dict, dict]: results = [] for name, newline in (("lf", "\n"), ("crlf", "\r\n")): root = tmp_path / name paths = [] for rel, text in files.items(): p = root / rel p.parent.mkdir(parents=True, exist_ok=True) p.write_bytes(text.replace("\n", newline).encode("utf-8")) paths.append(p) results.append(extract(paths, root=root, cache_root=root)) return results[0], results[1] def test_csharp_scope_metadata_does_not_depend_on_line_endings(tmp_path: Path): """Namespace scope ids were byte offsets, so a CRLF checkout (Windows, core.autocrlf) and an LF checkout of the same commit wrote different `scope_chain` / `scope_id` metadata into graph.json. Row:column is the same for both.""" files = { "core.cs": "// Core types.\nusing System;\n\nnamespace Game.Core\n{\n public class Damage {}\n}\n", "combat.cs": ( "// Combat.\n" "using System;\n" "\n" "namespace Game.Combat\n" "{\n" " using Dmg = Game.Core.Damage;\n" "\n" " public class Weapon : Dmg\n" " {\n" " public void Hit() {}\n" " }\n" "}\n" ), } lf, crlf = _crlf_vs_lf(tmp_path, files) def shape(result: dict) -> tuple[list, list]: nodes = sorted( (n["id"], repr(sorted((n.get("metadata") or {}).items())), n.get("source_location")) for n in result["nodes"] ) edges = sorted((e["source"], e["target"], e["relation"]) for e in result["edges"]) return nodes, edges assert any("scope_chain" in (n.get("metadata") or {}) for n in lf["nodes"]) assert shape(lf) == shape(crlf) damage = _targets(crlf, "inherits", "Damage") assert damage and all("core.cs" in d["source_file"] for d in damage), ( "the namespace-scoped alias must still resolve in a CRLF file" ) def test_csharp_enclosing_namespace_dotted(tmp_path: Path): demo = _write( tmp_path / "Demo.cs", "namespace Demo {\n" " public class Base {}\n" " public class Widget {}\n" "}\n", ) inner = _write( tmp_path / "Inner.cs", "namespace Demo.Inner {\n" " public class Registry {\n" " public Widget Item;\n" " }\n" " public class Factory : Base {\n" " public object Make() {\n" " return new Widget();\n" " }\n" " }\n" "}\n", ) result = extract([demo, inner], cache_root=tmp_path) widget_defs = _defs(result, "Widget") base_defs = _defs(result, "Base") assert len(widget_defs) == 1 and "Demo.cs" in widget_defs[0]["source_file"] assert len(base_defs) == 1 and "Demo.cs" in base_defs[0]["source_file"] # Inherits: Factory : Base inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")] assert inherits_targets and inherits_targets[0]["id"] == base_defs[0]["id"] # References: Registry.Item -> Widget ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")] assert ref_targets and ref_targets[0]["id"] == widget_defs[0]["id"] # Calls: Factory.Make() -> new Widget() make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()") calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"} assert (make_node["id"], widget_defs[0]["id"]) in calls # No orphan shadow stubs shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")] assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}" def test_csharp_enclosing_namespace_nested_blocks(tmp_path: Path): source = _write( tmp_path / "nested.cs", "namespace Demo {\n" " public class Base {}\n" " public class Widget {}\n" " namespace Inner {\n" " public class Registry {\n" " public Widget Item;\n" " }\n" " public class Factory : Base {\n" " public object Make() {\n" " return new Widget();\n" " }\n" " }\n" " }\n" "}\n", ) result = extract([source], cache_root=tmp_path) widget_def = _defs(result, "Widget")[0] base_def = _defs(result, "Base")[0] inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")] assert inherits_targets and inherits_targets[0]["id"] == base_def["id"] ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")] assert ref_targets and ref_targets[0]["id"] == widget_def["id"] make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()") calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"} assert (make_node["id"], widget_def["id"]) in calls shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")] assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}" def test_csharp_enclosing_namespace_file_scoped(tmp_path: Path): demo = _write( tmp_path / "Demo.cs", "namespace Demo;\n" "public class Base {}\n" "public class Widget {}\n", ) scoped = _write( tmp_path / "Scoped.cs", "namespace Demo.Scoped;\n" "public class Registry {\n" " public Widget Item;\n" "}\n" "public class Factory : Base {\n" " public object Make() {\n" " return new Widget();\n" " }\n" "}\n", ) result = extract([demo, scoped], cache_root=tmp_path) widget_def = _defs(result, "Widget")[0] base_def = _defs(result, "Base")[0] inherits_targets = [d for d in _targets(result, "inherits", "Base") if d.get("source_file")] assert inherits_targets and inherits_targets[0]["id"] == base_def["id"] ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")] assert ref_targets and ref_targets[0]["id"] == widget_def["id"] make_node = next(n for n in result["nodes"] if n.get("label") == ".Make()") calls = {(e["source"], e["target"]) for e in result["edges"] if e.get("relation") == "calls"} assert (make_node["id"], widget_def["id"]) in calls shadow = [n for n in result["nodes"] if n.get("label") in ("Widget", "Base") and not n.get("source_file")] assert not shadow, f"unexpected shadow stubs: {[n['id'] for n in shadow]}" def test_csharp_enclosing_namespace_multilevel_shadowing(tmp_path: Path): outer = _write( tmp_path / "outer.cs", "namespace Demo {\n" " public class Widget { public int Level = 1; }\n" "}\n", ) inner = _write( tmp_path / "inner.cs", "namespace Demo.Inner {\n" " public class Widget { public int Level = 2; }\n" "}\n", ) deep = _write( tmp_path / "deep.cs", "namespace Demo.Inner.Deep {\n" " public class Consumer {\n" " public Widget Item;\n" " }\n" "}\n", ) result = extract([outer, inner, deep], cache_root=tmp_path) inner_widget = next( n for n in _defs(result, "Widget") if "inner.cs" in n.get("source_file", "") ) ref_targets = [d for d in _targets(result, "references", "Widget") if d.get("source_file")] assert ref_targets, "expected Widget reference to resolve" assert all(r["id"] == inner_widget["id"] for r in ref_targets), \ "Demo.Inner.Deep must bind to Demo.Inner.Widget (innermost enclosing), not Demo.Widget" def test_csharp_enclosing_namespace_regression_controls(tmp_path: Path): source = _write( tmp_path / "types.cs", "namespace Demo {\n" " public class OuterType {}\n" " public class CollidingType {}\n" "}\n" "namespace Other {\n" " public class ImportedType {}\n" "}\n" "namespace Demo.Inner {\n" " using Other;\n" " public class CollidingType {}\n" " public class Tester {\n" " public CollidingType Local;\n" " public ImportedType Imported;\n" " public UnknownType Missing;\n" " }\n" "}\n", ) result = extract([source], cache_root=tmp_path) local_colliding = next( n for n in _defs(result, "CollidingType") if (n.get("metadata") or {}).get("namespace") == "Demo.Inner" ) imported_type = _defs(result, "ImportedType")[0] # CollidingType reference must bind to Demo.Inner.CollidingType (current ns shadows enclosing) ref_colliding = [d for d in _targets(result, "references", "CollidingType") if d.get("source_file")] assert ref_colliding and all(r["id"] == local_colliding["id"] for r in ref_colliding) # ImportedType reference must bind to Other.ImportedType via using ref_imported = [d for d in _targets(result, "references", "ImportedType") if d.get("source_file")] assert ref_imported and all(r["id"] == imported_type["id"] for r in ref_imported) # UnknownType must remain a sourceless stub ref_missing = _targets(result, "references", "UnknownType") assert ref_missing and not any(r.get("source_file") for r in ref_missing) # ── #4249: a generic and a non-generic type of the same name must not merge ── def test_csharp_generic_and_non_generic_type_of_same_name_get_separate_nodes(tmp_path: Path): """`Effect` / `Effect` (a common base-pair shape: `Comparer`/`Comparer` is another) must not mint the same id, and the resulting inherits edge must not become a self-loop.""" source = _write( tmp_path / "effects.cs", "namespace Demo {\n" " public abstract class Effect { public virtual int ReadSlot(string p) { return 0; } }\n" " public abstract class Effect : Effect where T : unmanaged { }\n" " public abstract class Tween : Effect where T : unmanaged {\n" " public override int ReadSlot(string p) { return base.ReadSlot(p) + 1; }\n" " }\n" "}\n", ) result = extract([source], cache_root=tmp_path) effects = _defs(result, "Effect") assert len(effects) == 2, f"generic and non-generic Effect must be separate nodes: {effects}" inherits = {(e["source"], e["target"]) for e in result["edges"] if e["relation"] == "inherits"} assert not any(src == tgt for src, tgt in inherits), ( f"a generic type inheriting its own non-generic base must never be a self-loop: {inherits}" ) tween = _defs(result, "Tween")[0] effect_targets = {tgt for src, tgt in inherits if src == tween["id"]} assert effect_targets, "Tween must have an inherits edge" assert effect_targets <= {e["id"] for e in effects}, ( "Tween must inherit one of the two real Effect definitions, not a dangling stub" ) def test_csharp_non_generic_pair_with_no_relation_stays_two_plain_nodes(tmp_path: Path): """Control: two same-named non-generic types in different namespaces are already correctly separate — confirms the fix does not touch that case.""" source = _write( tmp_path / "pair.cs", "namespace A { public class Worker {} }\n" "namespace B { public class Worker {} }\n", ) result = extract([source], cache_root=tmp_path) workers = _defs(result, "Worker") assert len(workers) == 2 assert workers[0]["id"] != workers[1]["id"] # ------------------------------------------------------------------------- # Issue #4216: C#: nested types imported with `using static` are resolved # ------------------------------------------------------------------------- def test_csharp_using_static_resolves_nested_type_reference(tmp_path: Path): layer = _write( tmp_path / "layer.cs", "namespace Demo\n{\n public class Layer\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", "using static Demo.Layer;\nnamespace Consumer\n{\n public class Service\n {\n public Record value;\n }\n}\n", ) result = extract([layer, consumer], cache_root=tmp_path) record_defs = _defs(result, "Record") assert len(record_defs) == 1, "nested Record definition must exist" record_def = record_defs[0] resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")] assert resolved, "Record reference must resolve to the real definition via `using static Demo.Layer;`" assert resolved[0]["id"] == record_def["id"] def test_csharp_using_static_resolves_nested_constructor_call(tmp_path: Path): layer = _write( tmp_path / "layer.cs", "namespace Demo\n{\n public class Layer\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", ( "using static Demo.Layer;\n" "namespace Consumer\n" "{\n" " public class Service\n" " {\n" " public void Run()\n" " {\n" " var r = new Record();\n" " }\n" " }\n" "}\n" ), ) result = extract([layer, consumer], cache_root=tmp_path) record_defs = _defs(result, "Record") assert len(record_defs) == 1, "nested Record definition must exist" record_def = record_defs[0] resolved_calls = [t for t in _targets(result, "calls", "Record") if t.get("source_file")] assert resolved_calls, "new Record() call must resolve to nested Record via `using static Demo.Layer;`" assert resolved_calls[0]["id"] == record_def["id"] def test_csharp_using_static_multi_level_nested_type(tmp_path: Path): print_file = _write( tmp_path / "print.cs", ( "namespace Demo\n" "{\n" " public class Print\n" " {\n" " public class Engine\n" " {\n" " public class Info {}\n" " }\n" " }\n" "}\n" ), ) consumer = _write( tmp_path / "consumer.cs", "using static Demo.Print.Engine;\nnamespace Consumer\n{\n public class App\n {\n public Info value;\n }\n}\n", ) result = extract([print_file, consumer], cache_root=tmp_path) info_defs = _defs(result, "Info") assert len(info_defs) == 1, "nested Info definition must exist" info_def = info_defs[0] resolved = [t for t in _targets(result, "references", "Info") if t.get("source_file")] assert resolved, "Info reference must resolve via multi-level `using static Demo.Print.Engine;`" assert resolved[0]["id"] == info_def["id"] def test_csharp_using_static_on_partial_holder_class(tmp_path: Path): slice_a = _write( tmp_path / "layer_a.cs", ( "namespace Demo\n" "{\n" " public partial class Layer\n" " {\n" " public class SliceA {}\n" " public partial class SharedNested {}\n" " }\n" "}\n" ), ) slice_b = _write( tmp_path / "layer_b.cs", ( "namespace Demo\n" "{\n" " public partial class Layer\n" " {\n" " public class SliceB {}\n" " public partial class SharedNested {}\n" " }\n" "}\n" ), ) consumer = _write( tmp_path / "consumer.cs", ( "using static Demo.Layer;\n" "namespace Consumer\n" "{\n" " public class App\n" " {\n" " public SliceA a;\n" " public SliceB b;\n" " public SharedNested s;\n" " }\n" "}\n" ), ) result = extract([slice_a, slice_b, consumer], cache_root=tmp_path) layer_defs = _defs(result, "Layer") assert len(layer_defs) == 1, "top-level partial Layer should merge into one canonical node" layer_nid = layer_defs[0]["id"] contains_targets = { e["target"] for e in result["edges"] if e.get("relation") == "contains" and e.get("source") == layer_nid } slice_a_defs = _defs(result, "SliceA") slice_b_defs = _defs(result, "SliceB") shared_defs = _defs(result, "SharedNested") assert len(slice_a_defs) == 1 assert len(slice_b_defs) == 1 assert len(shared_defs) == 1, "nested partial SharedNested halves merge under the same outer" assert shared_defs[0]["id"] in contains_targets assert slice_a_defs[0]["id"] in contains_targets assert slice_b_defs[0]["id"] in contains_targets resolved_a = [t for t in _targets(result, "references", "SliceA") if t.get("source_file")] assert resolved_a and resolved_a[0]["id"] == slice_a_defs[0]["id"] resolved_b = [t for t in _targets(result, "references", "SliceB") if t.get("source_file")] assert resolved_b and resolved_b[0]["id"] == slice_b_defs[0]["id"] resolved_s = [t for t in _targets(result, "references", "SharedNested") if t.get("source_file")] assert resolved_s, "SharedNested reference must resolve via `using static Demo.Layer;`" assert resolved_s[0]["id"] in {d["id"] for d in shared_defs} def test_csharp_using_static_scoped_to_namespace_block(tmp_path: Path): layer = _write( tmp_path / "layer.cs", "namespace Demo\n{\n public class Layer\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", ( "namespace Good\n" "{\n" " using static Demo.Layer;\n" " public class Service\n" " {\n" " public Record goodValue;\n" " }\n" "}\n" "namespace Bad\n" "{\n" " public class OtherService\n" " {\n" " public Record badValue;\n" " }\n" "}\n" ), ) result = extract([layer, consumer], cache_root=tmp_path) record_defs = _defs(result, "Record") assert len(record_defs) == 1 record_def = record_defs[0] found_good = False found_bad = False for e in result["edges"]: if e.get("relation") != "references": continue tgt = _node_by_id(result, e.get("target", "")) if tgt and tgt.get("label") == "Record": src = _node_by_id(result, e.get("source", "")) src_label = src.get("label", "") if src else "" if src_label == "Service": found_good = True assert tgt.get("source_file"), "Good.Service must resolve Record" assert tgt["id"] == record_def["id"] elif src_label == "OtherService": found_bad = True assert not tgt.get("source_file"), "Bad.OtherService must NOT resolve Record" assert found_good, "Good.Service Record reference edge must be found" assert found_bad, "Bad.OtherService Record reference edge must be found" def test_csharp_unimported_nested_type_dangles(tmp_path: Path): layer = _write( tmp_path / "layer.cs", "namespace Demo\n{\n public class Layer\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", "namespace Consumer\n{\n public class Service\n {\n public Record value;\n }\n}\n", ) result = extract([layer, consumer], cache_root=tmp_path) resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")] assert not resolved, f"unimported nested Record must not resolve: {resolved}" def test_csharp_ambiguous_static_imports_dangle(tmp_path: Path): layer_a = _write( tmp_path / "layer_a.cs", "namespace DemoA\n{\n public class LayerA\n {\n public class Record {}\n }\n}\n", ) layer_b = _write( tmp_path / "layer_b.cs", "namespace DemoB\n{\n public class LayerB\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", ( "using static DemoA.LayerA;\n" "using static DemoB.LayerB;\n" "namespace Consumer\n" "{\n" " public class Service\n" " {\n" " public Record value;\n" " }\n" "}\n" ), ) result = extract([layer_a, layer_b, consumer], cache_root=tmp_path) resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")] assert not resolved, f"ambiguous static imports must fail closed and remain unresolved: {resolved}" def test_csharp_static_import_vs_namespace_conflict_dangles(tmp_path: Path): other = _write( tmp_path / "other.cs", "namespace Other\n{\n public class Record {}\n}\n", ) layer = _write( tmp_path / "layer.cs", "namespace Demo\n{\n public class Layer\n {\n public class Record {}\n }\n}\n", ) consumer = _write( tmp_path / "consumer.cs", ( "using Other;\n" "using static Demo.Layer;\n" "namespace Consumer\n" "{\n" " public class Service\n" " {\n" " public Record value;\n" " }\n" "}\n" ), ) result = extract([other, layer, consumer], cache_root=tmp_path) resolved = [t for t in _targets(result, "references", "Record") if t.get("source_file")] assert not resolved, f"conflict between namespace and static import must fail closed: {resolved}" def test_csharp_external_type_shares_one_placeholder_across_files(tmp_path: Path): # #4247: an external type referenced from several files must get ONE sourceless # placeholder keyed by its name, shared by every file -- not a separate # per-file placeholder (`a_cs_list`, `b_cs_list`) that leaves the files # unconnected. a = _write( tmp_path / "A.cs", "using System.Collections.Generic;\n" "namespace Demo { public class A { public List Items; } }\n", ) b = _write( tmp_path / "B.cs", "using System.Collections.Generic;\n" "namespace Demo { public class B { public List Names; } }\n", ) result = extract([a, b], cache_root=tmp_path) list_stubs = [ n for n in result["nodes"] if n.get("label") == "List" and not n.get("source_file") ] assert len(list_stubs) == 1, ( f"expected one shared sourceless List placeholder, got {len(list_stubs)}: " f"{[n.get('id') for n in list_stubs]}" ) stub_id = list_stubs[0]["id"] referring_files = { e.get("source_file") for e in result["edges"] if e.get("relation") == "references" and e.get("target") == stub_id } # Both A.cs and B.cs reach the one shared placeholder, so the files connect. assert len({str(f).replace("\\", "/").rsplit("/", 1)[-1] for f in referring_files}) == 2, ( f"both files must reference the shared List placeholder: {referring_files}" ) # No per-file placeholder survives alongside the canonical one. assert not any( n.get("label") == "List" and not n.get("source_file") and n.get("id") != stub_id for n in result["nodes"] )