mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 16:56:36 +00:00
fix(csharp): walk generic type arguments in field position
A type used as a generic argument in a field declaration (private List<Order> _orders) did not emit a references edge to the argument type, so field-injected dependencies were invisible. Walk the field's type through the shared type-ref collector, bringing fields to parity with the property handler; builtins and in-scope type parameters are not fabricated.
This commit is contained in:
@@ -3714,13 +3714,29 @@ def _extract_generic(
|
||||
if name_node is not None:
|
||||
fields[_read_text(name_node, source)] = type_name
|
||||
line = node.start_point[0] + 1
|
||||
metadata = {"ref_token": type_name}
|
||||
if qualified:
|
||||
metadata["qualified"] = True
|
||||
if qualifier:
|
||||
metadata["ref_qualifier"] = qualifier
|
||||
add_edge(parent_class_nid, ensure_named_node(type_name, line),
|
||||
"references", line, context="field", metadata=metadata)
|
||||
# Walk the whole type expression rather than only its outer name, so
|
||||
# `Box<Widget>` yields the Box field ref AND the Widget generic_arg ref.
|
||||
# Reading just the outer name left every generic argument in field
|
||||
# position unlinked -- `IDbContextFactory<SomeContext>` lost SomeContext,
|
||||
# and `Mock<IThing>` lost IThing across entire test suites. The C#
|
||||
# property_declaration handler below and the tree_sitter_java
|
||||
# field_declaration handler beside it already do exactly this; C# fields
|
||||
# were the odd one out.
|
||||
refs: list[tuple[str, str, bool, str]] = []
|
||||
_csharp_collect_type_refs(
|
||||
type_node, source, False, refs, csharp_type_params
|
||||
)
|
||||
for ref_name, role, ref_qualified, ref_qualifier in refs:
|
||||
ctx = "generic_arg" if role == "generic_arg" else "field"
|
||||
target_nid = ensure_named_node(ref_name, line)
|
||||
if target_nid != parent_class_nid:
|
||||
metadata = {"ref_token": ref_name}
|
||||
if ref_qualified:
|
||||
metadata["qualified"] = True
|
||||
if ref_qualifier:
|
||||
metadata["ref_qualifier"] = ref_qualifier
|
||||
add_edge(parent_class_nid, target_nid, "references",
|
||||
line, context=ctx, metadata=metadata)
|
||||
return
|
||||
|
||||
if (config.ts_module == "tree_sitter_c_sharp"
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
"""C# generic type arguments in FIELD position.
|
||||
|
||||
The field_declaration handler read only the outer type name, so the inner argument of
|
||||
`Box<Widget>` produced no edge. Properties, return types and parameters already walked
|
||||
the full type expression via _csharp_collect_type_refs, which made fields the odd one
|
||||
out for two very common shapes: `IDbContextFactory<SomeContext>` lost SomeContext, and
|
||||
`Mock<IThing>` lost IThing across whole test suites.
|
||||
|
||||
Missing edges here are silent -- `affected` returns a smaller, confident answer rather
|
||||
than an error -- so each case asserts the edge exists rather than asserting a count.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _refs(tmp_path, files: dict[str, str]) -> set[tuple[str, str]]:
|
||||
"""Extract, returning {(source_label, target_label)} for `references` edges."""
|
||||
for name, body in files.items():
|
||||
p = tmp_path / name
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(body)
|
||||
old = os.getcwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
r = extract([Path(n) for n in files], cache_root=tmp_path / ".cache")
|
||||
finally:
|
||||
os.chdir(old)
|
||||
labels = {n["id"]: n.get("label", "") for n in r["nodes"]}
|
||||
return {
|
||||
(labels.get(e["source"], ""), labels.get(e["target"], ""))
|
||||
for e in r["edges"]
|
||||
if e["relation"] == "references"
|
||||
}
|
||||
|
||||
|
||||
_TYPES = (
|
||||
"public interface IThing { }\n"
|
||||
"public class Box<T> { }\n"
|
||||
"public class Outer { public class Inner { } }\n"
|
||||
)
|
||||
|
||||
|
||||
def test_field_generic_argument_produces_edge(tmp_path):
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe { private Box<IThing> _f = null!; }\n",
|
||||
})
|
||||
assert ("Probe", "Box") in refs, "outer field type must still be linked"
|
||||
assert ("Probe", "IThing") in refs, "generic argument in field position must be linked"
|
||||
|
||||
|
||||
def test_field_matches_property_behaviour(tmp_path):
|
||||
"""A field and a property of the same type must produce the same references."""
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": (
|
||||
"public class WithField { private Box<IThing> _f = null!; }\n"
|
||||
"public class WithProp { public Box<IThing> P { get; set; } = null!; }\n"
|
||||
),
|
||||
})
|
||||
field_targets = {t for s, t in refs if s == "WithField"}
|
||||
prop_targets = {t for s, t in refs if s == "WithProp"}
|
||||
assert field_targets == prop_targets, (
|
||||
f"field and property disagree: field={field_targets} property={prop_targets}"
|
||||
)
|
||||
|
||||
|
||||
def test_nested_generic_arguments(tmp_path):
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe { private Box<Box<IThing>> _f = null!; }\n",
|
||||
})
|
||||
assert ("Probe", "IThing") in refs, "innermost generic argument must be linked"
|
||||
|
||||
|
||||
def test_multiple_declarators_share_the_type(tmp_path):
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe { private Box<IThing> _a = null!, _b = null!; }\n",
|
||||
})
|
||||
assert ("Probe", "IThing") in refs
|
||||
|
||||
|
||||
def test_bare_type_parameter_is_not_fabricated(tmp_path):
|
||||
"""`T item` must not create a node for the type parameter itself."""
|
||||
refs = _refs(tmp_path, {
|
||||
"P.cs": "public class Probe<T> { private T _item = default!; }\n",
|
||||
})
|
||||
assert not any(t == "T" for _, t in refs), "type parameter must not become a node"
|
||||
|
||||
|
||||
def test_type_parameter_as_generic_argument_is_not_fabricated(tmp_path):
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe<T> { private Box<T> _f = null!; }\n",
|
||||
})
|
||||
assert ("Probe", "Box") in refs, "outer type is still a real reference"
|
||||
assert not any(t == "T" for _, t in refs), "type parameter must not become a node"
|
||||
|
||||
|
||||
def test_predefined_types_are_not_fabricated(tmp_path):
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe { private Box<int> _f = null!; private string _s = \"\"; }\n",
|
||||
})
|
||||
assert not any(t in {"int", "string"} for _, t in refs), (
|
||||
"builtin types must not become nodes"
|
||||
)
|
||||
|
||||
|
||||
def test_plain_field_still_links(tmp_path):
|
||||
"""The non-generic path must be unchanged."""
|
||||
refs = _refs(tmp_path, {
|
||||
"T.cs": _TYPES,
|
||||
"P.cs": "public class Probe { private IThing _f = null!; }\n",
|
||||
})
|
||||
assert ("Probe", "IThing") in refs
|
||||
Reference in New Issue
Block a user