mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
fix(csharp): keep names that differ only by _ or case apart, in ids and in member-call keys (#4415)
make_id strips leading/trailing underscores and casefolds, while C# is case-sensitive and spells private members `_Name`. In one file, `Action()` / `_Action()`, `Size` / `_Size`, `Load()` / `load()` and the types `Pair` / `_Pair` minted one id; add_node kept the first declaration and dropped the second, and the dropped method's calls were credited to the survivor. Apply the rule of #3302 to C#: a pre-scan groups the declared names of a file (types; per type its methods, properties and enum members) that would share an id, and _underscore_salted_nid (renamed from the Python helper, unchanged) keeps the plain id on the one name without a leading underscore and salts the rest. In-file type references and base lists go through the same rule. A name without a colliding sibling keeps its id. The C# member-call resolver keyed names by lowercased alphanumerics, so `this.Put()` bound to `_Put()` on a nearer base, and `Token` / `TOKEN` were one type key. The key now keeps `_` and case. Fixes #4415. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e8eb76e797
commit
914dda60cf
5 files changed
+441
-28
No files matched your search
+4
-1
@@ -5365,7 +5365,10 @@ def _resolve_csharp_member_calls(
|
||||
Must run after id-disambiguation so node ids and caller_nids are final.
|
||||
"""
|
||||
def _key(label: str) -> str:
|
||||
return re.sub(r"[^a-zA-Z0-9]+", "", str(label)).lower()
|
||||
# Case and `_` are part of a C# name: `Put` and `_Put` are two
|
||||
# methods, so `this.Put()` must not bind to `_Put` (#4415). Only the
|
||||
# label decoration (`.Put()`) goes.
|
||||
return re.sub(r"\W+", "", str(label))
|
||||
|
||||
contained = {e.get("target") for e in all_edges if e.get("relation") == "contains"}
|
||||
|
||||
|
||||
+144
-21
@@ -168,6 +168,107 @@ def _csharp_pre_scan_interfaces(root_node, source: bytes) -> set[str]:
|
||||
stack.extend(n.children)
|
||||
return out
|
||||
|
||||
_CSHARP_TYPE_DECLARATIONS = frozenset({
|
||||
"class_declaration",
|
||||
"interface_declaration",
|
||||
"enum_declaration",
|
||||
"struct_declaration",
|
||||
"record_declaration",
|
||||
})
|
||||
|
||||
|
||||
def _csharp_type_name_for_id(node, class_name: str) -> str:
|
||||
"""A C# type's name as its node id spells it: the arity folds in (#4249).
|
||||
|
||||
`Effect` and `Effect<T>` are different types, so the generic one is named
|
||||
the way C# metadata names it (`` Effect`1 ``); arity 0 is the bare name.
|
||||
"""
|
||||
arity = sum(
|
||||
1 for c in node.children if c.type == "type_parameter_list"
|
||||
for _p in c.children if _p.type in ("type_parameter", "identifier")
|
||||
)
|
||||
return f"{class_name}`{arity}" if arity else class_name
|
||||
|
||||
|
||||
def _csharp_pre_scan_name_collisions(
|
||||
root_node, source: bytes, stem: str
|
||||
) -> tuple[dict[str, set[str]], dict[str, set[str]]]:
|
||||
"""Pre-scan a C# file for declared names that collapse to one node id.
|
||||
|
||||
C# is case-sensitive and `_Name` is the usual spelling of a private member,
|
||||
but ``ids.py:make_id`` strips leading/trailing underscores and casefolds, so
|
||||
`Action()` / `_Action()`, `Size` / `_Size`, `Load()` / `load()` and the types
|
||||
`Pair` / `_Pair` each mint ONE id. ``add_node`` keeps the first declaration
|
||||
and drops the second; the dropped method's calls are credited to the
|
||||
survivor, and a type's members hang off the other type (#4415). This is
|
||||
the Python #3302 collision in C#; the outcome is the same rule
|
||||
(``_underscore_salted_nid``).
|
||||
|
||||
Returns ``(type_groups, member_groups)``, each ``{plain_nid: {raw names}}``
|
||||
for the ids two or more distinct names would share. Types are keyed the way
|
||||
the walk mints them (``_make_id(stem, namespace, name_for_id)``), members
|
||||
(methods, properties, enum members) by ``_make_id(type_nid, name)`` where
|
||||
``type_nid`` is the type's FINAL id, already salted when the type itself
|
||||
collides. Same-file partial declarations of one type share an id, so their
|
||||
members are grouped together. Overloads are one name, never a collision.
|
||||
"""
|
||||
type_names: dict[str, set[str]] = {}
|
||||
member_names: dict[str, set[str]] = {}
|
||||
type_decls: list[tuple[object, str, str]] = []
|
||||
|
||||
def _visit(n, ns: list[str]) -> None:
|
||||
# Types are declared only at file level, in namespace and type bodies
|
||||
# and in #if wrappers around them, so only those are entered: the
|
||||
# recursion depth is the nesting depth of declarations, never of
|
||||
# expressions.
|
||||
scoped = list(ns)
|
||||
for c in n.children:
|
||||
if c.type == "namespace_declaration":
|
||||
ns_name = _csharp_namespace_name(c, source)
|
||||
body = c.child_by_field_name("body")
|
||||
if body is not None:
|
||||
_visit(body, scoped + [ns_name] if ns_name else scoped)
|
||||
elif c.type == "file_scoped_namespace_declaration":
|
||||
# Applies to the rest of the file, as in the walk.
|
||||
ns_name = _csharp_namespace_name(c, source)
|
||||
if ns_name:
|
||||
scoped = scoped + [ns_name]
|
||||
elif c.type in _CSHARP_TYPE_DECLARATIONS:
|
||||
name_node = c.child_by_field_name("name")
|
||||
name = _read_text(name_node, source) if name_node is not None else ""
|
||||
if name:
|
||||
plain = _make_id(stem, ".".join(scoped), _csharp_type_name_for_id(c, name))
|
||||
type_names.setdefault(plain, set()).add(name)
|
||||
type_decls.append((c, plain, name))
|
||||
# Nested types take the namespace, not the enclosing type (ids
|
||||
# omit the outer type), so they join the same groups.
|
||||
_visit(c, scoped)
|
||||
elif c.type == "declaration_list" or c.type.startswith("preproc_"):
|
||||
_visit(c, scoped)
|
||||
|
||||
_visit(root_node, [])
|
||||
type_groups = {nid: names for nid, names in type_names.items() if len(names) >= 2}
|
||||
|
||||
def _members(n, type_nid: str) -> None:
|
||||
for c in n.children:
|
||||
if c.type in ("method_declaration", "property_declaration",
|
||||
"enum_member_declaration"):
|
||||
name_node = c.child_by_field_name("name")
|
||||
name = _read_text(name_node, source) if name_node is not None else ""
|
||||
if name:
|
||||
member_names.setdefault(_make_id(type_nid, name), set()).add(name)
|
||||
elif c.type in ("declaration_list", "enum_member_declaration_list") or (
|
||||
c.type.startswith("preproc_")
|
||||
):
|
||||
# The body, and #if/#region wrappers the walk sees through (#2631).
|
||||
_members(c, type_nid)
|
||||
|
||||
for decl, plain, name in type_decls:
|
||||
_members(decl, _underscore_salted_nid(plain, name, type_groups))
|
||||
member_groups = {nid: names for nid, names in member_names.items() if len(names) >= 2}
|
||||
return type_groups, member_groups
|
||||
|
||||
|
||||
def _csharp_classify_base(name: str, interface_names: set[str]) -> str:
|
||||
"""`implements` if the base name is an interface (declared or by I-prefix convention), else `inherits`."""
|
||||
if name in interface_names:
|
||||
@@ -1189,10 +1290,11 @@ def _python_pre_scan_underscore_collisions(root_node, source: bytes, stem: str)
|
||||
return {nid: names for nid, names in groups.items() if len(names) >= 2}
|
||||
|
||||
|
||||
def _python_underscore_salted_nid(plain_nid: str, name: str, groups: dict[str, set[str]]) -> str:
|
||||
"""Resolve a Python function/method's real node id against the collision map.
|
||||
def _underscore_salted_nid(plain_nid: str, name: str, groups: dict[str, set[str]]) -> str:
|
||||
"""Resolve a declaration's real node id against the collision map.
|
||||
|
||||
A name with no leading underscore at all is "public" per PEP 8 convention.
|
||||
Shared by the Python and C# pre-scans (#3302, #4415). A name with no
|
||||
leading underscore at all is "public" per PEP 8 convention.
|
||||
When a collision group has exactly one public member, that member keeps the
|
||||
plain id — cross-file/documentation references overwhelmingly target the
|
||||
public name, and keeping it stable means an incremental rebuild that adds or
|
||||
@@ -3999,7 +4101,8 @@ def _csharp_namespace_name(node, source: bytes) -> str:
|
||||
def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
||||
nodes: list, edges: list, seen_ids: set, function_bodies: list,
|
||||
parent_class_nid: str | None, add_node_fn, add_edge_fn,
|
||||
walk_fn, namespace_stack: list[str], scope_stack: list[str]) -> bool:
|
||||
walk_fn, namespace_stack: list[str], scope_stack: list[str],
|
||||
member_groups: dict[str, set[str]] | None = None) -> bool:
|
||||
"""Handle C# namespaces, enum members, and transparent class-member wrappers."""
|
||||
if node.type == "enum_member_declaration" and parent_class_nid:
|
||||
# `enum_declaration` is in C#'s class_types, so the enum type is a node
|
||||
@@ -4015,11 +4118,13 @@ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path:
|
||||
if not member_name:
|
||||
return True
|
||||
line = node.start_point[0] + 1
|
||||
member_nid = _make_id(parent_class_nid, member_name)
|
||||
# C# is case-sensitive, so `enum E { Value, value }` is legal, but the id
|
||||
# recipe casefolds — both members normalize to one id. Emitting the
|
||||
# second would hang a duplicate edge on the first member's node, so the
|
||||
# first declaration keeps it (same guard as the property nodes in #3006).
|
||||
# recipe casefolds — both members normalize to one id. The pre-scan
|
||||
# salts such a pair apart (#4415); the guard stays for anything else
|
||||
# that lands on one id.
|
||||
member_nid = _underscore_salted_nid(
|
||||
_make_id(parent_class_nid, member_name), member_name, member_groups or {}
|
||||
)
|
||||
if member_nid not in seen_ids:
|
||||
add_node_fn(member_nid, member_name, line)
|
||||
add_edge_fn(parent_class_nid, member_nid, "case_of", line)
|
||||
@@ -5285,8 +5390,13 @@ def _extract_generic(
|
||||
csharp_method_returns: dict[str, dict[str, list]] = {}
|
||||
|
||||
csharp_interface_names: set[str] = set()
|
||||
csharp_type_groups: dict[str, set[str]] = {}
|
||||
csharp_member_groups: dict[str, set[str]] = {}
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
csharp_interface_names = _csharp_pre_scan_interfaces(root, source)
|
||||
csharp_type_groups, csharp_member_groups = _csharp_pre_scan_name_collisions(
|
||||
root, source, stem
|
||||
)
|
||||
|
||||
swift_protocol_names: set[str] = set()
|
||||
swift_class_names: set[str] = set()
|
||||
@@ -5341,6 +5451,8 @@ def _extract_generic(
|
||||
|
||||
def ensure_named_node(name: str, line: int) -> str:
|
||||
nid = _make_id(stem, ".".join(namespace_stack), name)
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
nid = _underscore_salted_nid(nid, name, csharp_type_groups)
|
||||
if nid in seen_ids:
|
||||
return nid
|
||||
nid = _make_id(name)
|
||||
@@ -5487,13 +5599,11 @@ def _extract_generic(
|
||||
# matching how C# metadata itself names a generic type
|
||||
# (`` Effect`1 ``), so each arity gets its own node; the
|
||||
# label is untouched; arity 0 keeps today's id exactly.
|
||||
_csharp_arity = sum(
|
||||
1 for c in node.children if c.type == "type_parameter_list"
|
||||
for _p in c.children if _p.type in ("type_parameter", "identifier")
|
||||
)
|
||||
if _csharp_arity:
|
||||
class_name_for_id = f"{class_name}`{_csharp_arity}"
|
||||
class_name_for_id = _csharp_type_name_for_id(node, class_name)
|
||||
class_nid = _make_id(stem, ".".join(namespace_stack), class_name_for_id)
|
||||
if config.ts_module == "tree_sitter_c_sharp":
|
||||
# `Pair` / `_Pair` in one file share an id otherwise (#4415).
|
||||
class_nid = _underscore_salted_nid(class_nid, class_name, csharp_type_groups)
|
||||
if config.ts_module == "tree_sitter_python" and parent_class_nid:
|
||||
class_nid = _make_id(parent_class_nid, class_name)
|
||||
line = node.start_point[0] + 1
|
||||
@@ -5930,13 +6040,19 @@ def _extract_generic(
|
||||
)
|
||||
base_nid = None
|
||||
if base_arity:
|
||||
_arity_nid = _make_id(
|
||||
stem, ".".join(namespace_stack), f"{base}`{base_arity}"
|
||||
_arity_nid = _underscore_salted_nid(
|
||||
_make_id(
|
||||
stem, ".".join(namespace_stack), f"{base}`{base_arity}"
|
||||
),
|
||||
base, csharp_type_groups,
|
||||
)
|
||||
if _arity_nid in seen_ids:
|
||||
base_nid = _arity_nid
|
||||
if base_nid is None:
|
||||
base_nid = _make_id(stem, ".".join(namespace_stack), base)
|
||||
base_nid = _underscore_salted_nid(
|
||||
_make_id(stem, ".".join(namespace_stack), base),
|
||||
base, csharp_type_groups,
|
||||
)
|
||||
if base_nid not in seen_ids:
|
||||
base_nid = _make_id(base)
|
||||
if base_nid not in seen_ids:
|
||||
@@ -6470,7 +6586,10 @@ def _extract_generic(
|
||||
property_name = _read_text(prop_node_name, source)
|
||||
if property_name:
|
||||
property_line = node.start_point[0] + 1
|
||||
property_nid = _make_id(parent_class_nid, property_name)
|
||||
property_nid = _underscore_salted_nid(
|
||||
_make_id(parent_class_nid, property_name),
|
||||
property_name, csharp_member_groups,
|
||||
)
|
||||
if property_nid not in seen_ids:
|
||||
add_node(property_nid, property_name, property_line)
|
||||
add_edge(parent_class_nid, property_nid, "defines",
|
||||
@@ -6925,9 +7044,13 @@ def _extract_generic(
|
||||
if parent_class_nid:
|
||||
func_nid = _make_id(parent_class_nid, sanitized_name)
|
||||
if config.ts_module == "tree_sitter_python":
|
||||
func_nid = _python_underscore_salted_nid(
|
||||
func_nid = _underscore_salted_nid(
|
||||
func_nid, sanitized_name, python_underscore_groups
|
||||
)
|
||||
elif config.ts_module == "tree_sitter_c_sharp":
|
||||
func_nid = _underscore_salted_nid(
|
||||
func_nid, sanitized_name, csharp_member_groups
|
||||
)
|
||||
ruby_method_metadata = None
|
||||
if ruby_method_kind is not None:
|
||||
kinds = ruby_method_kinds.setdefault(func_nid, set())
|
||||
@@ -6962,7 +7085,7 @@ def _extract_generic(
|
||||
else:
|
||||
func_nid = _make_id(stem, sanitized_name)
|
||||
if config.ts_module == "tree_sitter_python":
|
||||
func_nid = _python_underscore_salted_nid(
|
||||
func_nid = _underscore_salted_nid(
|
||||
func_nid, sanitized_name, python_underscore_groups
|
||||
)
|
||||
add_node(func_nid, f"{func_name}()", line)
|
||||
@@ -7532,7 +7655,7 @@ def _extract_generic(
|
||||
if _csharp_extra_walk(node, source, file_nid, stem, str_path,
|
||||
nodes, edges, seen_ids, function_bodies,
|
||||
parent_class_nid, add_node, add_edge, walk,
|
||||
namespace_stack, scope_stack):
|
||||
namespace_stack, scope_stack, csharp_member_groups):
|
||||
return
|
||||
|
||||
if config.ts_module == "tree_sitter_swift":
|
||||
|
||||
@@ -76,15 +76,18 @@ def test_an_explicit_value_does_not_change_the_member(tmp_path):
|
||||
|
||||
|
||||
def test_case_only_sibling_members_do_not_duplicate_an_edge(tmp_path):
|
||||
# Legal C#, and one id after normalization. The first declaration keeps the
|
||||
# node rather than a second edge hanging off it.
|
||||
# Legal C#, and one id after normalization. Each member gets its own node
|
||||
# (#4415) with its own edge, rather than a second edge hanging off the
|
||||
# first member's node.
|
||||
case_of, r = _extract(tmp_path, {"S.cs": (
|
||||
"public enum Odd {\n"
|
||||
" Value,\n"
|
||||
" value\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert len(case_of) == 1
|
||||
odd = _find(r, "Odd")
|
||||
assert case_of == {(odd, _find(r, "Value")), (odd, _find(r, "value"))}
|
||||
assert _find(r, "Value") != _find(r, "value")
|
||||
|
||||
|
||||
def test_a_namespaced_enum_still_emits_members(tmp_path):
|
||||
|
||||
@@ -146,12 +146,15 @@ def test_methods_are_still_methods(tmp_path):
|
||||
|
||||
|
||||
def test_case_only_sibling_properties_do_not_duplicate_an_edge(tmp_path):
|
||||
# Legal C#, and one id after normalization. One node, one edge, rather than a
|
||||
# second edge hung on the first property's node.
|
||||
# Legal C#, and one id after normalization. Each property gets its own node
|
||||
# (#4415) and one edge, rather than a second edge hung on the first
|
||||
# property's node.
|
||||
defines, r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Odd {\n"
|
||||
" public int Count { get; set; }\n"
|
||||
" public int count { get; set; }\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert len(defines) == 1
|
||||
odd = _find(r, "Odd")
|
||||
assert defines == {(odd, _find(r, "Count")), (odd, _find(r, "count"))}
|
||||
assert _find(r, "Count") != _find(r, "count")
|
||||
@@ -0,0 +1,281 @@
|
||||
"""C# names that differ only by `_` or case keep their own nodes (#4415).
|
||||
|
||||
``ids.py:make_id`` strips leading/trailing underscores and casefolds, so in C#
|
||||
`Action()` / `_Action()`, `Size` / `_Size`, `Load()` / `load()` and the types
|
||||
`Pair` / `_Pair` each minted ONE id. ``add_node`` kept the first declaration
|
||||
and dropped the second: the dropped method's calls were credited to the
|
||||
survivor, and a dropped type's members hung off the other type. This is the
|
||||
Python collision of #3302; C# gets the same rule: a group with exactly one
|
||||
name without a leading underscore keeps the plain id on it, every other name
|
||||
is salted.
|
||||
|
||||
Separately, the C# member-call resolver keyed methods by a lowercased
|
||||
alphanumeric form of the name, so `this.Put()` bound to `_Put()` on a nearer
|
||||
base class instead of `Put()` further up.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.extract import extract
|
||||
|
||||
|
||||
def _extract(tmp_path, files: dict[str, str]):
|
||||
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)
|
||||
return extract([Path(n) for n in files], cache_root=Path(tempfile.mkdtemp()))
|
||||
finally:
|
||||
os.chdir(old)
|
||||
|
||||
|
||||
def _ids(r, label):
|
||||
return [n["id"] for n in r["nodes"] if n["label"] == label and n.get("source_file")]
|
||||
|
||||
|
||||
def _one(r, label):
|
||||
ids = _ids(r, label)
|
||||
assert len(ids) == 1, (label, ids)
|
||||
return ids[0]
|
||||
|
||||
|
||||
def _edges(r, relation):
|
||||
return {(e["source"], e["target"]) for e in r["edges"] if e["relation"] == relation}
|
||||
|
||||
|
||||
def test_underscore_method_pair_keeps_both_methods(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Widget {\n"
|
||||
" public bool Action() { return true; }\n"
|
||||
" protected bool _Action() { return Action(); }\n"
|
||||
"}\n"
|
||||
)})
|
||||
widget, action, private = _one(r, "Widget"), _one(r, ".Action()"), _one(r, "._Action()")
|
||||
assert action != private
|
||||
assert {(widget, action), (widget, private)} <= _edges(r, "method")
|
||||
# The private method's call is its own, not a self-loop on the public one.
|
||||
assert (private, action) in _edges(r, "calls")
|
||||
assert (action, action) not in _edges(r, "calls")
|
||||
|
||||
|
||||
def test_public_name_keeps_the_id_it_has_without_the_sibling(tmp_path):
|
||||
solo = _extract(tmp_path / "a", {"S.cs": (
|
||||
"public class Widget { public bool Action() { return true; } }\n"
|
||||
)})
|
||||
pair = _extract(tmp_path / "b", {"S.cs": (
|
||||
"public class Widget {\n"
|
||||
" protected bool _Action() { return true; }\n"
|
||||
" public bool Action() { return true; }\n"
|
||||
"}\n"
|
||||
)})
|
||||
# Adding or removing the private sibling re-points nothing.
|
||||
assert _one(pair, ".Action()") == _one(solo, ".Action()")
|
||||
|
||||
|
||||
def test_ids_do_not_depend_on_declaration_order(tmp_path):
|
||||
first = _extract(tmp_path / "a", {"S.cs": (
|
||||
"public class Widget {\n"
|
||||
" public bool Action() { return true; }\n"
|
||||
" protected bool _Action() { return true; }\n"
|
||||
"}\n"
|
||||
)})
|
||||
second = _extract(tmp_path / "b", {"S.cs": (
|
||||
"public class Widget {\n"
|
||||
" protected bool _Action() { return true; }\n"
|
||||
" public bool Action() { return true; }\n"
|
||||
"}\n"
|
||||
)})
|
||||
for label in (".Action()", "._Action()"):
|
||||
assert _one(first, label) == _one(second, label)
|
||||
|
||||
|
||||
def test_underscore_property_pair_keeps_both_properties(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public int Size { get; set; }\n"
|
||||
" private int _Size { get; set; }\n"
|
||||
"}\n"
|
||||
)})
|
||||
host = _one(r, "Host")
|
||||
assert {(host, _one(r, "Size")), (host, _one(r, "_Size"))} <= _edges(r, "defines")
|
||||
|
||||
|
||||
def test_case_only_method_pair_keeps_both_methods(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public void Load() { }\n"
|
||||
" public void load() { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _one(r, ".Load()") != _one(r, ".load()")
|
||||
|
||||
|
||||
def test_overloads_stay_one_node(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public void Save() { }\n"
|
||||
" public void Save(int x) { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
_one(r, ".Save()")
|
||||
|
||||
|
||||
def test_member_behind_preprocessor_directive_is_counted(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public void Run() { }\n"
|
||||
"#if DEBUG\n"
|
||||
" private void _Run() { }\n"
|
||||
"#endif\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _one(r, ".Run()") != _one(r, "._Run()")
|
||||
|
||||
|
||||
def test_same_file_partial_halves_share_the_group(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public partial class Host { public void Foo() { } }\n"
|
||||
"public partial class Host { private void _Foo() { } }\n"
|
||||
)})
|
||||
assert _one(r, ".Foo()") != _one(r, "._Foo()")
|
||||
|
||||
|
||||
def test_underscore_type_pair_keeps_both_types_and_their_members(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"namespace Demo {\n"
|
||||
" public class Pair { public void Go() { } }\n"
|
||||
" public class _Pair { public void Stop() { } }\n"
|
||||
"}\n"
|
||||
)})
|
||||
pair, private = _one(r, "Pair"), _one(r, "_Pair")
|
||||
assert pair != private
|
||||
method = _edges(r, "method")
|
||||
assert (pair, _one(r, ".Go()")) in method
|
||||
assert (private, _one(r, ".Stop()")) in method
|
||||
|
||||
|
||||
def test_type_behind_file_level_preprocessor_directive_is_counted(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"namespace Demo {\n"
|
||||
"#if NET8\n"
|
||||
" public class _Pair { public void Stop() { } }\n"
|
||||
"#endif\n"
|
||||
" public class Pair { public void Go() { } }\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _one(r, "Pair") != _one(r, "_Pair")
|
||||
|
||||
|
||||
def test_underscore_generic_type_pair_keeps_both_types(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Effect<T> { public void Fire() { } }\n"
|
||||
"public class _Effect<T> { public void Fire() { } }\n"
|
||||
)})
|
||||
assert _one(r, "Effect") != _one(r, "_Effect")
|
||||
assert len(_ids(r, ".Fire()")) == 2
|
||||
|
||||
|
||||
def test_underscore_nested_type_pair_keeps_both_types(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Host {\n"
|
||||
" public class Item { }\n"
|
||||
" private class _Item { }\n"
|
||||
"}\n"
|
||||
)})
|
||||
assert _one(r, "Item") != _one(r, "_Item")
|
||||
|
||||
|
||||
def test_bases_in_another_file_reach_the_right_type(tmp_path):
|
||||
r = _extract(tmp_path, {
|
||||
"A.cs": (
|
||||
"namespace Demo {\n"
|
||||
" public class _Pair { public void Stop() { } }\n"
|
||||
" public class Pair { public void Go() { } }\n"
|
||||
"}\n"
|
||||
),
|
||||
"B.cs": (
|
||||
"namespace Demo {\n"
|
||||
" public class Z : _Pair { public void M() { this.Stop(); } }\n"
|
||||
" public class Y : Pair { public void N() { this.Go(); } }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
inherits = _edges(r, "inherits")
|
||||
assert (_one(r, "Z"), _one(r, "_Pair")) in inherits
|
||||
assert (_one(r, "Y"), _one(r, "Pair")) in inherits
|
||||
calls = _edges(r, "calls")
|
||||
assert (_one(r, ".M()"), _one(r, ".Stop()")) in calls
|
||||
assert (_one(r, ".N()"), _one(r, ".Go()")) in calls
|
||||
|
||||
|
||||
def test_base_in_the_same_file_reaches_the_right_type(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class _Pair { }\n"
|
||||
"public class Pair { }\n"
|
||||
"public class Z : _Pair { }\n"
|
||||
"public class Y : Pair { }\n"
|
||||
)})
|
||||
inherits = _edges(r, "inherits")
|
||||
assert (_one(r, "Z"), _one(r, "_Pair")) in inherits
|
||||
assert (_one(r, "Y"), _one(r, "Pair")) in inherits
|
||||
|
||||
|
||||
def test_member_call_does_not_bind_to_an_underscore_sibling_on_a_nearer_base(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class BaseStore {\n"
|
||||
" public virtual object Get(string n) { return null; }\n"
|
||||
" public virtual bool Put(string n, object v) { return true; }\n"
|
||||
"}\n"
|
||||
"public class CachedStore : BaseStore {\n"
|
||||
" protected object _Get(string n) { return null; }\n"
|
||||
" protected bool _Put(string n, object v) { return true; }\n"
|
||||
"}\n"
|
||||
"public class Widget : CachedStore {\n"
|
||||
" public void Consume(Widget other) {\n"
|
||||
" this.Put(\"A\", 1);\n"
|
||||
" other.Get(\"B\");\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
)})
|
||||
calls = _edges(r, "calls")
|
||||
consume = _one(r, ".Consume()")
|
||||
assert (consume, _one(r, ".Put()")) in calls
|
||||
assert (consume, _one(r, ".Get()")) in calls
|
||||
assert (consume, _one(r, "._Put()")) not in calls
|
||||
assert (consume, _one(r, "._Get()")) not in calls
|
||||
|
||||
|
||||
def test_member_call_across_partial_halves_binds_by_exact_name(tmp_path):
|
||||
r = _extract(tmp_path, {
|
||||
"H1.cs": "public partial class Host { public void Foo() { } }\n",
|
||||
"H2.cs": (
|
||||
"public partial class Host {\n"
|
||||
" private void _Foo() { this.Foo(); }\n"
|
||||
" public void Bar() { this._Foo(); }\n"
|
||||
"}\n"
|
||||
),
|
||||
})
|
||||
calls = _edges(r, "calls")
|
||||
assert (_one(r, "._Foo()"), _one(r, ".Foo()")) in calls
|
||||
assert (_one(r, ".Bar()"), _one(r, "._Foo()")) in calls
|
||||
|
||||
|
||||
def test_base_call_reaches_the_underscore_method(tmp_path):
|
||||
r = _extract(tmp_path, {"S.cs": (
|
||||
"public class Widget {\n"
|
||||
" public virtual bool Action() { return true; }\n"
|
||||
" protected virtual bool _Action() { return true; }\n"
|
||||
"}\n"
|
||||
"public class SubWidget : Widget {\n"
|
||||
" protected override bool _Action() { return base._Action(); }\n"
|
||||
"}\n"
|
||||
)})
|
||||
sub, base = sorted(_ids(r, "._Action()"), key=lambda nid: "subwidget" not in nid)
|
||||
assert (sub, base) in _edges(r, "calls")
|
||||
assert (sub, _one(r, ".Action()")) not in _edges(r, "calls")
|
||||
Reference in new issue
Block a user