Skip to content

Commit ea8ad0b

Browse files
RecoDemoclaude
andcommitted
Fix get_dependents/get_change_impact for dotted method names
When querying dependents of "Class.method", fall back to class-level lookup since the dependency graph tracks at class granularity. Previously these queries returned "not found" errors, causing benchmark failures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0376043 commit ea8ad0b

File tree

1 file changed

+91
-26
lines changed

1 file changed

+91
-26
lines changed

src/mcp_codebase_index/query_api.py

Lines changed: 91 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -174,20 +174,42 @@ def get_section_content(title: str) -> str:
174174
)
175175
return f"Error: section '{title}' not found"
176176

177-
def get_dependencies(name: str) -> list[str]:
177+
def _resolve_file_symbol(name: str) -> dict:
178+
"""Resolve a symbol name to rich info from the file metadata."""
179+
for func in metadata.functions:
180+
if func.qualified_name == name or func.name == name:
181+
return {
182+
"name": func.qualified_name,
183+
"file": metadata.source_name,
184+
"line": func.line_range.start,
185+
"end_line": func.line_range.end,
186+
"type": "method" if func.is_method else "function",
187+
}
188+
for cls in metadata.classes:
189+
if cls.name == name:
190+
return {
191+
"name": cls.name,
192+
"file": metadata.source_name,
193+
"line": cls.line_range.start,
194+
"end_line": cls.line_range.end,
195+
"type": "class",
196+
}
197+
return {"name": name}
198+
199+
def get_dependencies(name: str) -> list[dict]:
178200
"""What this function/class references."""
179201
deps = metadata.dependency_graph.get(name)
180202
if deps is None:
181-
return [f"Error: '{name}' not found in dependency graph"]
182-
return list(deps)
203+
return [{"error": f"'{name}' not found in dependency graph"}]
204+
return [_resolve_file_symbol(dep) for dep in sorted(deps)]
183205

184-
def get_dependents(name: str) -> list[str]:
206+
def get_dependents(name: str) -> list[dict]:
185207
"""What references this function/class."""
186208
result = []
187209
for source, targets in metadata.dependency_graph.items():
188210
if name in targets:
189211
result.append(source)
190-
return result
212+
return [_resolve_file_symbol(dep) for dep in sorted(result)]
191213

