Skip to content

Commit 815bb67

Browse files
author
jgstern-agent
committed
fix(go): resolve import paths correctly for multi-file symbol disambiguation
INV-007: When multiple Go files define the same symbol (e.g., generated protobuf files copied across services), the analyzer now correctly uses import paths to disambiguate. Changes: - ListNameResolver.lookup() now tries progressively shorter path suffixes (e.g., "zzz_correct/genproto" → "genproto") to find unique matches - Falls back to deterministic (sorted by path) ordering when ambiguous - Fixes flaky test failures on Python 3.13 due to dict ordering changes The fix ensures call edges point to the correct target file based on the import statement, not arbitrary file traversal order. Signed-off-by: jgstern-agent <josh-agent@iterabloom.com>
1 parent d8f1b5e commit 815bb67

3 files changed

Lines changed: 46 additions & 8 deletions

File tree

.agent/invariant-ledger.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,21 @@ type, AND location).
176176

177177
---
178178

179-
## INV-007: Template for New Invariants
179+
## INV-007: Go Import Path Resolution
180+
- **Statement:** When multiple Go files define the same symbol (same package name, same function),
181+
call resolution must use the import path to pick the correct target file
182+
- **Status:** ✅ FIXED
183+
- **Root cause:** `symbol_resolution.py:ListNameResolver.lookup()` only checked the last directory
184+
segment of the import path (e.g., "genproto") which matched multiple candidates. When no unique
185+
match was found, it returned the first candidate, which was non-deterministic across Python versions.
186+
- **Fix:** Enhanced `ListNameResolver.lookup()` to:
187+
1. Try progressively shorter suffixes of the import path (e.g., "zzz_correct/genproto", "genproto")
188+
2. Return when exactly one candidate matches a suffix
189+
3. Sort candidates deterministically (by path) when falling back to ambiguous resolution
190+
- **Regression tests:**
191+
- `tests/test_go.py::TestGoImportPathResolution::test_resolves_call_to_correct_file_by_import_path`
192+
193+
## INV-XXX: Template for New Invariants
180194
- **Statement:** [What must always be true]
181195
- **Status:** [UNFIXED | PARTIALLY ADDRESSED | FIXED | TBD]
182196
- **Root cause:** [File:line or description]

src/hypergumbo/symbol_resolution.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -584,20 +584,41 @@ def lookup(
584584

585585
# Multiple candidates - try to disambiguate with path hint
586586
if path_hint:
587-
# Extract directory hint from path (e.g., "grpc" from "/path/to/grpc/server.go")
588-
dir_hint = path_hint.rstrip("/").rsplit("/", 1)[-1] if "/" in path_hint else path_hint
589-
for candidate in candidates:
590-
if dir_hint in candidate.path:
587+
# Try progressively shorter suffixes of the path hint to find unique match
588+
# e.g., for "github.com/example/src/zzz_correct/genproto", try:
589+
# 1. "src/zzz_correct/genproto" (longest useful suffix)
590+
# 2. "zzz_correct/genproto"
591+
# 3. "genproto" (shortest)
592+
path_parts = path_hint.rstrip("/").split("/")
593+
594+
# Start from second-to-last segment (skip domain parts like github.com)
595+
# and try progressively shorter suffixes
596+
for i in range(len(path_parts) - 1, 0, -1):
597+
suffix = "/".join(path_parts[i:])
598+
matching = [c for c in candidates if suffix in c.path]
599+
if len(matching) == 1:
591600
return LookupResult(
592-
symbol=candidate,
601+
symbol=matching[0],
593602
confidence=self.CONFIDENCE_PATH_HINT,
594603
match_type="path_hint",
595604
candidates=candidates,
596605
)
597606

598-
# Ambiguous - return first with low confidence
607+
# Fallback: try just the last segment
608+
dir_hint = path_parts[-1]
609+
matching = [c for c in candidates if dir_hint in c.path]
610+
if len(matching) == 1:
611+
return LookupResult(
612+
symbol=matching[0],
613+
confidence=self.CONFIDENCE_PATH_HINT,
614+
match_type="path_hint",
615+
candidates=candidates,
616+
)
617+
618+
# Ambiguous - sort for deterministic ordering, return first with low confidence
619+
sorted_candidates = sorted(candidates, key=lambda s: s.path)
599620
return LookupResult(
600-
symbol=candidates[0],
621+
symbol=sorted_candidates[0],
601622
confidence=self.CONFIDENCE_AMBIGUOUS,
602623
match_type="ambiguous",
603624
candidates=candidates,

tests/test_go.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,9 @@ def test_resolves_call_to_correct_file_by_import_path(self, tmp_path: Path) -> N
977977
978978
We use 'aaa_wrong' vs 'zzz_correct' naming to ensure alphabetical
979979
ordering would pick the WRONG file (aaa < zzz).
980+
981+
Fixed in INV-007: ListNameResolver now tries progressively shorter
982+
path suffixes to find unique matches.
980983
"""
981984
from hypergumbo.analyze.go import analyze_go
982985

0 commit comments

Comments
 (0)