From 05e1e4c01d6dfd02bb3917f19de51f08c6082f05 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Sun, 16 Aug 2026 18:35:46 +0100 Subject: [PATCH] test(go): cover method-level and all-exported case-only collisions (#2779) Adds the two cases the deep-dive flagged as untested: a receiver-based method collision (Get/get on the same type) and the all-salted branch where no member is the unique exported one (Run/RUN). Also adds the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + tests/test_go_qualified_resolution.py | 38 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a48c8437..1c47d93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.45 (unreleased) - Fix: `graphify install ` now advances the `.graphify_version` stamp only for the platform it actually (re)writes, instead of stamping every installed platform as current; a platform whose skill content was left untouched keeps its old stamp so its staleness warning stays truthful (#2694, thanks @ousamabenyounes). This completes #2694 (the CLAUDE_CONFIG_DIR half shipped in 0.9.44). +- Fix: a Go file that declares both an exported and an unexported symbol differing only by case (e.g. `Run` and `run`, which are distinct in Go's case-sensitive visibility rules) no longer collapses them onto one node id and drops one; the exported symbol keeps its stable id and the unexported one is disambiguated, so an intra-file call to the unexported symbol resolves locally instead of phantoming to another package (#2779, thanks @catpotd). Only the Go extractor's id assignment is affected; the shared id normalization is unchanged, so no other language's ids move. - Fix: loading a `graph.json` that contains a hyperedge with no `id` field (the semantic extractor emits them and they persist verbatim) no longer crashes the incremental re-extract with `KeyError: 'id'`; id-less hyperedges are tolerated and retained (#2775, thanks @ousamabenyounes). ## 0.9.44 (2026-08-15) diff --git a/tests/test_go_qualified_resolution.py b/tests/test_go_qualified_resolution.py index dd5bd0ac..a8a5b682 100644 --- a/tests/test_go_qualified_resolution.py +++ b/tests/test_go_qualified_resolution.py @@ -318,3 +318,41 @@ def test_incremental_sibling_addition_keeps_cross_package_edge(tmp_path: Path) - assert start_targets == {"Run()"}, ( f"cross-package edge re-pointed after partial rebuild: {start_targets}" ) + + +def test_case_only_sibling_methods_are_both_extracted(tmp_path: Path) -> None: + """Methods ``Get`` and ``get`` on the same type in one file are two symbols, + not one — the receiver-based id path must salt the collision too (#2779).""" + (tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n") + pkg = tmp_path / "pkga" + pkg.mkdir() + (pkg / "a.go").write_text( + "package pkga\n\n" + "type Store struct{}\n\n" + "func (s Store) Get() int { return s.get() }\n\n" + "func (s Store) get() int { return 0 }\n" + ) + result = _extract(tmp_path) + exported = _ids(result, label="Get", suffix="a.go") + unexported = _ids(result, label="get", suffix="a.go") + assert len(exported) == 1, f"exported Get missing: {exported}" + assert len(unexported) == 1, f"unexported get missing: {unexported}" + assert exported != unexported, "Get and get collapsed onto one node id" + + +def test_case_only_sibling_all_exported_are_both_extracted(tmp_path: Path) -> None: + """When no member is the unique exported one (``Run`` and ``RUN`` both start + uppercase), each is salted so neither is dropped (#2779, all-salted branch).""" + (tmp_path / "go.mod").write_text("module example.com/repro\n\ngo 1.22\n") + pkg = tmp_path / "pkga" + pkg.mkdir() + (pkg / "a.go").write_text( + "package pkga\n\n" + "func Run() int { return 1 }\n\n" + "func RUN() int { return 2 }\n" + ) + result = _extract(tmp_path) + run1 = _ids(result, label="Run", suffix="a.go") + run2 = _ids(result, label="RUN", suffix="a.go") + assert len(run1) == 1 and len(run2) == 1, f"Run={run1} RUN={run2}" + assert run1 != run2, "Run and RUN collapsed onto one node id"