feat: major improvements — layout, cycles, integrations, usage examples, tests #1

Merged
dparmeev merged 15 commits from feature/improvements-v2 into main 2026-02-15 11:21:47 +03:00
10 changed files with 314 additions and 89 deletions
Showing only changes of commit a3ee003947 - Show all commits

View File

@@ -49,6 +49,14 @@ pub fn init_project(root: &str, out: &str) -> Result<()> {
---
## Integrations
<!-- ARCHDOC:BEGIN section=integrations -->
> Generated. Do not edit inside this block.
<AUTO: detected integrations by category>
<!-- ARCHDOC:END section=integrations -->
---
## Rails / Tooling
<!-- ARCHDOC:BEGIN section=rails -->
> Generated. Do not edit inside this block.

View File

@@ -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() {

View File

@@ -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)]

View File

@@ -364,42 +364,61 @@ 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<String> = 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;
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;
}
}
} 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;
}
}
}
}
flags
@@ -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;
}
}

View File

@@ -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}}
<!-- ARCHDOC:END section=integrations -->
---
@@ -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<String, Vec<String>> = 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::<Vec<_>>()
.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<String, anyhow::Error> {
// 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<String, Vec<String>> = 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<String, anyhow::Error> {
// 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<String, Vec<String>> = 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}}
<!-- ARCHDOC:END section=integrations -->
#### Risk / impact

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,16 +122,15 @@ 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);

View File

@@ -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,

View File

@@ -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) |
<!-- ARCHDOC:END section=modules_index -->
---