Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions codebase_rag/constants/ast_csharp.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@
TS_CSHARP_CONVERSION_OPERATOR_DECLARATION = "conversion_operator_declaration"
TS_CSHARP_PROPERTY_DECLARATION = "property_declaration"

# (H) Members whose registered leaf name is synthesized (no usable `name` field,
# (H) or one that collides), routed through csharp.utils.synthesize_method_name.
CSHARP_SYNTHESIZED_NAME_TYPES = frozenset(
{
TS_CSHARP_OPERATOR_DECLARATION,
TS_CSHARP_CONVERSION_OPERATOR_DECLARATION,
TS_CSHARP_DESTRUCTOR_DECLARATION,
}
)

# (H) Base spec: `class C : Base, IShape` / `interface I : IOther` /
# (H) `enum E : byte`. A single base_list lumps the base class and interfaces
# (H) together (unlike Java's separate superclass/super_interfaces clauses), so
Expand Down
15 changes: 15 additions & 0 deletions codebase_rag/dead_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ def _is_csharp_dispose_root(name: str, is_method: bool, path: str) -> bool:
)


def _is_csharp_operator_or_finalizer_root(name: str, path: str) -> bool:
# (H) An operator overload is invoked by operator SYNTAX (`a + b`) and a
# (H) finalizer (`~Foo`) by the GC -- never a named call the graph sees -- so
# (H) both are reachability roots on a .cs file (cf. the C++ operator root).
# (H) The synthesized leaf carries the `operator_`/`~` prefix.
return path.endswith(cs.EXT_CS) and (
name.startswith(cs.TS_CSHARP_OPERATOR_NAME_PREFIX)
or name.startswith(cs.TS_CSHARP_DESTRUCTOR_NAME_PREFIX)
)


