From a3ee00394710dad65f5495ccc7d726d396b44c66 Mon Sep 17 00:00:00 2001 From: Arkasha Date: Sun, 15 Feb 2026 11:14:42 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20all=204=20archdoc=20issues=20=E2=80=94?= =?UTF-8?q?=20cycles,=20layout,=20integrations,=20usage=20examples?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Module Cycles: properly format cycle paths as A → B → C → A 2. Repository layout: group by top-level directory with file counts 3. Integration detection: match patterns against import names (substring), add Storage and AI/ML categories to all templates and summary 4. Usage examples: extract __init__ required params for class constructors Also fix golden test to use ends_with for module-prefixed symbol IDs. --- archdoc-cli/src/commands/init.rs | 8 + archdoc-cli/src/output.rs | 2 + archdoc-core/src/model.rs | 4 + archdoc-core/src/python_analyzer.rs | 83 +++++--- archdoc-core/src/renderer.rs | 213 ++++++++++++++++++-- archdoc-core/tests/enhanced_analysis.rs | 6 +- archdoc-core/tests/golden/mod.rs | 4 +- archdoc-core/tests/integration_detection.rs | 77 +++---- archdoc-core/tests/renderer_tests.rs | 4 + test-project/ARCHITECTURE.md | 2 +- 10 files changed, 314 insertions(+), 89 deletions(-) diff --git a/archdoc-cli/src/commands/init.rs b/archdoc-cli/src/commands/init.rs index 43f9a56..6c4a81b 100644 --- a/archdoc-cli/src/commands/init.rs +++ b/archdoc-cli/src/commands/init.rs @@ -49,6 +49,14 @@ pub fn init_project(root: &str, out: &str) -> Result<()> { --- +## Integrations + +> Generated. Do not edit inside this block. + + + +--- + ## Rails / Tooling > Generated. Do not edit inside this block. diff --git a/archdoc-cli/src/output.rs b/archdoc-cli/src/output.rs index 6d90224..65026e5 100644 --- a/archdoc-cli/src/output.rs +++ b/archdoc-cli/src/output.rs @@ -24,6 +24,8 @@ pub fn print_generate_summary(model: &ProjectModel) { if model.symbols.values().any(|s| s.integrations_flags.http) { v.push("HTTP"); } if model.symbols.values().any(|s| s.integrations_flags.db) { v.push("DB"); } if model.symbols.values().any(|s| s.integrations_flags.queue) { v.push("Queue"); } + if model.symbols.values().any(|s| s.integrations_flags.storage) { v.push("Storage"); } + if model.symbols.values().any(|s| s.integrations_flags.ai) { v.push("AI/ML"); } v }; if !integrations.is_empty() { diff --git a/archdoc-core/src/model.rs b/archdoc-core/src/model.rs index 764559d..0832a43 100644 --- a/archdoc-core/src/model.rs +++ b/archdoc-core/src/model.rs @@ -84,6 +84,10 @@ pub struct IntegrationFlags { pub http: bool, pub db: bool, pub queue: bool, + #[serde(default)] + pub storage: bool, + #[serde(default)] + pub ai: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/archdoc-core/src/python_analyzer.rs b/archdoc-core/src/python_analyzer.rs index 66ffd88..b474fac 100644 --- a/archdoc-core/src/python_analyzer.rs +++ b/archdoc-core/src/python_analyzer.rs @@ -364,40 +364,59 @@ impl PythonAnalyzer { None } - fn detect_integrations(&self, body: &[Stmt], config: &Config) -> crate::model::IntegrationFlags { + fn detect_integrations(&self, _body: &[Stmt], _config: &Config) -> crate::model::IntegrationFlags { + // Integration detection is now done at module level in resolve_symbols + // based on actual imports, not AST body debug strings + crate::model::IntegrationFlags { + http: false, + db: false, + queue: false, + storage: false, + ai: false, + } + } + + /// Detect integrations for a module based on its actual imports + fn detect_module_integrations(&self, imports: &[Import], config: &Config) -> crate::model::IntegrationFlags { let mut flags = crate::model::IntegrationFlags { http: false, db: false, queue: false, + storage: false, + ai: false, }; if !config.analysis.detect_integrations { return flags; } - let body_str = format!("{:?}", body); + // Build a set of all import names (both module names and their parts) + let import_names: Vec = imports.iter().flat_map(|imp| { + let mut names = vec![imp.module_name.clone()]; + // Also add individual parts: "from minio import Minio" -> module_name is "minio.Minio" + for part in imp.module_name.split('.') { + names.push(part.to_lowercase()); + } + names + }).collect(); for pattern in &config.analysis.integration_patterns { - if pattern.type_ == "http" { - for lib in &pattern.patterns { - if body_str.contains(lib) { - flags.http = true; - break; - } - } - } else if pattern.type_ == "db" { - for lib in &pattern.patterns { - if body_str.contains(lib) { - flags.db = true; - break; - } - } - } else if pattern.type_ == "queue" { - for lib in &pattern.patterns { - if body_str.contains(lib) { - flags.queue = true; - break; + for lib in &pattern.patterns { + let lib_lower = lib.to_lowercase(); + let matched = import_names.iter().any(|name| { + let name_lower = name.to_lowercase(); + name_lower.contains(&lib_lower) + }); + if matched { + match pattern.type_.as_str() { + "http" => flags.http = true, + "db" => flags.db = true, + "queue" => flags.queue = true, + "storage" => flags.storage = true, + "ai" => flags.ai = true, + _ => {} } + break; } } } @@ -610,15 +629,28 @@ impl PythonAnalyzer { imports: parsed_module.imports.iter().map(|i| i.module_name.clone()).collect(), outbound_modules: Vec::new(), inbound_files: Vec::new(), - symbols: parsed_module.symbols.iter().map(|s| s.id.clone()).collect(), + symbols: parsed_module.symbols.iter().map(|s| format!("{}::{}", module_id, s.id)).collect(), file_purpose, }; project_model.files.insert(file_id.clone(), file_doc); + // Detect integrations based on actual imports + let module_integrations = self.detect_module_integrations(&parsed_module.imports, &self.config); + let mut module_symbol_ids = Vec::new(); for mut symbol in parsed_module.symbols.clone() { symbol.module_id = module_id.clone(); symbol.file_id = file_id.clone(); - project_model.symbols.insert(symbol.id.clone(), symbol); + // Make symbol ID unique by prefixing with module + let unique_id = format!("{}::{}", module_id, symbol.id); + symbol.id = unique_id.clone(); + // Apply module-level integration flags to all symbols + symbol.integrations_flags.http |= module_integrations.http; + symbol.integrations_flags.db |= module_integrations.db; + symbol.integrations_flags.queue |= module_integrations.queue; + symbol.integrations_flags.storage |= module_integrations.storage; + symbol.integrations_flags.ai |= module_integrations.ai; + module_symbol_ids.push(unique_id.clone()); + project_model.symbols.insert(unique_id, symbol); } // Use __init__.py docstring for module doc_summary, or file docstring for single-file modules @@ -638,7 +670,7 @@ impl PythonAnalyzer { doc_summary, outbound_modules: Vec::new(), inbound_modules: Vec::new(), - symbols: parsed_module.symbols.iter().map(|s| s.id.clone()).collect(), + symbols: module_symbol_ids, }; project_model.modules.insert(module_id, module); } @@ -787,7 +819,8 @@ impl PythonAnalyzer { if let Some(symbol) = project_model.symbols.get_mut(symbol_id) { symbol.metrics.fan_in = *fan_in; symbol.metrics.fan_out = *fan_out; - symbol.metrics.is_critical = *fan_in > 10 || *fan_out > 10; + symbol.metrics.is_critical = *fan_in > self.config.thresholds.critical_fan_in + || *fan_out > self.config.thresholds.critical_fan_out; } } diff --git a/archdoc-core/src/renderer.rs b/archdoc-core/src/renderer.rs index 46d9910..5df0dd3 100644 --- a/archdoc-core/src/renderer.rs +++ b/archdoc-core/src/renderer.rs @@ -87,6 +87,16 @@ impl Renderer { {{#each queue_integrations}} - {{{this}}} {{/each}} + +### Storage Integrations +{{#each storage_integrations}} +- {{{this}}} +{{/each}} + +### AI/ML Integrations +{{#each ai_integrations}} +- {{{this}}} +{{/each}} --- @@ -222,6 +232,20 @@ impl Renderer { {{/each}} {{/if}} +{{#if has_storage_integrations}} +### Storage Integrations +{{#each storage_symbols}} +- {{{this}}} +{{/each}} +{{/if}} + +{{#if has_ai_integrations}} +### AI/ML Integrations +{{#each ai_symbols}} +- {{{this}}} +{{/each}} +{{/if}} + ## Usage Examples {{#each usage_examples}} @@ -238,6 +262,8 @@ impl Renderer { let mut db_integrations = Vec::new(); let mut http_integrations = Vec::new(); let mut queue_integrations = Vec::new(); + let mut storage_integrations = Vec::new(); + let mut ai_integrations = Vec::new(); for (symbol_id, symbol) in &model.symbols { if symbol.integrations_flags.db { @@ -249,9 +275,15 @@ impl Renderer { if symbol.integrations_flags.queue { queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); } + if symbol.integrations_flags.storage { + storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); + } + if symbol.integrations_flags.ai { + ai_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); + } } - // Determine project name: config > directory name > fallback + // Determine project name: config > pyproject.toml > directory name > fallback let project_name = config .and_then(|c| { if c.project.name.is_empty() { @@ -260,6 +292,36 @@ impl Renderer { Some(c.project.name.clone()) } }) + .or_else(|| { + // Try pyproject.toml + config.and_then(|c| { + let pyproject_path = std::path::Path::new(&c.project.root).join("pyproject.toml"); + std::fs::read_to_string(&pyproject_path).ok().and_then(|content| { + // Simple TOML parsing for [project] name = "..." + let mut in_project = false; + for line in content.lines() { + let trimmed = line.trim(); + if trimmed == "[project]" { + in_project = true; + continue; + } + if trimmed.starts_with('[') { + in_project = false; + continue; + } + if in_project && trimmed.starts_with("name") { + if let Some(val) = trimmed.split('=').nth(1) { + let name = val.trim().trim_matches('"').trim_matches('\''); + if !name.is_empty() { + return Some(name.to_string()); + } + } + } + } + None + }) + }) + }) .or_else(|| { config.map(|c| { std::path::Path::new(&c.project.root) @@ -273,14 +335,31 @@ impl Renderer { let today = Utc::now().format("%Y-%m-%d").to_string(); - // Collect layout items for template - let mut layout_items = Vec::new(); + // Collect layout items grouped by top-level directory + let mut dir_files: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for file_doc in model.files.values() { - let purpose = file_doc.file_purpose.as_deref().unwrap_or("Source file"); + let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path); + let top_dir = path.split('/').next().unwrap_or(path); + // If file is at root level (no '/'), use the filename itself + let top = if path.contains('/') { + format!("{}/", top_dir) + } else { + path.to_string() + }; + dir_files.entry(top).or_default().push(path.to_string()); + } + let mut layout_items = Vec::new(); + for (dir, files) in &dir_files { + let file_count = files.len(); + let purpose = if dir.ends_with('/') { + format!("{} files", file_count) + } else { + "Root file".to_string() + }; layout_items.push(serde_json::json!({ - "path": file_doc.path, + "path": dir, "purpose": purpose, - "link": format!("docs/architecture/files/{}.md", sanitize_for_link(&file_doc.path)) + "link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/'))) })); } @@ -343,6 +422,8 @@ impl Renderer { "db_integrations": db_integrations, "http_integrations": http_integrations, "queue_integrations": queue_integrations, + "storage_integrations": storage_integrations, + "ai_integrations": ai_integrations, "rails_summary": "\n\nNo tooling information available.\n", "layout_items": layout_items, "modules": modules_list, @@ -380,6 +461,8 @@ impl Renderer { let mut db_symbols = Vec::new(); let mut http_symbols = Vec::new(); let mut queue_symbols = Vec::new(); + let mut storage_symbols = Vec::new(); + let mut ai_symbols = Vec::new(); for symbol_id in &module.symbols { if let Some(symbol) = model.symbols.get(symbol_id) { @@ -392,6 +475,12 @@ impl Renderer { if symbol.integrations_flags.queue { queue_symbols.push(symbol.qualname.clone()); } + if symbol.integrations_flags.storage { + storage_symbols.push(symbol.qualname.clone()); + } + if symbol.integrations_flags.ai { + ai_symbols.push(symbol.qualname.clone()); + } } } @@ -425,9 +514,33 @@ impl Renderer { )); } SymbolKind::Class => { + // Find __init__ method to get constructor args + let init_name = format!("{}.__init__", short_name); + let init_args = module.symbols.iter() + .find_map(|sid| { + model.symbols.get(sid).and_then(|s| { + if s.qualname == init_name || s.id == init_name { + // Extract args from __init__ signature + let args = s.signature + .find('(') + .and_then(|start| s.signature.rfind(')').map(|end| (start, end))) + .map(|(st, en)| &s.signature[st+1..en]) + .unwrap_or(""); + let clean = args.split(',') + .map(|a| a.split(':').next().unwrap_or("").split('=').next().unwrap_or("").trim()) + .filter(|a| !a.is_empty() && *a != "self" && *a != "cls" && !a.starts_with('*')) + .collect::>() + .join(", "); + Some(clean) + } else { + None + } + }) + }) + .unwrap_or_default(); usage_examples.push(format!( - "from {} import {}\ninstance = {}()", - module_id, short_name, short_name + "from {} import {}\ninstance = {}({})", + module_id, short_name, short_name, init_args )); } SymbolKind::Method => { @@ -451,9 +564,13 @@ impl Renderer { "has_db_integrations": !db_symbols.is_empty(), "has_http_integrations": !http_symbols.is_empty(), "has_queue_integrations": !queue_symbols.is_empty(), + "has_storage_integrations": !storage_symbols.is_empty(), + "has_ai_integrations": !ai_symbols.is_empty(), "db_symbols": db_symbols, "http_symbols": http_symbols, "queue_symbols": queue_symbols, + "storage_symbols": storage_symbols, + "ai_symbols": ai_symbols, "usage_examples": usage_examples, }); @@ -466,6 +583,8 @@ impl Renderer { let mut db_integrations = Vec::new(); let mut http_integrations = Vec::new(); let mut queue_integrations = Vec::new(); + let mut storage_integrations = Vec::new(); + let mut ai_integrations = Vec::new(); for (symbol_id, symbol) in &model.symbols { if symbol.integrations_flags.db { @@ -477,6 +596,12 @@ impl Renderer { if symbol.integrations_flags.queue { queue_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); } + if symbol.integrations_flags.storage { + storage_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); + } + if symbol.integrations_flags.ai { + ai_integrations.push(format!("{} in {}", symbol_id, symbol.file_id)); + } } // Prepare data for integrations section @@ -484,6 +609,8 @@ impl Renderer { "db_integrations": db_integrations, "http_integrations": http_integrations, "queue_integrations": queue_integrations, + "storage_integrations": storage_integrations, + "ai_integrations": ai_integrations, }); // Create a smaller template just for the integrations section @@ -503,6 +630,16 @@ impl Renderer { {{#each queue_integrations}} - {{{this}}} {{/each}} + +### Storage Integrations +{{#each storage_integrations}} +- {{{this}}} +{{/each}} + +### AI/ML Integrations +{{#each ai_integrations}} +- {{{this}}} +{{/each}} "#; let mut handlebars = Handlebars::new(); @@ -519,15 +656,30 @@ impl Renderer { } pub fn render_layout_section(&self, model: &ProjectModel) -> Result { - // Collect layout information from files - let mut layout_items = Vec::new(); - + // Collect layout items grouped by top-level directory + let mut dir_files: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for file_doc in model.files.values() { - let purpose = file_doc.file_purpose.as_deref().unwrap_or("Source file"); + let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path); + let top_dir = path.split('/').next().unwrap_or(path); + let top = if path.contains('/') { + format!("{}/", top_dir) + } else { + path.to_string() + }; + dir_files.entry(top).or_default().push(path.to_string()); + } + let mut layout_items = Vec::new(); + for (dir, files) in &dir_files { + let file_count = files.len(); + let purpose = if dir.ends_with('/') { + format!("{} files", file_count) + } else { + "Root file".to_string() + }; layout_items.push(serde_json::json!({ - "path": file_doc.path, + "path": dir, "purpose": purpose, - "link": format!("docs/architecture/files/{}.md", sanitize_for_link(&file_doc.path)) + "link": format!("docs/architecture/files/{}.md", sanitize_for_link(dir.trim_end_matches('/'))) })); } @@ -646,7 +798,7 @@ impl Renderer { ### Module Cycles {{#each cycles}} -- {{{this}}} +- {{{cycle_path}}} {{/each}} "#; @@ -659,15 +811,30 @@ impl Renderer { } pub fn render_layout_md(&self, model: &ProjectModel) -> Result { - // Collect layout information from files - let mut layout_items = Vec::new(); - + // Collect layout items grouped by top-level directory + let mut dir_files: std::collections::BTreeMap> = std::collections::BTreeMap::new(); for file_doc in model.files.values() { - let purpose = file_doc.file_purpose.as_deref().unwrap_or("Source file"); + let path = file_doc.path.strip_prefix("./").unwrap_or(&file_doc.path); + let top_dir = path.split('/').next().unwrap_or(path); + let top = if path.contains('/') { + format!("{}/", top_dir) + } else { + path.to_string() + }; + dir_files.entry(top).or_default().push(path.to_string()); + } + let mut layout_items = Vec::new(); + for (dir, files) in &dir_files { + let file_count = files.len(); + let purpose = if dir.ends_with('/') { + format!("{} files", file_count) + } else { + "Root file".to_string() + }; layout_items.push(serde_json::json!({ - "path": file_doc.path, + "path": dir, "purpose": purpose, - "link": format!("files/{}.md", sanitize_for_link(&file_doc.path)) + "link": format!("files/{}.md", sanitize_for_link(dir.trim_end_matches('/'))) })); } @@ -722,6 +889,8 @@ impl Renderer { "http": symbol.integrations_flags.http, "db": symbol.integrations_flags.db, "queue": symbol.integrations_flags.queue, + "storage": symbol.integrations_flags.storage, + "ai": symbol.integrations_flags.ai, }, "metrics": { "fan_in": symbol.metrics.fan_in, @@ -764,6 +933,8 @@ impl Renderer { - HTTP: {{#if integrations.http}}yes{{else}}no{{/if}} - DB: {{#if integrations.db}}yes{{else}}no{{/if}} - Queue/Tasks: {{#if integrations.queue}}yes{{else}}no{{/if}} +- Storage: {{#if integrations.storage}}yes{{else}}no{{/if}} +- AI/ML: {{#if integrations.ai}}yes{{else}}no{{/if}} #### Risk / impact diff --git a/archdoc-core/tests/enhanced_analysis.rs b/archdoc-core/tests/enhanced_analysis.rs index 8a6b753..6741b6b 100644 --- a/archdoc-core/tests/enhanced_analysis.rs +++ b/archdoc-core/tests/enhanced_analysis.rs @@ -98,17 +98,17 @@ fn test_enhanced_analysis_with_integrations() { assert!(found_advanced_module); // Check that we found the UserService class with DB integration - let user_service_symbol = project_model.symbols.values().find(|s| s.id == "UserService"); + let user_service_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::UserService")); assert!(user_service_symbol.is_some()); assert_eq!(user_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class); // Check that we found the NotificationService class with queue integration - let notification_service_symbol = project_model.symbols.values().find(|s| s.id == "NotificationService"); + let notification_service_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::NotificationService")); assert!(notification_service_symbol.is_some()); assert_eq!(notification_service_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class); // Check that we found the fetch_external_user_data function with HTTP integration - let fetch_external_user_data_symbol = project_model.symbols.values().find(|s| s.id == "fetch_external_user_data"); + let fetch_external_user_data_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::fetch_external_user_data")); assert!(fetch_external_user_data_symbol.is_some()); assert_eq!(fetch_external_user_data_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function); diff --git a/archdoc-core/tests/golden/mod.rs b/archdoc-core/tests/golden/mod.rs index 26ef4f2..78b2051 100644 --- a/archdoc-core/tests/golden/mod.rs +++ b/archdoc-core/tests/golden/mod.rs @@ -90,12 +90,12 @@ fn test_simple_project_generation() { assert!(found_example_module); // Check that we found the Calculator class - let calculator_symbol = project_model.symbols.values().find(|s| s.id == "Calculator"); + let calculator_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::Calculator")); assert!(calculator_symbol.is_some()); assert_eq!(calculator_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Class); // Check that we found the process_numbers function - let process_numbers_symbol = project_model.symbols.values().find(|s| s.id == "process_numbers"); + let process_numbers_symbol = project_model.symbols.values().find(|s| s.id.ends_with("::process_numbers")); assert!(process_numbers_symbol.is_some()); assert_eq!(process_numbers_symbol.unwrap().kind, archdoc_core::model::SymbolKind::Function); diff --git a/archdoc-core/tests/integration_detection.rs b/archdoc-core/tests/integration_detection.rs index 2cebdcf..b237577 100644 --- a/archdoc-core/tests/integration_detection.rs +++ b/archdoc-core/tests/integration_detection.rs @@ -1,6 +1,8 @@ //! Integration detection tests for ArchDoc //! //! These tests verify that the integration detection functionality works correctly. +//! Integration detection now happens at module level during resolve_symbols, +//! based on actual imports rather than AST body inspection. use std::fs; use tempfile::TempDir; @@ -8,11 +10,12 @@ use archdoc_core::{Config, python_analyzer::PythonAnalyzer}; #[test] fn test_http_integration_detection() { - let config = Config::default(); + let mut config = Config::default(); + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + config.project.root = temp_dir.path().to_string_lossy().to_string(); + config.python.src_roots = vec![".".to_string()]; let analyzer = PythonAnalyzer::new(config); - // Create a temporary Python file with HTTP integration - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let temp_file = temp_dir.path().join("test.py"); let python_code = r#" import requests @@ -23,16 +26,16 @@ def fetch_data(): "#; fs::write(&temp_file, python_code).expect("Failed to write test file"); - // Parse the module let parsed_module = analyzer.parse_module(&temp_file) .expect("Failed to parse module"); - // Check that we found the function - assert_eq!(parsed_module.symbols.len(), 1); - let symbol = &parsed_module.symbols[0]; - assert_eq!(symbol.id, "fetch_data"); + let model = analyzer.resolve_symbols(&[parsed_module]) + .expect("Failed to resolve symbols"); + + // Find the symbol (now prefixed with module id) + let symbol = model.symbols.values().find(|s| s.qualname == "fetch_data") + .expect("fetch_data symbol not found"); - // Check that HTTP integration is detected assert!(symbol.integrations_flags.http); assert!(!symbol.integrations_flags.db); assert!(!symbol.integrations_flags.queue); @@ -40,11 +43,12 @@ def fetch_data(): #[test] fn test_db_integration_detection() { - let config = Config::default(); + let mut config = Config::default(); + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + config.project.root = temp_dir.path().to_string_lossy().to_string(); + config.python.src_roots = vec![".".to_string()]; let analyzer = PythonAnalyzer::new(config); - // Create a temporary Python file with DB integration - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let temp_file = temp_dir.path().join("test.py"); let python_code = r#" import sqlite3 @@ -57,16 +61,15 @@ def get_user(user_id): "#; fs::write(&temp_file, python_code).expect("Failed to write test file"); - // Parse the module let parsed_module = analyzer.parse_module(&temp_file) .expect("Failed to parse module"); - // Check that we found the function - assert_eq!(parsed_module.symbols.len(), 1); - let symbol = &parsed_module.symbols[0]; - assert_eq!(symbol.id, "get_user"); + let model = analyzer.resolve_symbols(&[parsed_module]) + .expect("Failed to resolve symbols"); + + let symbol = model.symbols.values().find(|s| s.qualname == "get_user") + .expect("get_user symbol not found"); - // Check that DB integration is detected assert!(!symbol.integrations_flags.http); assert!(symbol.integrations_flags.db); assert!(!symbol.integrations_flags.queue); @@ -74,11 +77,12 @@ def get_user(user_id): #[test] fn test_queue_integration_detection() { - let config = Config::default(); + let mut config = Config::default(); + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + config.project.root = temp_dir.path().to_string_lossy().to_string(); + config.python.src_roots = vec![".".to_string()]; let analyzer = PythonAnalyzer::new(config); - // Create a temporary Python file with queue integration - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let temp_file = temp_dir.path().join("test.py"); let python_code = r#" import redis @@ -89,16 +93,15 @@ def process_job(job_data): "#; fs::write(&temp_file, python_code).expect("Failed to write test file"); - // Parse the module let parsed_module = analyzer.parse_module(&temp_file) .expect("Failed to parse module"); - // Check that we found the function - assert_eq!(parsed_module.symbols.len(), 1); - let symbol = &parsed_module.symbols[0]; - assert_eq!(symbol.id, "process_job"); + let model = analyzer.resolve_symbols(&[parsed_module]) + .expect("Failed to resolve symbols"); + + let symbol = model.symbols.values().find(|s| s.qualname == "process_job") + .expect("process_job symbol not found"); - // Check that queue integration is detected assert!(!symbol.integrations_flags.http); assert!(!symbol.integrations_flags.db); assert!(symbol.integrations_flags.queue); @@ -106,11 +109,12 @@ def process_job(job_data): #[test] fn test_no_integration_detection() { - let config = Config::default(); + let mut config = Config::default(); + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + config.project.root = temp_dir.path().to_string_lossy().to_string(); + config.python.src_roots = vec![".".to_string()]; let analyzer = PythonAnalyzer::new(config); - // Create a temporary Python file with no integrations - let temp_dir = TempDir::new().expect("Failed to create temp dir"); let temp_file = temp_dir.path().join("test.py"); let python_code = r#" def calculate_sum(a, b): @@ -118,17 +122,16 @@ def calculate_sum(a, b): "#; fs::write(&temp_file, python_code).expect("Failed to write test file"); - // Parse the module let parsed_module = analyzer.parse_module(&temp_file) .expect("Failed to parse module"); - // Check that we found the function - assert_eq!(parsed_module.symbols.len(), 1); - let symbol = &parsed_module.symbols[0]; - assert_eq!(symbol.id, "calculate_sum"); + let model = analyzer.resolve_symbols(&[parsed_module]) + .expect("Failed to resolve symbols"); + + let symbol = model.symbols.values().find(|s| s.qualname == "calculate_sum") + .expect("calculate_sum symbol not found"); - // Check that no integrations are detected assert!(!symbol.integrations_flags.http); assert!(!symbol.integrations_flags.db); assert!(!symbol.integrations_flags.queue); -} \ No newline at end of file +} diff --git a/archdoc-core/tests/renderer_tests.rs b/archdoc-core/tests/renderer_tests.rs index 2d2fb87..b58a9bc 100644 --- a/archdoc-core/tests/renderer_tests.rs +++ b/archdoc-core/tests/renderer_tests.rs @@ -28,6 +28,8 @@ fn test_render_with_integrations() { db: true, http: false, queue: false, + storage: false, + ai: false, }, metrics: SymbolMetrics { fan_in: 0, @@ -54,6 +56,8 @@ fn test_render_with_integrations() { db: false, http: true, queue: false, + storage: false, + ai: false, }, metrics: SymbolMetrics { fan_in: 0, diff --git a/test-project/ARCHITECTURE.md b/test-project/ARCHITECTURE.md index 79a030d..f9def1c 100644 --- a/test-project/ARCHITECTURE.md +++ b/test-project/ARCHITECTURE.md @@ -46,9 +46,9 @@ No tooling information available. | Module | Symbols | Inbound | Outbound | Link | |--------|---------|---------|----------|------| -| core | 6 | 0 | 0 | [details](docs/architecture/modules/core.md) | | utils | 4 | 0 | 0 | [details](docs/architecture/modules/utils.md) | | src | 0 | 0 | 0 | [details](docs/architecture/modules/src.md) | +| core | 6 | 0 | 0 | [details](docs/architecture/modules/core.md) | ---