mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
272 lines
8.3 KiB
Python
272 lines
8.3 KiB
Python
"""file_context API implementation.
|
|
|
|
This module provides the file_context() function for retrieving
|
|
method call graphs from a source file.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import List, Optional, Tuple
|
|
|
|
from ..entities import Symbol
|
|
from ..storage.global_index import GlobalSymbolIndex
|
|
from ..storage.dir_index import DirIndexStore
|
|
from ..storage.registry import RegistryStore
|
|
from ..errors import IndexNotFoundError
|
|
from .models import (
|
|
FileContextResult,
|
|
MethodContext,
|
|
CallInfo,
|
|
)
|
|
from .utils import resolve_project, normalize_relationship_type
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def file_context(
|
|
project_root: str,
|
|
file_path: str,
|
|
include_calls: bool = True,
|
|
include_callers: bool = True,
|
|
max_depth: int = 1,
|
|
format: str = "brief"
|
|
) -> FileContextResult:
|
|
"""Get method call context for a code file.
|
|
|
|
Retrieves all methods/functions in the file along with their
|
|
outgoing calls and incoming callers.
|
|
|
|
Args:
|
|
project_root: Project root directory (for index location)
|
|
file_path: Path to the code file to analyze
|
|
include_calls: Whether to include outgoing calls
|
|
include_callers: Whether to include incoming callers
|
|
max_depth: Call chain depth (V1 only supports 1)
|
|
format: Output format (brief | detailed | tree)
|
|
|
|
Returns:
|
|
FileContextResult with method contexts and summary
|
|
|
|
Raises:
|
|
IndexNotFoundError: If project is not indexed
|
|
FileNotFoundError: If file does not exist
|
|
ValueError: If max_depth > 1 (V1 limitation)
|
|
"""
|
|
# V1 limitation: only depth=1 supported
|
|
if max_depth > 1:
|
|
raise ValueError(
|
|
f"max_depth > 1 not supported in V1. "
|
|
f"Requested: {max_depth}, supported: 1"
|
|
)
|
|
|
|
project_path = resolve_project(project_root)
|
|
file_path_resolved = Path(file_path).resolve()
|
|
|
|
# Validate file exists
|
|
if not file_path_resolved.exists():
|
|
raise FileNotFoundError(f"File not found: {file_path_resolved}")
|
|
|
|
# Get project info from registry
|
|
registry = RegistryStore()
|
|
project_info = registry.get_project(project_path)
|
|
if project_info is None:
|
|
raise IndexNotFoundError(f"Project not indexed: {project_path}")
|
|
|
|
# Open global symbol index
|
|
index_db = project_info.index_root / "_global_symbols.db"
|
|
if not index_db.exists():
|
|
raise IndexNotFoundError(f"Global symbol index not found: {index_db}")
|
|
|
|
global_index = GlobalSymbolIndex(str(index_db), project_info.id)
|
|
|
|
# Get all symbols in the file
|
|
symbols = global_index.get_file_symbols(str(file_path_resolved))
|
|
|
|
# Filter to functions, methods, and classes
|
|
method_symbols = [
|
|
s for s in symbols
|
|
if s.kind in ("function", "method", "class")
|
|
]
|
|
|
|
logger.debug(f"Found {len(method_symbols)} methods in {file_path}")
|
|
|
|
# Try to find dir_index for relationship queries
|
|
dir_index = _find_dir_index(project_info, file_path_resolved)
|
|
|
|
# Build method contexts
|
|
methods: List[MethodContext] = []
|
|
outgoing_resolved = True
|
|
incoming_resolved = True
|
|
targets_resolved = True
|
|
|
|
for symbol in method_symbols:
|
|
calls: List[CallInfo] = []
|
|
callers: List[CallInfo] = []
|
|
|
|
if include_calls and dir_index:
|
|
try:
|
|
outgoing = dir_index.get_outgoing_calls(
|
|
str(file_path_resolved),
|
|
symbol.name
|
|
)
|
|
for target_name, rel_type, line, target_file in outgoing:
|
|
calls.append(CallInfo(
|
|
symbol_name=target_name,
|
|
file_path=target_file,
|
|
line=line,
|
|
relationship=normalize_relationship_type(rel_type)
|
|
))
|
|
if target_file is None:
|
|
targets_resolved = False
|
|
except Exception as e:
|
|
logger.debug(f"Failed to get outgoing calls: {e}")
|
|
outgoing_resolved = False
|
|
|
|
if include_callers and dir_index:
|
|
try:
|
|
incoming = dir_index.get_incoming_calls(symbol.name)
|
|
for source_name, rel_type, line, source_file in incoming:
|
|
callers.append(CallInfo(
|
|
symbol_name=source_name,
|
|
file_path=source_file,
|
|
line=line,
|
|
relationship=normalize_relationship_type(rel_type)
|
|
))
|
|
except Exception as e:
|
|
logger.debug(f"Failed to get incoming calls: {e}")
|
|
incoming_resolved = False
|
|
|
|
methods.append(MethodContext(
|
|
name=symbol.name,
|
|
kind=symbol.kind,
|
|
line_range=symbol.range if symbol.range else (1, 1),
|
|
signature=None, # Could extract from source
|
|
calls=calls,
|
|
callers=callers
|
|
))
|
|
|
|
# Detect language from file extension
|
|
language = _detect_language(file_path_resolved)
|
|
|
|
# Generate summary
|
|
summary = _generate_summary(file_path_resolved, methods, format)
|
|
|
|
return FileContextResult(
|
|
file_path=str(file_path_resolved),
|
|
language=language,
|
|
methods=methods,
|
|
summary=summary,
|
|
discovery_status={
|
|
"outgoing_resolved": outgoing_resolved,
|
|
"incoming_resolved": incoming_resolved,
|
|
"targets_resolved": targets_resolved
|
|
}
|
|
)
|
|
|
|
|
|
def _find_dir_index(project_info, file_path: Path) -> Optional[DirIndexStore]:
|
|
"""Find the dir_index that contains the file.
|
|
|
|
Args:
|
|
project_info: Project information from registry
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
DirIndexStore if found, None otherwise
|
|
"""
|
|
try:
|
|
# Look for _index.db in file's directory or parent directories
|
|
current = file_path.parent
|
|
while current != current.parent:
|
|
index_db = current / "_index.db"
|
|
if index_db.exists():
|
|
return DirIndexStore(str(index_db))
|
|
|
|
# Also check in project's index_root
|
|
relative = current.relative_to(project_info.source_root)
|
|
index_in_cache = project_info.index_root / relative / "_index.db"
|
|
if index_in_cache.exists():
|
|
return DirIndexStore(str(index_in_cache))
|
|
|
|
current = current.parent
|
|
except Exception as e:
|
|
logger.debug(f"Failed to find dir_index: {e}")
|
|
|
|
return None
|
|
|
|
|
|
def _detect_language(file_path: Path) -> str:
|
|
"""Detect programming language from file extension.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
Language name
|
|
"""
|
|
ext_map = {
|
|
".py": "python",
|
|
".js": "javascript",
|
|
".ts": "typescript",
|
|
".jsx": "javascript",
|
|
".tsx": "typescript",
|
|
".go": "go",
|
|
".rs": "rust",
|
|
".java": "java",
|
|
".c": "c",
|
|
".cpp": "cpp",
|
|
".h": "c",
|
|
".hpp": "cpp",
|
|
}
|
|
return ext_map.get(file_path.suffix.lower(), "unknown")
|
|
|
|
|
|
def _generate_summary(
|
|
file_path: Path,
|
|
methods: List[MethodContext],
|
|
format: str
|
|
) -> str:
|
|
"""Generate human-readable summary of file context.
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
methods: List of method contexts
|
|
format: Output format (brief | detailed | tree)
|
|
|
|
Returns:
|
|
Markdown-formatted summary
|
|
"""
|
|
lines = [f"## {file_path.name} ({len(methods)} methods)\n"]
|
|
|
|
for method in methods:
|
|
start, end = method.line_range
|
|
lines.append(f"### {method.name} (line {start}-{end})")
|
|
|
|
if method.calls:
|
|
calls_str = ", ".join(
|
|
f"{c.symbol_name} ({c.file_path or 'unresolved'}:{c.line})"
|
|
if format == "detailed"
|
|
else c.symbol_name
|
|
for c in method.calls
|
|
)
|
|
lines.append(f"- Calls: {calls_str}")
|
|
|
|
if method.callers:
|
|
callers_str = ", ".join(
|
|
f"{c.symbol_name} ({c.file_path}:{c.line})"
|
|
if format == "detailed"
|
|
else c.symbol_name
|
|
for c in method.callers
|
|
)
|
|
lines.append(f"- Called by: {callers_str}")
|
|
|
|
if not method.calls and not method.callers:
|
|
lines.append("- (no call relationships)")
|
|
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|