def _matches_test_path(path: str, patterns: tuple[str, ...]) -> bool:
# (H) Match test-path patterns against a leading-slash-normalized path so a dir
# (H) pattern like `/tests/` also matches a ROOT `tests/` dir (Rust integration
Expand Down Expand Up @@ -283,6 +294,10 @@ def dead_code_from_graph(
str(props.get(cs.KEY_PATH, "")),
):
roots.add(qn)
elif _is_csharp_operator_or_finalizer_root(
leaf, str(props.get(cs.KEY_PATH, ""))
):
roots.add(qn)
elif any(qn.endswith(entry) for entry in config.entry_points):
roots.add(qn)
elif config.include_tests and _matches_test_path(
Expand Down
30 changes: 8 additions & 22 deletions codebase_rag/language_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,28 +173,14 @@ def _csharp_get_name(node: Node) -> str | None:
if name_node and name_node.text:
return name_node.text.decode(cs.ENCODING_UTF8)
return None
# (H) Operators expose no `name` field; synthesize a stable qn segment from
# (H) the operator symbol (or conversion target type) so the node still gets
# (H) a qn instead of being dropped.
if node.type == cs.TS_CSHARP_OPERATOR_DECLARATION:
op_node = node.child_by_field_name(cs.TS_CSHARP_FIELD_OPERATOR)
if op_node and op_node.text:
return cs.TS_CSHARP_OPERATOR_NAME_PREFIX + op_node.text.decode(
cs.ENCODING_UTF8
)
return None
if node.type == cs.TS_CSHARP_CONVERSION_OPERATOR_DECLARATION:
type_node = node.child_by_field_name(cs.TS_CSHARP_FIELD_TYPE)
if type_node and type_node.text:
return cs.TS_CSHARP_OPERATOR_NAME_PREFIX + type_node.text.decode(
cs.ENCODING_UTF8
)
return None
# (H) A ctor and dtor both take the type's name; prefix `~` on the dtor so
# (H) `Foo()` and `~Foo()` don't collapse onto one qn.
if node.type == cs.TS_CSHARP_DESTRUCTOR_DECLARATION:
base = _generic_get_name(node)
return cs.TS_CSHARP_DESTRUCTOR_NAME_PREFIX + base if base else None
# (H) Operators expose no `name` field and a destructor's `name` collides
# (H) with the constructor; delegate to the shared synthesizer so the FQN
# (H) scope walk and the registered node qn agree. Local import avoids a
# (H) module-load cycle (csharp.utils -> parsers.utils).
if node.type in cs.CSHARP_SYNTHESIZED_NAME_TYPES:
from .parsers.csharp import utils as csharp_utils

return csharp_utils.synthesize_method_name(node)
return _generic_get_name(node)


Expand Down
35 changes: 29 additions & 6 deletions codebase_rag/parsers/csharp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,34 @@ def build_field_type_map(class_node: Node) -> dict[str, str]:
return fields


def extract_method_signature(method_node: Node) -> tuple[str | None, list[str]]:
# (H) (method name, parameter type names). The name is the `name` field, the
# (H) same leaf ingest_method registers, so the signatured qn stays consistent.
# (H) Operators/destructors have no `name` field -> (None, ...), and the
# (H) caller leaves them with their bare synthesized name.
def synthesize_method_name(method_node: Node) -> str | None:
# (H) The registered leaf name for a C# member. Operators expose no `name`
# (H) field, so synthesize `operator_<symbol>` (binary/unary operators) or
# (H) `operator_<target-type>` (conversion operators). A destructor HAS a
# (H) `name` field equal to the type name, which would collide with the
# (H) constructor, so prefix `~`. Everything else uses the plain `name` leaf.
# (H) Kept identical to _csharp_get_name so the FQN scope walk and the node
# (H) qn agree.
if method_node.type == cs.TS_CSHARP_OPERATOR_DECLARATION:
op_node = method_node.child_by_field_name(cs.TS_CSHARP_FIELD_OPERATOR)
symbol = safe_decode_text(op_node) if op_node and op_node.text else None
return cs.TS_CSHARP_OPERATOR_NAME_PREFIX + symbol if symbol else None
if method_node.type == cs.TS_CSHARP_CONVERSION_OPERATOR_DECLARATION:
type_node = method_node.child_by_field_name(cs.TS_CSHARP_FIELD_TYPE)
target = safe_decode_text(type_node) if type_node and type_node.text else None
return cs.TS_CSHARP_OPERATOR_NAME_PREFIX + target if target else None
name_node = method_node.child_by_field_name(cs.FIELD_NAME)
name = safe_decode_text(name_node) if name_node and name_node.text else None
return name, extract_parameter_type_names(method_node)
if name and method_node.type == cs.TS_CSHARP_DESTRUCTOR_DECLARATION:
return cs.TS_CSHARP_DESTRUCTOR_NAME_PREFIX + name
return name


def extract_method_signature(method_node: Node) -> tuple[str | None, list[str]]:
# (H) (method name, parameter type names). The name matches the leaf
# (H) ingest_method registers (synthesized for operators/destructors), so the
# (H) signatured qn stays consistent. Overloaded operators (`operator +` on
# (H) two operand types) still get distinct qns via the parameter signature.
return synthesize_method_name(method_node), extract_parameter_type_names(
method_node
)
9 changes: 9 additions & 0 deletions codebase_rag/parsers/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,15 @@ def ingest_method(
method_name = cpp_utils.extract_function_name(method_node)
if not method_name:
return None
elif language == cs.SupportedLanguage.CSHARP:
# (H) Operators expose no `name` field (they would be dropped) and a
# (H) destructor's `name` field collides with the constructor; synthesize
# (H) the leaf so both register with the same name the FQN walk uses.
from .csharp import utils as csharp_utils

method_name = csharp_utils.synthesize_method_name(method_node)
if not method_name:
return None
elif language == cs.SupportedLanguage.DART:
# (H) Constructors/factories expose no `name` field; take the last bare
# (H) identifier (`factory C.empty` -> `empty`) so they are not dropped.
Expand Down
16 changes: 16 additions & 0 deletions codebase_rag/tests/test_csharp_dead_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,19 @@ def test_attribute_and_dispose_methods_are_roots() -> None:
assert "proj.Svc.Get" not in dead
assert "proj.Svc.Bracketed" not in dead
assert "proj.Svc.Dispose" not in dead


def test_operator_and_finalizer_are_roots() -> None:
# (H) An operator overload is invoked by operator syntax (`a + b`) and a
# (H) finalizer (`~Svc`) by the GC, never a named call the graph sees, so
# (H) neither may be reported dead despite having no incoming CALLS.
nodes = [
_method("proj.Svc.operator_+(int, int)", "operator_+"),
_method("proj.Svc.~Svc", "~Svc"),
_method("proj.Svc.Helper", "Helper"),
]
dead = _dead(FakeIngestor(nodes, []))

assert "proj.Svc.Helper" in dead
assert "proj.Svc.operator_+(int, int)" not in dead
assert "proj.Svc.~Svc" not in dead
36 changes: 36 additions & 0 deletions codebase_rag/tests/test_csharp_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,42 @@ def test_members_generics_and_operators(
assert _endswith_any(members, "N.Box.Size")
# (H) Constructor is named after the type; destructor is distinct from it.
assert _endswith_any(members, "N.Box.Box(T)")
# (H) Operators and destructors must register as members too. The operator
# (H) has no `name` field (synthesize `operator_<symbol>` + signature so
# (H) overloaded operators stay distinct); the destructor's identifier
# (H) collides with the ctor unless prefixed with `~`.
assert _endswith_any(members, "N.Box.operator_+(Box, Box)")
assert _endswith_any(members, "N.Box.~Box")


def test_operator_overloads_and_conversions_are_distinct(
csharp_project: Path, mock_ingestor: MagicMock
) -> None:
(csharp_project / "Ops.cs").write_text(
"""
namespace N;
public struct Vec {
public int X;
public static Vec operator +(Vec a, Vec b) => a;
public static Vec operator +(Vec a, int b) => a;
public static explicit operator int(Vec v) => v.X;
public static implicit operator string(Vec v) => "";
}
""",
encoding="utf-8",
)
run_updater(csharp_project, mock_ingestor, skip_if_missing=SKIP)

members = get_node_names(mock_ingestor, NodeType.METHOD) | get_node_names(
mock_ingestor, NodeType.FUNCTION
)
# (H) Two `operator +` overloads differ only by parameter type, so the
# (H) signature must keep them as two nodes (not one @line-suffixed collision).
assert _endswith_any(members, "N.Vec.operator_+(Vec, Vec)")
assert _endswith_any(members, "N.Vec.operator_+(Vec, int)")
# (H) Conversion operators are named by their target type.
assert _endswith_any(members, "N.Vec.operator_int(Vec)")
assert _endswith_any(members, "N.Vec.operator_string(Vec)")


def test_nested_types(csharp_project: Path, mock_ingestor: MagicMock) -> None:
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading