mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-14 16:09:26 +00:00
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>
(cherry picked from commit 914dda60cf)
282 lines
9.6 KiB
Python
282 lines
9.6 KiB
Python
"""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")
|