mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
fix(csharp): make namespace scope ids independent of line endings
C# namespace scope ids (the scope_chain / scope_id node metadata a
namespace-scoped `using` is matched against) were `s{start_byte}`. A
CRLF checkout (Windows with core.autocrlf) has one more byte per line
than an LF checkout of the same commit, so the two wrote different
metadata into graph.json: on Newtonsoft.Json's 242 source files, 3,096
of 4,352 nodes (71%) differed by line endings alone.
Use row:column, which is the same for both and just as unique. The ids
are only compared within one file, so resolution is unchanged: all
11,627 edges are identical before and after, and the nodes only differ
in the id text (identical once each id is mapped to its scope).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit ffd855a6fa)
This commit is contained in:
1 parent
251722cf17
commit
32034eccf5
2 files changed
+68
-2
No files matched your search
@@ -3244,6 +3244,18 @@ def _ts_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path: str,
|
||||
return True
|
||||
return False
|
||||
|
||||
def _csharp_scope_id(node) -> str:
|
||||
"""Id of a namespace scope, stored in ``scope_chain`` / ``scope_id`` metadata.
|
||||
|
||||
Row and column, not ``start_byte``: a CRLF checkout has one more byte per
|
||||
line than an LF checkout of the same file, so a byte offset gave a
|
||||
Windows and a Linux build of the same commit different graph.json
|
||||
metadata. Row and column are the same for both and just as unique.
|
||||
"""
|
||||
row, column = node.start_point
|
||||
return f"s{row}:{column}"
|
||||
|
||||
|
||||
def _csharp_namespace_name(node, source: bytes) -> str:
|
||||
name_node = node.child_by_field_name("name")
|
||||
if name_node is not None:
|
||||
@@ -3286,7 +3298,7 @@ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path:
|
||||
pushed = False
|
||||
if ns_name:
|
||||
namespace_stack.append(ns_name)
|
||||
scope_stack.append(f"s{node.start_byte}")
|
||||
scope_stack.append(_csharp_scope_id(node))
|
||||
pushed = True
|
||||
ns_label = ".".join(namespace_stack)
|
||||
ns_nid = _csharp_namespace_id(ns_label)
|
||||
@@ -3310,7 +3322,7 @@ def _csharp_extra_walk(node, source: bytes, file_nid: str, stem: str, str_path:
|
||||
ns_name = _csharp_namespace_name(node, source)
|
||||
if ns_name:
|
||||
namespace_stack.append(ns_name)
|
||||
scope_stack.append(f"s{node.start_byte}")
|
||||
scope_stack.append(_csharp_scope_id(node))
|
||||
ns_label = ".".join(namespace_stack)
|
||||
ns_nid = _csharp_namespace_id(ns_label)
|
||||
line = node.start_point[0] + 1
|
||||
|
||||
@@ -765,3 +765,57 @@ def test_csharp_name_resolver_resolves_type_in_partial_graph_without_contains():
|
||||
)
|
||||
assert resolved == "lib::type", "Valid C# type must resolve even without `contains` edges"
|
||||
assert decisive is True
|
||||
|
||||
|
||||
def _crlf_vs_lf(tmp_path: Path, files: dict[str, str]) -> tuple[dict, dict]:
|
||||
results = []
|
||||
for name, newline in (("lf", "\n"), ("crlf", "\r\n")):
|
||||
root = tmp_path / name
|
||||
paths = []
|
||||
for rel, text in files.items():
|
||||
p = root / rel
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(text.replace("\n", newline).encode("utf-8"))
|
||||
paths.append(p)
|
||||
results.append(extract(paths, root=root, cache_root=root))
|
||||
return results[0], results[1]
|
||||
|
||||
|
||||
def test_csharp_scope_metadata_does_not_depend_on_line_endings(tmp_path: Path):
|
||||
"""Namespace scope ids were byte offsets, so a CRLF checkout (Windows,
|
||||
core.autocrlf) and an LF checkout of the same commit wrote different
|
||||
`scope_chain` / `scope_id` metadata into graph.json. Row:column is the
|
||||
same for both."""
|
||||
files = {
|
||||
"core.cs": "// Core types.\nusing System;\n\nnamespace Game.Core\n{\n public class Damage {}\n}\n",
|
||||
"combat.cs": (
|
||||
"// Combat.\n"
|
||||
"using System;\n"
|
||||
"\n"
|
||||
"namespace Game.Combat\n"
|
||||
"{\n"
|
||||
" using Dmg = Game.Core.Damage;\n"
|
||||
"\n"
|
||||
" public class Weapon : Dmg\n"
|
||||
" {\n"
|
||||
" public void Hit() {}\n"
|
||||
" }\n"
|
||||
"}\n"
|
||||
),
|
||||
}
|
||||
lf, crlf = _crlf_vs_lf(tmp_path, files)
|
||||
|
||||
def shape(result: dict) -> tuple[list, list]:
|
||||
nodes = sorted(
|
||||
(n["id"], repr(sorted((n.get("metadata") or {}).items())), n.get("source_location"))
|
||||
for n in result["nodes"]
|
||||
)
|
||||
edges = sorted((e["source"], e["target"], e["relation"]) for e in result["edges"])
|
||||
return nodes, edges
|
||||
|
||||
assert any("scope_chain" in (n.get("metadata") or {}) for n in lf["nodes"])
|
||||
assert shape(lf) == shape(crlf)
|
||||
damage = _targets(crlf, "inherits", "Damage")
|
||||
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"
|
||||
)
|
||||
Reference in new issue
Block a user