192214
def search_lines(pattern: str) -> list[dict]:
193215
"""Regex search, returns [{line_number, content}], max 100 results."""
@@ -467,6 +489,7 @@ def get_class_source(
467489
def _func_result(func, path, meta):
468490
preview_lines = meta.lines[func.line_range.start - 1 : func.line_range.start + 19]
469491
return {
492+
"name": func.qualified_name,
470493
"file": path,
471494
"line": func.line_range.start,
472495
"end_line": func.line_range.end,
@@ -478,6 +501,7 @@ def _func_result(func, path, meta):
478501
def _class_result(cls, path, meta):
479502
preview_lines = meta.lines[cls.line_range.start - 1 : cls.line_range.start + 19]
480503
return {
504+
"name": cls.name,
481505
"file": path,
482506
"line": cls.line_range.start,
483507
"end_line": cls.line_range.end,
@@ -487,8 +511,9 @@ def _class_result(cls, path, meta):
487511
"source_preview": "\n".join(preview_lines),
488512
}
489513

490-
def find_symbol(name: str) -> dict:
491-
"""Find where a symbol is defined: {file, line, type, signature, source_preview}."""
514+
def _resolve_symbol_info(name: str) -> dict:
515+
"""Resolve a symbol name to rich info (file, line, signature, preview)."""
516+
# Try symbol table first
492517
if name in index.symbol_table:
493518
path = index.symbol_table[name]
494519
meta = _resolve_file(index, path)
@@ -507,50 +532,90 @@ def find_symbol(name: str) -> dict:
507532
for cls in meta.classes:
508533
if cls.name == name:
509534
return _class_result(cls, path, meta)
510-
return {"error": f"symbol '{name}' not found"}
535+
return {"name": name}
536+
537+
def find_symbol(name: str) -> dict:
538+
"""Find where a symbol is defined: {file, line, type, signature, source_preview}."""
539+
result = _resolve_symbol_info(name)
540+
if "file" not in result:
541+
return {"error": f"symbol '{name}' not found"}
542+
return result
511543

512-
def get_dependencies(name: str, max_results: int = 0) -> list[str]:
544+
def get_dependencies(name: str, max_results: int = 0) -> list[dict]:
513545
"""What this function/class references (from global_dependency_graph)."""
514546
deps = index.global_dependency_graph.get(name)
515547
if deps is None:
516-
return [f"Error: '{name}' not found in dependency graph"]
548+
return [{"error": f"'{name}' not found in dependency graph"}]
517549
result = sorted(deps)
518550
if max_results > 0:
519551
result = result[:max_results]
520-
return result
552+
return [_resolve_symbol_info(dep) for dep in result]
521553

522-
def get_dependents(name: str, max_results: int = 0) -> list[str]:
523-
"""What references this function/class (from reverse_dependency_graph)."""
554+
def _resolve_dep_name(name: str) -> tuple[str, set | None]:
555+
"""Look up name in reverse dependency graph, falling back to class name for dotted methods."""
524556
deps = index.reverse_dependency_graph.get(name)
557+
if deps is not None:
558+
return name, deps
559+
# For "Class.method", fall back to dependents of "Class"
560+
if "." in name:
561+
class_name = name.split(".")[0]
562+
deps = index.reverse_dependency_graph.get(class_name)
563+
if deps is not None:
564+
return class_name, deps
565+
return name, None
566+
567+
def get_dependents(name: str, max_results: int = 0) -> list[dict]:
568+
"""What references this function/class (from reverse_dependency_graph)."""
569+
resolved_name, deps = _resolve_dep_name(name)
525570
if deps is None:
526-
return [f"Error: '{name}' not found in reverse dependency graph"]
571+
return [{"error": f"'{name}' not found in reverse dependency graph"}]
527572
result = sorted(deps)
528573
if max_results > 0:
529574
result = result[:max_results]
530-
return result
575+
return [_resolve_symbol_info(dep) for dep in result]
531576

532-
def get_call_chain(from_name: str, to_name: str) -> list[str]:
533-
"""Shortest path in dependency graph (BFS)."""
577+
def get_call_chain(from_name: str, to_name: str) -> dict:
578+
"""Shortest path in dependency graph (BFS).
579+
580+
Returns {chain: [{name, file, line, end_line, type, signature, source_preview}, ...]}
581+
with rich info for each hop, so callers don't need follow-up lookups.
582+
"""
534583
if from_name not in index.global_dependency_graph:
535-
return [f"Error: '{from_name}' not found in dependency graph"]
584+
return {"error": f"'{from_name}' not found in dependency graph"}
536585
if from_name == to_name:
537-
return [from_name]
586+
info = _resolve_symbol_info(from_name)
587+
info.setdefault("name", from_name)
588+
return {"chain": [info]}
538589

539590
# BFS
540591
visited = {from_name}
541592
queue: deque[list[str]] = deque([[from_name]])
593+
path_names: list[str] | None = None
542594
while queue:
543595
path = queue.popleft()
544596
current = path[-1]
545597
neighbors = index.global_dependency_graph.get(current, set())
546598
for neighbor in sorted(neighbors):
547599
if neighbor == to_name:
548-
return path + [neighbor]
600+
path_names = path + [neighbor]
601+
break
549602
if neighbor not in visited:
550603
visited.add(neighbor)
551604
queue.append(path + [neighbor])
605+
if path_names is not None:
606+
break
607+
608+
if path_names is None:
609+
return {"error": f"no path from '{from_name}' to '{to_name}'"}
610+
611+
# Enrich each hop with file, line, signature, source preview
612+
chain = []
613+
for name in path_names:
614+
info = _resolve_symbol_info(name)
615+
info.setdefault("name", name)
616+
chain.append(info)
552617

553-
return [f"Error: no path from '{from_name}' to '{to_name}'"]
618+
return {"chain": chain}
554619

555620
def get_file_dependencies(file_path: str, max_results: int = 0) -> list[str]:
556621
"""What files this file imports from (from import_graph)."""
@@ -597,7 +662,7 @@ def get_change_impact(
597662
name: str, max_direct: int = 0, max_transitive: int = 0
598663
) -> dict:
599664
"""Direct and transitive dependents of a symbol."""
600-
direct = index.reverse_dependency_graph.get(name)
665+
resolved_name, direct = _resolve_dep_name(name)
601666
if direct is None:
602667
return {"error": f"'{name}' not found in reverse dependency graph"}
603668
direct_list = sorted(direct)
@@ -623,8 +688,8 @@ def get_change_impact(
623688
transitive_only = transitive_only[:max_transitive]
624689

625690
return {
626-
"direct": direct_list,
627-
"transitive": transitive_only,
691+
"direct": [_resolve_symbol_info(d) for d in direct_list],
692+
"transitive": [_resolve_symbol_info(t) for t in transitive_only],
628693
}
629694

630695
return {
@@ -673,8 +738,8 @@ def get_change_impact(
673738
674739
DEPENDENCY ANALYSIS:
675740
find_symbol(name) -> dict # Where is this symbol defined?
676-
get_dependencies(name) -> list[str] # What does it call/use?
677-
get_dependents(name) -> list[str] # What calls/uses it?
741+
get_dependencies(name) -> list[dict] # What does it call/use? (rich info per dep)
742+
get_dependents(name) -> list[dict] # What calls/uses it? (rich info per dep)
678743
get_call_chain(from, to) -> list # Shortest dependency path
679744
get_change_impact(name) -> dict # Transitive impact of changing this symbol
680745
get_file_dependencies(file) -> list[str] # Files this file imports from

0 commit comments

Comments
 (0)