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:
rohit-jsfreaky authored and safishamsi committed 2026-10-06 20:54:15 +01:00
1 parent 251722cf17
commit 32034eccf5
2 files changed
+68 -2

No files matched your search

+14 -2
View File
@@ -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
+54
View File
@@ -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"
)