pub mod arch;
pub mod cache;
pub mod dead;
pub mod impact;
pub mod indexer;
pub mod resolver;
pub mod scanner;
pub mod types;
#[allow(unused_imports)]
pub use arch::{analyze_architecture, ArchAnalysis, ArchStats, CycleDependency};
#[allow(unused_imports)]
pub use dead::{
analyze_dead_code, analyze_dead_code_with_config, classify_entry_point,
detect_entry_points_with_config, DeadCodeConfig, DeadCodeResult, DeadCodeStats, DeadFunction,
DeadReason, EntryPointKind,
};
#[allow(unused_imports)]
pub use impact::{analyze_impact, CallerInfo, ImpactConfig, ImpactResult};
#[allow(unused_imports)]
pub use indexer::{FunctionDef, FunctionIndex, IndexStats};
#[allow(unused_imports)]
pub use scanner::{
ErrorHandling, FileMetadata, ProjectScanner, ScanConfig, ScanError, ScanErrorKind, ScanResult,
};
#[allow(unused_imports)]
pub use types::{CallEdge, CallGraph, FunctionRef};
#[allow(unused_imports)]
pub use cache::{
get_or_build_graph_with_config, warm_cache_with_config, invalidate_cache,
CachedCallGraph, CachedEdge,
get_cache_dir, get_cache_file,
};
use crate::error::Result;
pub fn build(path: &str) -> Result<CallGraph> {
build_with_config(path, None, false)
}
pub fn build_with_config(path: &str, lang: Option<&str>, no_ignore: bool) -> Result<CallGraph> {
let project_root = std::path::Path::new(path);
let scanner = scanner::ProjectScanner::new(path)?;
let mut config = match lang {
Some(l) if l != "all" => ScanConfig::for_language(l),
_ => ScanConfig::default(),
};
config.no_ignore = no_ignore;
let scan_result = scanner.scan_with_config(&config)?;
let files = scan_result.files;
let index = indexer::FunctionIndex::build(&files)?;
let graph = resolver::resolve_calls(&files, &index, project_root)?;
Ok(graph)
}
#[allow(dead_code)]
pub fn build_with_index(path: &str) -> Result<(CallGraph, FunctionIndex)> {
let project_root = std::path::Path::new(path);
let scanner = scanner::ProjectScanner::new(path)?;
let files = scanner.scan_files()?;
let index = indexer::FunctionIndex::build(&files)?;
let graph = resolver::resolve_calls(&files, &index, project_root)?;
Ok((graph, index))
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct FunctionContextInfo {
pub name: String,
pub file: String,
pub line: usize,
pub signature: String,
pub docstring: Option<String>,
pub calls: Vec<String>,
}
pub fn get_context_with_lang(
project: &str,
entry_point: &str,
depth: usize,
lang: Option<&str>,
) -> Result<serde_json::Value> {
use crate::ast::extractor::AstExtractor;
use std::collections::{HashMap, HashSet};
use std::path::Path;
let project_root = std::path::Path::new(project);
let scanner = scanner::ProjectScanner::new(project)?;
let files = match lang {
Some(lang_str) => {
let config = scanner::ScanConfig::for_language(lang_str);
scanner.scan_with_config(&config)?.files
}
None => scanner.scan_files()?,
};
let index = indexer::FunctionIndex::build(&files)?;
let mut graph = resolver::resolve_calls(&files, &index, project_root)?;
graph.build_indexes();
let entry_target = FunctionRef {
file: String::new(),
name: entry_point.to_string(),
qualified_name: None,
};
let callees = graph.get_callees(&entry_target, depth);
let mut all_funcs: Vec<FunctionRef> = Vec::new();
let entry_found = graph
.all_functions()
.iter()
.find(|f| f.name == entry_point || f.qualified_name.as_deref() == Some(entry_point));
if let Some(entry_func) = entry_found {
all_funcs.push(entry_func.clone());
}
let mut seen: HashSet<String> = HashSet::new();
for func in &all_funcs {
seen.insert(func.name.clone());
}
for callee in callees {
if seen.insert(callee.name.clone()) {
all_funcs.push(callee);
}
}
let mut module_cache: HashMap<String, crate::ast::types::ModuleInfo> = HashMap::new();
let mut function_contexts: Vec<FunctionContextInfo> = Vec::new();
for func_ref in &all_funcs {
if func_ref.file.is_empty() {
function_contexts.push(FunctionContextInfo {
name: func_ref.qualified_name.clone().unwrap_or_else(|| func_ref.name.clone()),
file: "?".to_string(),
line: 0,
signature: format!("def {}(...)", func_ref.name),
docstring: None,
calls: graph
.callees
.get(func_ref)
.map(|c| c.iter().map(|f| f.name.clone()).collect())
.unwrap_or_default(),
});
continue;
}
let module_info = if let Some(cached) = module_cache.get(&func_ref.file) {
cached.clone()
} else {
match AstExtractor::extract_file(Path::new(&func_ref.file)) {
Ok(info) => {
module_cache.insert(func_ref.file.clone(), info.clone());
info
}
Err(_) => {
function_contexts.push(FunctionContextInfo {
name: func_ref.qualified_name.clone().unwrap_or_else(|| func_ref.name.clone()),
file: func_ref.file.clone(),
line: 0,
signature: format!("def {}(...)", func_ref.name),
docstring: None,
calls: graph
.callees
.get(func_ref)
.map(|c| c.iter().map(|f| f.name.clone()).collect())
.unwrap_or_default(),
});
continue;
}
}
};
let func_info: Option<&crate::ast::types::FunctionInfo> = module_info
.functions
.iter()
.find(|f| f.name == func_ref.name)
.or_else(|| {
module_info.classes.iter().find_map(|c| {
c.methods.iter().find(|m| {
m.name == func_ref.name
|| func_ref.name == format!("{}.{}", c.name, m.name)
})
})
});
let (signature, docstring, line) = if let Some(info) = func_info {
(info.signature(), info.docstring.clone(), info.line_number)
} else {
let line = index
.get_definition(func_ref.qualified_name.as_deref().unwrap_or(&func_ref.name))
.map(|d| d.line_number)
.unwrap_or(0);
(format!("def {}(...)", func_ref.name), None, line)
};
let calls: Vec<String> = graph
.callees
.get(func_ref)
.map(|c| c.iter().map(|f| f.name.clone()).collect())
.unwrap_or_default();
function_contexts.push(FunctionContextInfo {
name: func_ref.qualified_name.clone().unwrap_or_else(|| func_ref.name.clone()),
file: func_ref.file.clone(),
line,
signature,
docstring,
calls,
});
}
let mut llm_context = String::new();
llm_context.push_str(&format!(
"## Code Context: {} (depth={})\n\n",
entry_point, depth
));
for (i, func) in function_contexts.iter().enumerate() {
let indent_level = i.min(depth);
let indent = " ".repeat(indent_level);
let short_file = Path::new(&func.file)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("?");
llm_context.push_str(&format!(
"{}* {} ({}:{})\n",
indent, func.name, short_file, func.line
));
llm_context.push_str(&format!("{} {}\n", indent, func.signature));
if let Some(ref doc) = func.docstring {
let first_line: &str = doc.lines().next().unwrap_or("");
let truncated = if first_line.len() > 80 {
&first_line[..77]
} else {
first_line
};
if !truncated.is_empty() {
llm_context.push_str(&format!("{} # {}\n", indent, truncated));
}
}
if !func.calls.is_empty() {
let calls_str = if func.calls.len() > 5 {
format!(
"{} (+{} more)",
func.calls[..5].join(", "),
func.calls.len() - 5
)
} else {
func.calls.join(", ")
};
llm_context.push_str(&format!("{} -> calls: {}\n", indent, calls_str));
}
llm_context.push('\n');
}
Ok(serde_json::json!({
"entry_point": entry_point,
"project": project,
"depth": depth,
"function_count": function_contexts.len(),
"functions": function_contexts.iter().map(|f| serde_json::json!({
"name": f.name,
"file": f.file,
"line": f.line,
"signature": f.signature,
"docstring": f.docstring,
"calls": f.calls,
})).collect::<Vec<_>>(),
"llm_context": llm_context,
}))
}