@@ -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
674739DEPENDENCY 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