fix(csharp): resolve nested types from using static

This commit is contained in:
himanshupatro-334 committed 2026-10-08 19:01:30 +05:30
1 parent 6478eb7123
commit b66040d902
2 files changed
+358 -3

No files matched your search

+76
View File
@@ -219,6 +219,9 @@ class CsharpNameResolver:
self.namespace_usings_by_file: dict[str, list[tuple[str, str, str | None]]] = {}
self.aliases_by_file: dict[str, dict[str, list[tuple[str, str, str | None]]]] = {}
self.static_usings_by_file: dict[str, list[tuple[str, str, str | None]]] = {}
self.nested_types_by_holder: dict[str, dict[str, str]] = {}
self._holder_nid_cache: dict[str, str | None] = {}
for edge in all_edges:
if edge.get("relation") != "imports":
@@ -252,6 +255,44 @@ class CsharpNameResolver:
bucket = self.aliases_by_file.setdefault(source_file, {}).setdefault(alias, [])
if entry not in bucket:
bucket.append(entry)
elif using_kind == "static":
entry = (target_fqn, scope_kind, scope_id)
bucket = self.static_usings_by_file.setdefault(source_file, [])
if entry not in bucket:
bucket.append(entry)
nested_candidates: dict[tuple[str, str], dict[str, dict]] = {}
for edge in all_edges:
if edge.get("relation") != "contains":
continue
target_nid = edge.get("target")
if not isinstance(target_nid, str) or not target_nid:
continue
target_node = self.node_by_id.get(target_nid)
if not (
target_node
and target_node.get("file_type") == "code"
and _metadata(target_node.get("metadata")).get("is_nested_type") is True
):
continue
label = target_node.get("label")
if not isinstance(label, str) or not label:
continue
holder_nid = edge.get("source")
if not isinstance(holder_nid, str) or not holder_nid:
continue
nested_candidates.setdefault((holder_nid, label), {})[target_nid] = target_node
for (holder_nid, label), node_dict in nested_candidates.items():
canonical_nid = sorted(
node_dict.values(),
key=lambda n: (
str(n.get("source_file") or ""),
str(n.get("source_location") or ""),
str(n.get("id") or ""),
),
)[0]["id"]
self.nested_types_by_holder.setdefault(holder_nid, {})[label] = canonical_nid
@staticmethod
def _namespace(node: dict | None) -> str:
@@ -299,6 +340,34 @@ class CsharpNameResolver:
hits.add(hit)
return next(iter(hits)) if len(hits) == 1 else None
def _resolve_holder_nid(self, target_fqn: str) -> str | None:
base_fqn = _strip_trailing_csharp_generic_args(html.unescape(target_fqn)).strip()
if not base_fqn:
return None
if base_fqn in self._holder_nid_cache:
return self._holder_nid_cache[base_fqn]
prefix, sep, name = base_fqn.rpartition(".")
if not sep:
nid = self.type_def_index.get(("", base_fqn))
self._holder_nid_cache[base_fqn] = nid
return nid
top_hit = self.type_def_index.get((prefix, name))
if top_hit:
self._holder_nid_cache[base_fqn] = top_hit
return top_hit
parent_nid = self._resolve_holder_nid(prefix)
if parent_nid:
nested_hit = self.nested_types_by_holder.get(parent_nid, {}).get(name)
if nested_hit:
self._holder_nid_cache[base_fqn] = nested_hit
return nested_hit
self._holder_nid_cache[base_fqn] = None
return None
def resolve_type_name(
self, label: str, source_node: dict, source_file: str
) -> tuple[str | None, bool]:
@@ -319,6 +388,13 @@ class CsharpNameResolver:
hit = self.type_def_index.get((namespace, label))
if hit and hit not in candidates:
candidates.append(hit)
for target_fqn, scope_kind, scope_id in self.static_usings_by_file.get(source_file, []):
if self._using_in_scope(scope_kind, scope_id, source_node):
holder_nid = self._resolve_holder_nid(target_fqn)
if holder_nid:
hit = self.nested_types_by_holder.get(holder_nid, {}).get(label)
if hit and hit not in candidates:
candidates.append(hit)
if len(candidates) == 1:
return candidates[0], True
return None, bool(candidates)
+282 -3
View File
@@ -312,7 +312,7 @@ def test_csharp_nested_type_carries_metadata(tmp_path: Path):
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.
# 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")]
@@ -320,7 +320,7 @@ def test_csharp_cross_namespace_ref_not_misbound(tmp_path: Path):
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).
# 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")]
@@ -328,7 +328,7 @@ def test_csharp_same_file_cross_namespace_ref_not_misbound(tmp_path: Path):
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).
# 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"}
@@ -819,3 +819,282 @@ def test_csharp_scope_metadata_does_not_depend_on_line_endings(tmp_path: Path):
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"
)
# -------------------------------------------------------------------------
# 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) == 2, "nested partial SharedNested declarations remain unmerged in graph"
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}"