use crate::models::error::TemplateError;
#[cfg(feature = "python-ast")]
use crate::services::ast_python;
#[cfg(feature = "typescript-ast")]
use crate::services::ast_typescript;
use crate::services::cache::{
manager::SessionCacheManager, persistent_manager::PersistentCacheManager,
};
use crate::services::deep_context::DeepContext;
use futures::future::join_all;
use ignore::gitignore::GitignoreBuilder;
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::sync::Arc;
use syn::visit::Visit;
use syn::{ItemEnum, ItemFn, ItemImpl, ItemMod, ItemStruct, ItemTrait, ItemUse};
use walkdir::WalkDir;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectContext {
pub project_type: String,
pub files: Vec<FileContext>,
pub summary: ProjectSummary,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ProjectSummary {
pub total_files: usize,
pub total_functions: usize,
pub total_structs: usize,
pub total_enums: usize,
pub total_traits: usize,
pub total_impls: usize,
pub dependencies: Vec<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct FileContext {
pub path: String,
pub language: String,
pub items: Vec<AstItem>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity_metrics: Option<crate::services::complexity::FileComplexityMetrics>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(tag = "type")]
pub enum AstItem {
Function {
name: String,
visibility: String,
is_async: bool,
line: usize,
},
Struct {
name: String,
visibility: String,
fields_count: usize,
derives: Vec<String>,
line: usize,
},
Enum {
name: String,
visibility: String,
variants_count: usize,
line: usize,
},
Trait {
name: String,
visibility: String,
line: usize,
},
Impl {
type_name: String,
trait_name: Option<String>,
line: usize,
},
Module {
name: String,
visibility: String,
line: usize,
},
Use {
path: String,
line: usize,
},
Import {
module: String,
items: Vec<String>,
alias: Option<String>,
line: usize,
},
}
impl AstItem {
#[must_use]
pub fn display_name(&self) -> &str {
match self {
AstItem::Function { name, .. } => name,
AstItem::Struct { name, .. } => name,
AstItem::Enum { name, .. } => name,
AstItem::Trait { name, .. } => name,
AstItem::Impl { type_name, .. } => type_name,
AstItem::Module { name, .. } => name,
AstItem::Use { path, .. } => path,
AstItem::Import { module, .. } => module,
}
}
}
struct RustVisitor {
items: Vec<AstItem>,
#[allow(dead_code)]
source: String,
}
impl RustVisitor {
fn new(source: String) -> Self {
Self {
items: Vec::new(),
source,
}
}
fn get_line<T: syn::spanned::Spanned>(&self, _span: T) -> usize {
1
}
fn get_visibility(&self, vis: &syn::Visibility) -> String {
match vis {
syn::Visibility::Public(_) => "pub".to_string(),
syn::Visibility::Restricted(r) => format!(
"pub({})",
r.path
.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
),
syn::Visibility::Inherited => "private".to_string(),
}
}
fn get_derives(_attrs: &[syn::Attribute]) -> Vec<String> {
Vec::new()
}
}
impl<'ast> Visit<'ast> for RustVisitor {
fn visit_item_fn(&mut self, node: &'ast ItemFn) {
self.items.push(AstItem::Function {
name: node.sig.ident.to_string(),
visibility: self.get_visibility(&node.vis),
is_async: node.sig.asyncness.is_some(),
line: self.get_line(node.sig.ident.span()),
});
}
fn visit_item_struct(&mut self, node: &'ast ItemStruct) {
let fields_count = match &node.fields {
syn::Fields::Named(fields) => fields.named.len(),
syn::Fields::Unnamed(fields) => fields.unnamed.len(),
syn::Fields::Unit => 0,
};
self.items.push(AstItem::Struct {
name: node.ident.to_string(),
visibility: self.get_visibility(&node.vis),
fields_count,
derives: Self::get_derives(&node.attrs),
line: self.get_line(node.ident.span()),
});
}
fn visit_item_enum(&mut self, node: &'ast ItemEnum) {
self.items.push(AstItem::Enum {
name: node.ident.to_string(),
visibility: self.get_visibility(&node.vis),
variants_count: node.variants.len(),
line: self.get_line(node.ident.span()),
});
}
fn visit_item_trait(&mut self, node: &'ast ItemTrait) {
self.items.push(AstItem::Trait {
name: node.ident.to_string(),
visibility: self.get_visibility(&node.vis),
line: self.get_line(node.ident.span()),
});
}
fn visit_item_impl(&mut self, node: &'ast ItemImpl) {
let type_name = if let syn::Type::Path(type_path) = &*node.self_ty {
type_path
.path
.segments
.last().map_or_else(|| "Unknown".to_string(), |s| s.ident.to_string())
} else {
"Unknown".to_string()
};
let trait_name = node.trait_.as_ref().map(|(_, path, _)| {
path.segments
.last().map_or_else(|| "Unknown".to_string(), |s| s.ident.to_string())
});
self.items.push(AstItem::Impl {
type_name,
trait_name,
line: 1, });
}
fn visit_item_mod(&mut self, node: &'ast ItemMod) {
self.items.push(AstItem::Module {
name: node.ident.to_string(),
visibility: self.get_visibility(&node.vis),
line: self.get_line(node.ident.span()),
});
}
fn visit_item_use(&mut self, node: &'ast ItemUse) {
let path = match &node.tree {
syn::UseTree::Path(p) => p.ident.to_string(),
syn::UseTree::Name(n) => n.ident.to_string(),
syn::UseTree::Rename(r) => r.ident.to_string(),
syn::UseTree::Glob(_) => "*".to_string(),
syn::UseTree::Group(_) => "...".to_string(),
};
self.items.push(AstItem::Use {
path,
line: 1, });
}
}
pub async fn analyze_rust_file(path: &Path) -> Result<FileContext, TemplateError> {
analyze_rust_file_with_cache(path, None).await
}
pub async fn analyze_rust_file_with_cache(
path: &Path,
cache_manager: Option<Arc<SessionCacheManager>>,
) -> Result<FileContext, TemplateError> {
if let Some(cache) = cache_manager {
cache
.get_or_compute_ast(path, || async {
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| anyhow::anyhow!("Failed to read file: {e}"))?;
let syntax = syn::parse_file(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse Rust file: {e}"))?;
let mut visitor = RustVisitor::new(content);
visitor.visit_file(&syntax);
Ok(FileContext {
path: path.display().to_string(),
language: "rust".to_string(),
items: visitor.items,
complexity_metrics: None,
})
})
.await
.map(|arc| (*arc).clone())
.map_err(|e| TemplateError::InvalidUtf8(e.to_string()))
} else {
let content = tokio::fs::read_to_string(path)
.await
.map_err(TemplateError::Io)?;
let syntax =
syn::parse_file(&content).map_err(|e| TemplateError::InvalidUtf8(e.to_string()))?;
let mut visitor = RustVisitor::new(content);
visitor.visit_file(&syntax);
Ok(FileContext {
path: path.display().to_string(),
language: "rust".to_string(),
items: visitor.items,
complexity_metrics: None,
})
}
}
pub async fn analyze_project(
root_path: &Path,
toolchain: &str,
) -> Result<ProjectContext, TemplateError> {
analyze_project_with_cache(root_path, toolchain, None).await
}
pub async fn analyze_rust_file_with_persistent_cache(
path: &Path,
cache_manager: Option<Arc<PersistentCacheManager>>,
) -> Result<FileContext, TemplateError> {
if let Some(cache) = cache_manager {
cache
.get_or_compute_ast(path, || async {
let content = tokio::fs::read_to_string(path)
.await
.map_err(|e| anyhow::anyhow!("Failed to read file: {e}"))?;
let syntax = syn::parse_file(&content)
.map_err(|e| anyhow::anyhow!("Failed to parse Rust file: {e}"))?;
let mut visitor = RustVisitor::new(content);
visitor.visit_file(&syntax);
Ok(FileContext {
path: path.display().to_string(),
language: "rust".to_string(),
items: visitor.items,
complexity_metrics: None,
})
})
.await
.map(|arc| (*arc).clone())
.map_err(|e| TemplateError::InvalidUtf8(e.to_string()))
} else {
let content = tokio::fs::read_to_string(path)
.await
.map_err(TemplateError::Io)?;
let syntax =
syn::parse_file(&content).map_err(|e| TemplateError::InvalidUtf8(e.to_string()))?;
let mut visitor = RustVisitor::new(content);
visitor.visit_file(&syntax);
Ok(FileContext {
path: path.display().to_string(),
language: "rust".to_string(),
items: visitor.items,
complexity_metrics: None,
})
}
}
pub async fn analyze_project_for_dead_code(
root_path: &Path,
toolchain: &str,
) -> Result<ProjectContext, TemplateError> {
let gitignore = build_gitignore(root_path)?;
let files = scan_rust_files_only(root_path, toolchain, None, &gitignore).await;
let summary = build_project_summary(&files, root_path, toolchain).await;
Ok(ProjectContext {
project_type: toolchain.to_string(),
files,
summary,
})
}
pub async fn analyze_project_with_cache(
root_path: &Path,
toolchain: &str,
cache_manager: Option<Arc<SessionCacheManager>>,
) -> Result<ProjectContext, TemplateError> {
let gitignore = build_gitignore(root_path)?;
let files = scan_and_analyze_files(root_path, toolchain, cache_manager, &gitignore).await;
let summary = build_project_summary(&files, root_path, toolchain).await;
Ok(ProjectContext {
project_type: toolchain.to_string(),
files,
summary,
})
}
fn build_gitignore(root_path: &Path) -> Result<ignore::gitignore::Gitignore, TemplateError> {
let mut gitignore = GitignoreBuilder::new(root_path);
let default_ignores = [".git", "target", "node_modules", ".venv", "__pycache__"];
for pattern in &default_ignores {
gitignore.add_line(None, pattern).ok();
}
if let Ok(gi_path) = root_path.join(".gitignore").canonicalize() {
gitignore.add(&gi_path);
}
gitignore
.build()
.map_err(|e| TemplateError::InvalidUtf8(e.to_string()))
}
async fn scan_rust_files_only(
root_path: &Path,
toolchain: &str,
cache_manager: Option<Arc<SessionCacheManager>>,
gitignore: &ignore::gitignore::Gitignore,
) -> Vec<FileContext> {
const MAX_DEPTH: usize = 5; const MAX_FILES: usize = 100; const BATCH_SIZE: usize = 20;
let paths: Vec<_> = WalkDir::new(root_path)
.follow_links(false)
.max_depth(MAX_DEPTH)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| {
let path = entry.path();
if path.is_dir() || gitignore.matched(path, false).is_ignore() {
return false;
}
if !path.extension().is_some_and(|ext| ext == "rs") {
return false;
}
let path_str = path.to_string_lossy();
!path_str.contains("/tests/")
&& !path_str.contains("/test/")
&& !path_str.contains("/examples/")
&& !path_str.contains("/benches/")
&& !path_str.contains("_test.rs")
&& !path_str.ends_with("/build.rs")
})
.take(MAX_FILES)
.map(|entry| entry.path().to_path_buf())
.collect();
eprintln!(
"🎯 Dead code analysis: scanning {} Rust source files (max {})",
paths.len(),
MAX_FILES
);
let mut all_results = Vec::new();
for chunk in paths.chunks(BATCH_SIZE) {
let batch_tasks: Vec<_> = chunk
.iter()
.map(|path| {
let path = path.clone();
let toolchain = toolchain.to_string();
let cache_manager = cache_manager.clone();
tokio::spawn(async move {
let timeout_duration = tokio::time::Duration::from_secs(2);
tokio::time::timeout(timeout_duration, async move {
analyze_file_by_toolchain(&path, &toolchain, cache_manager).await
})
.await
.ok()
.flatten()
})
})
.collect();
let batch_results = join_all(batch_tasks).await;
all_results.extend(
batch_results
.into_iter()
.filter_map(std::result::Result::ok)
.flatten(),
);
}
all_results
}
async fn scan_and_analyze_files(
root_path: &Path,
toolchain: &str,
cache_manager: Option<Arc<SessionCacheManager>>,
gitignore: &ignore::gitignore::Gitignore,
) -> Vec<FileContext> {
const MAX_DEPTH: usize = 10; const MAX_FILES: usize = 10000; const BATCH_SIZE: usize = 100;
let mut file_count = 0;
let paths: Vec<_> = WalkDir::new(root_path)
.follow_links(false)
.max_depth(MAX_DEPTH) .into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| {
let path = entry.path();
!path.is_dir() && !gitignore.matched(path, false).is_ignore()
})
.take(MAX_FILES) .map(|entry| {
file_count += 1;
if file_count % 1000 == 0 {
eprintln!("📁 Scanning files... ({file_count} so far)");
}
entry.path().to_path_buf()
})
.collect();
if file_count > MAX_FILES / 2 {
eprintln!(
"⚠️ Large project detected: {file_count} files. Limited to {MAX_FILES} for performance."
);
}
let mut all_results = Vec::new();
for chunk in paths.chunks(BATCH_SIZE) {
let batch_tasks: Vec<_> = chunk
.iter()
.map(|path| {
let path = path.clone();
let toolchain = toolchain.to_string();
let cache_manager = cache_manager.clone();
tokio::spawn(async move {
let timeout_duration = tokio::time::Duration::from_secs(5);
tokio::time::timeout(timeout_duration, async move {
analyze_file_by_toolchain(&path, &toolchain, cache_manager).await
})
.await
.ok()
.flatten()
})
})
.collect();
let batch_results = join_all(batch_tasks).await;
all_results.extend(
batch_results
.into_iter()
.filter_map(std::result::Result::ok)
.flatten(),
);
}
all_results
}
async fn analyze_file_by_toolchain(
path: &Path,
toolchain: &str,
cache_manager: Option<Arc<SessionCacheManager>>,
) -> Option<FileContext> {
match toolchain {
"rust" => {
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
analyze_rust_file_with_cache(path, cache_manager).await.ok()
} else {
None
}
}
"deno" => analyze_deno_file(path).await,
"python-uv" => {
#[cfg(feature = "python-ast")]
if path.extension().and_then(|s| s.to_str()) == Some("py") {
ast_python::analyze_python_file(path).await.ok()
} else {
None
}
#[cfg(not(feature = "python-ast"))]
None
}
"kotlin" => {
None
}
_ => None,
}
}
async fn analyze_deno_file(path: &Path) -> Option<FileContext> {
let ext = path.extension().and_then(|s| s.to_str());
match ext {
#[cfg(feature = "typescript-ast")]
Some("ts" | "tsx") => ast_typescript::analyze_typescript_file(path).await.ok(),
#[cfg(feature = "typescript-ast")]
Some("js" | "jsx") => ast_typescript::analyze_javascript_file(path).await.ok(),
_ => None,
}
}
async fn build_project_summary(
files: &[FileContext],
root_path: &Path,
toolchain: &str,
) -> ProjectSummary {
let mut summary = ProjectSummary {
total_files: files.len(),
total_functions: 0,
total_structs: 0,
total_enums: 0,
total_traits: 0,
total_impls: 0,
dependencies: Vec::new(),
};
calculate_item_counts(&mut summary, files);
summary.dependencies = read_dependencies(root_path, toolchain).await;
summary
}
fn calculate_item_counts(summary: &mut ProjectSummary, files: &[FileContext]) {
for file in files {
for item in &file.items {
match item {
AstItem::Function { .. } => summary.total_functions += 1,
AstItem::Struct { .. } => summary.total_structs += 1,
AstItem::Enum { .. } => summary.total_enums += 1,
AstItem::Trait { .. } => summary.total_traits += 1,
AstItem::Impl { .. } => summary.total_impls += 1,
_ => {}
}
}
}
}
async fn read_dependencies(root_path: &Path, toolchain: &str) -> Vec<String> {
match toolchain {
"rust" => read_rust_dependencies(root_path).await,
"deno" => read_deno_dependencies(root_path).await,
"python-uv" => read_python_dependencies(root_path).await,
_ => Vec::new(),
}
}
async fn read_rust_dependencies(root_path: &Path) -> Vec<String> {
if let Ok(cargo_content) = tokio::fs::read_to_string(root_path.join("Cargo.toml")).await {
if let Ok(cargo_toml) = cargo_content.parse::<toml::Value>() {
if let Some(deps) = cargo_toml.get("dependencies").and_then(|d| d.as_table()) {
return deps.keys().cloned().collect();
}
}
}
Vec::new()
}
async fn read_deno_dependencies(root_path: &Path) -> Vec<String> {
let mut dependencies = Vec::new();
if let Ok(deno_json) = tokio::fs::read_to_string(root_path.join("deno.json")).await {
if let Ok(deno_config) = serde_json::from_str::<serde_json::Value>(&deno_json) {
if let Some(imports) = deno_config.get("imports").and_then(|i| i.as_object()) {
dependencies.extend(imports.keys().cloned());
}
}
}
if let Ok(package_json) = tokio::fs::read_to_string(root_path.join("package.json")).await {
if let Ok(package) = serde_json::from_str::<serde_json::Value>(&package_json) {
if let Some(deps) = package.get("dependencies").and_then(|d| d.as_object()) {
dependencies.extend(deps.keys().cloned());
}
}
}
dependencies
}
async fn read_python_dependencies(root_path: &Path) -> Vec<String> {
let mut dependencies = Vec::new();
if let Ok(pyproject_content) = tokio::fs::read_to_string(root_path.join("pyproject.toml")).await
{
if let Ok(pyproject) = pyproject_content.parse::<toml::Value>() {
if let Some(deps) = pyproject
.get("project")
.and_then(|p| p.get("dependencies"))
.and_then(|d| d.as_array())
{
dependencies.extend(
deps.iter()
.filter_map(|d| d.as_str())
.map(|s| s.split_whitespace().next().unwrap_or(s).to_string()),
);
}
}
}
if let Ok(requirements) = tokio::fs::read_to_string(root_path.join("requirements.txt")).await {
for line in requirements.lines() {
let line = line.trim();
if !line.is_empty() && !line.starts_with('#') {
let dep_name = line
.split(['=', '>', '<', '~'])
.next()
.unwrap_or(line)
.trim();
dependencies.push(dep_name.to_string());
}
}
}
dependencies
}
pub async fn analyze_project_with_persistent_cache(
root_path: &Path,
toolchain: &str,
cache_manager: Option<Arc<PersistentCacheManager>>,
) -> Result<ProjectContext, TemplateError> {
let gitignore = build_gitignore(root_path)?;
let files =
scan_and_analyze_files_persistent(root_path, toolchain, cache_manager, &gitignore).await;
let summary = build_project_summary(&files, root_path, toolchain).await;
Ok(ProjectContext {
project_type: toolchain.to_string(),
files,
summary,
})
}
async fn scan_and_analyze_files_persistent(
root_path: &Path,
toolchain: &str,
cache_manager: Option<Arc<PersistentCacheManager>>,
gitignore: &ignore::gitignore::Gitignore,
) -> Vec<FileContext> {
const MAX_DEPTH: usize = 10; const MAX_FILES: usize = 10000;
let mut files = Vec::new();
let mut file_count = 0;
for entry in WalkDir::new(root_path)
.follow_links(false)
.max_depth(MAX_DEPTH) .into_iter()
.filter_map(std::result::Result::ok)
{
let path = entry.path();
if gitignore.matched(path, path.is_dir()).is_ignore() {
continue;
}
file_count += 1;
if file_count > MAX_FILES {
eprintln!("⚠️ Reached file limit of {MAX_FILES}. Stopping analysis.");
break;
}
if file_count % 1000 == 0 {
eprintln!("📁 Scanning files... ({file_count} so far)");
}
let timeout_duration = tokio::time::Duration::from_secs(5);
let result = tokio::time::timeout(timeout_duration, async {
analyze_file_by_toolchain_persistent(path, toolchain, cache_manager.clone()).await
})
.await;
if let Ok(Some(file_context)) = result {
files.push(file_context);
}
}
files
}
async fn analyze_file_by_toolchain_persistent(
path: &Path,
toolchain: &str,
cache_manager: Option<Arc<PersistentCacheManager>>,
) -> Option<FileContext> {
match toolchain {
"rust" => {
if path.extension().and_then(|s| s.to_str()) == Some("rs") {
analyze_rust_file_with_persistent_cache(path, cache_manager)
.await
.ok()
} else {
None
}
}
"deno" => analyze_deno_file(path).await,
"python-uv" => {
#[cfg(feature = "python-ast")]
if path.extension().and_then(|s| s.to_str()) == Some("py") {
ast_python::analyze_python_file(path).await.ok()
} else {
None
}
#[cfg(not(feature = "python-ast"))]
None
}
"kotlin" => {
None
}
_ => None,
}
}
#[must_use]
pub fn format_context_as_markdown(context: &ProjectContext) -> String {
let mut output = String::new();
format_header(&mut output, context);
format_summary(&mut output, &context.summary);
format_dependencies(&mut output, &context.summary.dependencies);
format_files(&mut output, &context.files);
format_footer(&mut output);
output
}
fn format_header(output: &mut String, context: &ProjectContext) {
output.push_str(&format!(
"# Project Context: {} Project\n\n",
context.project_type
));
output.push_str(&format!(
"Generated: {}\n\n",
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
));
}
fn format_summary(output: &mut String, summary: &ProjectSummary) {
output.push_str("## Summary\n\n");
output.push_str(&format!("- Files analyzed: {}\n", summary.total_files));
output.push_str(&format!("- Functions: {}\n", summary.total_functions));
output.push_str(&format!("- Structs: {}\n", summary.total_structs));
output.push_str(&format!("- Enums: {}\n", summary.total_enums));
output.push_str(&format!("- Traits: {}\n", summary.total_traits));
output.push_str(&format!("- Implementations: {}\n", summary.total_impls));
}
fn format_dependencies(output: &mut String, dependencies: &[String]) {
if !dependencies.is_empty() {
output.push_str("\n## Dependencies\n\n");
for dep in dependencies {
output.push_str(&format!("- {dep}\n"));
}
}
}
fn format_files(output: &mut String, files: &[FileContext]) {
output.push_str("\n## Files\n\n");
for file in files {
output.push_str(&format!("### {}\n\n", file.path));
let grouped_items = group_items_by_type(&file.items);
format_item_groups(output, &grouped_items);
}
}
struct GroupedItems<'a> {
functions: Vec<&'a AstItem>,
structs: Vec<&'a AstItem>,
enums: Vec<&'a AstItem>,
traits: Vec<&'a AstItem>,
impls: Vec<&'a AstItem>,
modules: Vec<&'a AstItem>,
}
fn group_items_by_type(items: &[AstItem]) -> GroupedItems<'_> {
let mut grouped = GroupedItems {
functions: Vec::new(),
structs: Vec::new(),
enums: Vec::new(),
traits: Vec::new(),
impls: Vec::new(),
modules: Vec::new(),
};
for item in items {
match item {
AstItem::Function { .. } => grouped.functions.push(item),
AstItem::Struct { .. } => grouped.structs.push(item),
AstItem::Enum { .. } => grouped.enums.push(item),
AstItem::Trait { .. } => grouped.traits.push(item),
AstItem::Impl { .. } => grouped.impls.push(item),
AstItem::Module { .. } => grouped.modules.push(item),
_ => {}
}
}
grouped
}
fn format_item_groups(output: &mut String, groups: &GroupedItems) {
format_item_group(output, "Modules", &groups.modules, format_module_item);
format_item_group(output, "Structs", &groups.structs, format_struct_item);
format_item_group(output, "Enums", &groups.enums, format_enum_item);
format_item_group(output, "Traits", &groups.traits, format_trait_item);
format_item_group(output, "Functions", &groups.functions, format_function_item);
format_item_group(output, "Implementations", &groups.impls, format_impl_item);
}
fn format_item_group<F>(output: &mut String, title: &str, items: &[&AstItem], formatter: F)
where
F: Fn(&AstItem) -> String,
{
if !items.is_empty() {
output.push_str(&format!("**{title}:**\n"));
for item in items {
output.push_str(&format!("{}\n", formatter(item)));
}
output.push('\n');
}
}
fn format_module_item(item: &AstItem) -> String {
if let AstItem::Module {
name,
visibility,
line,
} = item
{
format!("- `{visibility} mod {name}` (line {line})")
} else {
String::new()
}
}
fn format_struct_item(item: &AstItem) -> String {
if let AstItem::Struct {
name,
visibility,
fields_count,
derives,
line,
} = item
{
let mut result = format!("- `{visibility} struct {name}` ({fields_count} fields)");
if !derives.is_empty() {
result.push_str(&format!(" [derives: {}]", derives.join(", ")));
}
result.push_str(&format!(" (line {line})"));
result
} else {
String::new()
}
}
fn format_enum_item(item: &AstItem) -> String {
if let AstItem::Enum {
name,
visibility,
variants_count,
line,
} = item
{
format!("- `{visibility} enum {name}` ({variants_count} variants) (line {line})")
} else {
String::new()
}
}
fn format_trait_item(item: &AstItem) -> String {
if let AstItem::Trait {
name,
visibility,
line,
} = item
{
format!("- `{visibility} trait {name}` (line {line})")
} else {
String::new()
}
}
fn format_function_item(item: &AstItem) -> String {
if let AstItem::Function {
name,
visibility,
is_async,
line,
} = item
{
format!(
"- `{} {}fn {}` (line {})",
visibility,
if *is_async { "async " } else { "" },
name,
line
)
} else {
String::new()
}
}
fn format_impl_item(item: &AstItem) -> String {
if let AstItem::Impl {
type_name,
trait_name,
line,
} = item
{
match trait_name {
Some(trait_name) => {
format!("- `impl {trait_name} for {type_name}` (line {line})")
}
None => format!("- `impl {type_name}` (line {line})"),
}
} else {
String::new()
}
}
fn format_footer(output: &mut String) {
output.push_str("---\n");
output.push_str("Generated by paiml-mcp-agent-toolkit\n");
}
#[must_use]
pub fn format_deep_context_as_markdown(context: &DeepContext) -> String {
let mut output = String::new();
output.push_str(&format!(
"# Deep Project Context\n\nGenerated: {}\nTool Version: {}\n\n",
context
.metadata
.generated_at
.format("%Y-%m-%d %H:%M:%S UTC"),
context.metadata.tool_version
));
format_quality_scorecard(&mut output, &context.quality_scorecard);
format_project_summary(&mut output, context);
format_analysis_results(&mut output, &context.analyses);
format_ast_summary(&mut output, &context.analyses.ast_contexts);
output
}
fn format_quality_scorecard(
output: &mut String,
scorecard: &crate::services::deep_context::QualityScorecard,
) {
output.push_str("## Quality Scorecard\n\n");
output.push_str(&format!(
"- **Overall Health**: {:.1}%\n",
scorecard.overall_health
));
output.push_str(&format!(
"- **Complexity Score**: {:.1}%\n",
scorecard.complexity_score
));
output.push_str(&format!(
"- **Maintainability Index**: {:.1}%\n",
scorecard.maintainability_index
));
output.push_str(&format!(
"- **Modularity Score**: {:.1}%\n",
scorecard.modularity_score
));
if let Some(coverage) = scorecard.test_coverage {
output.push_str(&format!("- **Test Coverage**: {coverage:.1}%\n"));
}
output.push_str(&format!(
"- **Refactoring Estimate**: {:.1} hours\n\n",
scorecard.technical_debt_hours
));
}
fn format_project_summary(output: &mut String, context: &DeepContext) {
output.push_str("## Project Summary\n\n");
output.push_str(&format!(
"- **Total Files**: {}\n",
context.file_tree.total_files
));
output.push_str(&format!(
"- **Total Size**: {} bytes\n",
context.file_tree.total_size_bytes
));
output.push_str(&format!(
"- **AST Contexts**: {}\n",
context.analyses.ast_contexts.len()
));
let (functions, structs, enums, traits, impls) =
count_ast_items(&context.analyses.ast_contexts);
output.push_str(&format!("- **Functions**: {functions}\n"));
output.push_str(&format!("- **Structs**: {structs}\n"));
output.push_str(&format!("- **Enums**: {enums}\n"));
output.push_str(&format!("- **Traits**: {traits}\n"));
output.push_str(&format!("- **Implementations**: {impls}\n\n"));
}
fn format_analysis_results(
output: &mut String,
analyses: &crate::services::deep_context::AnalysisResults,
) {
output.push_str("## Analysis Results\n\n");
if let Some(ref complexity) = analyses.complexity_report {
output.push_str(&format!(
"### Complexity Metrics\n\n\
- **Total Files Analyzed**: {}\n\
- **Median Cyclomatic Complexity**: {:.1}\n\
- **Max Cyclomatic Complexity**: {}\n\
- **Median Cognitive Complexity**: {:.1}\n\
- **Max Cognitive Complexity**: {}\n\
- **Refactoring Hours**: {:.1}\n\n",
complexity.files.len(),
complexity.summary.median_cyclomatic,
complexity.summary.max_cyclomatic,
complexity.summary.median_cognitive,
complexity.summary.max_cognitive,
complexity.summary.technical_debt_hours
));
}
if let Some(ref churn) = analyses.churn_analysis {
let hotspots = if churn.summary.hotspot_files.is_empty() {
String::new()
} else {
let mut hotspots_str = "- **Top Hotspots**:\n".to_string();
for (i, hotspot) in churn.summary.hotspot_files.iter().take(5).enumerate() {
hotspots_str.push_str(&format!(" {}. {}\n", i + 1, hotspot.display()));
}
hotspots_str
};
output.push_str(&format!(
"### Code Churn Analysis\n\n\
- **Analysis Period**: {} days\n\
- **Total Files Changed**: {}\n\
- **Total Commits**: {}\n\
- **Hotspot Files**: {}\n{}\n",
churn.period_days,
churn.summary.total_files_changed,
churn.summary.total_commits,
churn.summary.hotspot_files.len(),
hotspots
));
}
if let Some(ref dag) = analyses.dependency_graph {
output.push_str(&format!(
"### Dependency Graph Statistics\n\n\
- **Total Nodes**: {}\n\
- **Total Edges**: {}\n\
- **Graph Analysis**: Dependency relationships analyzed\n\n",
dag.nodes.len(),
dag.edges.len()
));
}
if let Some(ref dead_code) = analyses.dead_code_results {
output.push_str(&format!(
"### Dead Code Analysis\n\n\
- **Total Files Analyzed**: {}\n\
- **Dead Functions Found**: {}\n\
- **Dead Classes Found**: {}\n\
- **Dead Lines**: {}\n\n",
dead_code.summary.total_files_analyzed,
dead_code.summary.dead_functions,
dead_code.summary.dead_classes,
dead_code.summary.total_dead_lines
));
}
if let Some(ref satd) = analyses.satd_results {
output.push_str(&format!(
"### Self-Admitted Debt Analysis\n\n\
- **Total SATD Items**: {}\n\
- **Categories**: Various debt types detected\n\n",
satd.items.len()
));
}
}
fn format_ast_summary(
output: &mut String,
ast_contexts: &[crate::services::deep_context::EnhancedFileContext],
) {
if ast_contexts.is_empty() {
return;
}
output.push_str("## AST Analysis\n\n");
for enhanced_context in ast_contexts.iter().take(20) {
let file_context = &enhanced_context.base;
output.push_str(&format!("### {}\n\n", file_context.path));
let grouped_items = group_items_by_type(&file_context.items);
format_item_groups(output, &grouped_items);
}
}
fn count_ast_items(
ast_contexts: &[crate::services::deep_context::EnhancedFileContext],
) -> (usize, usize, usize, usize, usize) {
let mut functions = 0;
let mut structs = 0;
let mut enums = 0;
let mut traits = 0;
let mut impls = 0;
for enhanced_context in ast_contexts {
for item in &enhanced_context.base.items {
match item {
AstItem::Function { .. } => functions += 1,
AstItem::Struct { .. } => structs += 1,
AstItem::Enum { .. } => enums += 1,
AstItem::Trait { .. } => traits += 1,
AstItem::Impl { .. } => impls += 1,
_ => {}
}
}
}
(functions, structs, enums, traits, impls)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_project_context_creation() {
let context = ProjectContext {
project_type: "rust".to_string(),
files: vec![],
summary: ProjectSummary {
total_files: 0,
total_functions: 0,
total_structs: 0,
total_enums: 0,
total_traits: 0,
total_impls: 0,
dependencies: vec![],
},
};
assert_eq!(context.project_type, "rust");
assert!(context.files.is_empty());
assert_eq!(context.summary.total_files, 0);
}
#[test]
fn test_file_context_creation() {
let file_ctx = FileContext {
path: "src/main.rs".to_string(),
language: "rust".to_string(),
items: vec![],
complexity_metrics: None,
};
assert_eq!(file_ctx.path, "src/main.rs");
assert_eq!(file_ctx.language, "rust");
assert!(file_ctx.items.is_empty());
assert!(file_ctx.complexity_metrics.is_none());
}
#[test]
fn test_ast_item_function() {
let func = AstItem::Function {
name: "test_func".to_string(),
visibility: "pub".to_string(),
is_async: true,
line: 42,
};
assert_eq!(func.display_name(), "test_func");
if let AstItem::Function { name, is_async, .. } = func {
assert_eq!(name, "test_func");
assert!(is_async);
}
}
#[test]
fn test_ast_item_struct() {
let struct_item = AstItem::Struct {
name: "MyStruct".to_string(),
visibility: "pub".to_string(),
fields_count: 3,
derives: vec!["Debug".to_string(), "Clone".to_string()],
line: 10,
};
assert_eq!(struct_item.display_name(), "MyStruct");
if let AstItem::Struct {
fields_count,
derives,
..
} = struct_item
{
assert_eq!(fields_count, 3);
assert_eq!(derives.len(), 2);
}
}
#[test]
fn test_ast_item_enum() {
let enum_item = AstItem::Enum {
name: "MyEnum".to_string(),
visibility: "pub".to_string(),
variants_count: 5,
line: 20,
};
assert_eq!(enum_item.display_name(), "MyEnum");
if let AstItem::Enum { variants_count, .. } = enum_item {
assert_eq!(variants_count, 5);
}
}
#[test]
fn test_ast_item_trait() {
let trait_item = AstItem::Trait {
name: "MyTrait".to_string(),
visibility: "pub".to_string(),
line: 30,
};
assert_eq!(trait_item.display_name(), "MyTrait");
}
#[test]
fn test_ast_item_impl() {
let impl_item = AstItem::Impl {
type_name: "MyStruct".to_string(),
trait_name: Some("Display".to_string()),
line: 40,
};
assert_eq!(impl_item.display_name(), "MyStruct");
if let AstItem::Impl { trait_name, .. } = impl_item {
assert_eq!(trait_name, Some("Display".to_string()));
}
}
#[test]
fn test_ast_item_module() {
let mod_item = AstItem::Module {
name: "utils".to_string(),
visibility: "pub".to_string(),
line: 50,
};
assert_eq!(mod_item.display_name(), "utils");
}
#[test]
fn test_ast_item_use() {
let use_item = AstItem::Use {
path: "std::collections::HashMap".to_string(),
line: 1,
};
assert_eq!(use_item.display_name(), "std::collections::HashMap");
}
#[test]
fn test_ast_item_import() {
let import = AstItem::Import {
module: "numpy".to_string(),
items: vec![],
alias: Some("np".to_string()),
line: 2,
};
assert_eq!(import.display_name(), "numpy");
if let AstItem::Import { alias, .. } = import {
assert_eq!(alias, Some("np".to_string()));
}
}
#[test]
fn test_ast_item_struct_fields_and_derives() {
let struct_item = AstItem::Struct {
name: "MyStruct".to_string(),
visibility: "pub".to_string(),
fields_count: 3,
derives: vec!["Debug".to_string(), "Clone".to_string()],
line: 10,
};
assert_eq!(struct_item.display_name(), "MyStruct");
if let AstItem::Struct {
fields_count,
derives,
..
} = struct_item
{
assert_eq!(fields_count, 3);
assert_eq!(derives.len(), 2);
}
}
#[test]
fn test_project_summary_totals() {
let summary = ProjectSummary {
total_files: 10,
total_functions: 50,
total_structs: 15,
total_enums: 5,
total_traits: 8,
total_impls: 20,
dependencies: vec!["serde".to_string(), "tokio".to_string()],
};
assert_eq!(summary.total_files, 10);
assert_eq!(summary.total_functions, 50);
assert_eq!(summary.dependencies.len(), 2);
assert!(summary.dependencies.contains(&"serde".to_string()));
}
#[tokio::test]
async fn test_analyze_rust_file_simple() {
let temp_dir = TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.rs");
fs::write(
&file_path,
r#"
pub fn hello() {
println!("Hello, world!");
}
pub struct TestStruct {
field: String,
}
pub enum TestEnum {
Variant1,
Variant2,
}
"#,
)
.unwrap();
let result = analyze_rust_file(&file_path).await;
assert!(result.is_ok());
let context = result.unwrap();
assert!(context.path.ends_with("test.rs"));
assert_eq!(context.language, "rust");
let func_count = context
.items
.iter()
.filter(|item| matches!(item, AstItem::Function { .. }))
.count();
let struct_count = context
.items
.iter()
.filter(|item| matches!(item, AstItem::Struct { .. }))
.count();
let enum_count = context
.items
.iter()
.filter(|item| matches!(item, AstItem::Enum { .. }))
.count();
assert_eq!(func_count, 1);
assert_eq!(struct_count, 1);
assert_eq!(enum_count, 1);
}
#[test]
fn test_format_context_as_markdown() {
let context = ProjectContext {
project_type: "rust".to_string(),
files: vec![FileContext {
path: "src/main.rs".to_string(),
language: "rust".to_string(),
items: vec![AstItem::Function {
name: "main".to_string(),
visibility: "pub".to_string(),
is_async: false,
line: 1,
}],
complexity_metrics: None,
}],
summary: ProjectSummary {
total_files: 1,
total_functions: 1,
total_structs: 0,
total_enums: 0,
total_traits: 0,
total_impls: 0,
dependencies: vec![],
},
};
let markdown = format_context_as_markdown(&context);
assert!(markdown.contains("# Project Context"));
assert!(markdown.contains("Type: rust"));
assert!(markdown.contains("Total Files: 1"));
assert!(markdown.contains("Total Functions: 1"));
assert!(markdown.contains("src/main.rs"));
assert!(markdown.contains("main"));
}
#[test]
#[ignore = "Test needs update for new DeepContext API structure"]
fn test_format_deep_context_as_markdown() {
}
#[test]
fn test_rust_visitor_struct() {
use syn::parse_str;
let code = r#"
pub struct TestStruct {
field1: String,
field2: i32,
}
"#;
let syntax = parse_str::<syn::File>(code).unwrap();
let mut visitor = RustVisitor::new(code.to_string());
visitor.visit_file(&syntax);
assert_eq!(visitor.items.len(), 1);
if let AstItem::Struct {
name, fields_count, ..
} = &visitor.items[0]
{
assert_eq!(name, "TestStruct");
assert_eq!(*fields_count, 2);
} else {
panic!("Expected struct item");
}
}
#[test]
fn test_rust_visitor_function() {
use syn::parse_str;
let code = r#"
pub async fn test_function(param: String) -> Result<(), Error> {
Ok(())
}
"#;
let syntax = parse_str::<syn::File>(code).unwrap();
let mut visitor = RustVisitor::new(code.to_string());
visitor.visit_file(&syntax);
assert_eq!(visitor.items.len(), 1);
if let AstItem::Function { name, is_async, .. } = &visitor.items[0] {
assert_eq!(name, "test_function");
assert!(*is_async);
} else {
panic!("Expected function item");
}
}
#[test]
fn test_rust_visitor_enum() {
use syn::parse_str;
let code = r#"
#[derive(Debug, Clone)]
pub enum TestEnum {
Variant1,
Variant2(String),
Variant3 { field: i32 },
}
"#;
let syntax = parse_str::<syn::File>(code).unwrap();
let mut visitor = RustVisitor::new(code.to_string());
visitor.visit_file(&syntax);
assert_eq!(visitor.items.len(), 1);
if let AstItem::Enum {
name,
variants_count,
..
} = &visitor.items[0]
{
assert_eq!(name, "TestEnum");
assert_eq!(*variants_count, 3);
} else {
panic!("Expected enum item");
}
}
#[test]
fn test_rust_visitor_trait() {
use syn::parse_str;
let code = r#"
pub trait TestTrait {
fn method(&self);
}
"#;
let syntax = parse_str::<syn::File>(code).unwrap();
let mut visitor = RustVisitor::new(code.to_string());
visitor.visit_file(&syntax);
assert_eq!(visitor.items.len(), 1);
if let AstItem::Trait { name, .. } = &visitor.items[0] {
assert_eq!(name, "TestTrait");
} else {
panic!("Expected trait item");
}
}
#[test]
fn test_rust_visitor_impl() {
use syn::parse_str;
let code = r#"
impl Display for TestStruct {
fn fmt(&self, f: &mut Formatter) -> Result {
Ok(())
}
}
"#;
let syntax = parse_str::<syn::File>(code).unwrap();
let mut visitor = RustVisitor::new(code.to_string());
visitor.visit_file(&syntax);
assert_eq!(visitor.items.len(), 1);
if let AstItem::Impl {
type_name,
trait_name,
..
} = &visitor.items[0]
{
assert_eq!(type_name, "TestStruct");
assert_eq!(trait_name, &Some("Display".to_string()));
} else {
panic!("Expected impl item");
}
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}