feat: major improvements — layout, cycles, integrations, usage examples, tests #1
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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<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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user