Compare commits
3 Commits
8e72f140d2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 3097764fb9 | |||
| 136697caf0 | |||
| 8e79e3950f |
@@ -1,9 +1,48 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
|
|
||||||
|
/// Detect project name from pyproject.toml or directory basename.
|
||||||
|
fn detect_project_name(root: &str) -> String {
|
||||||
|
let root_path = std::path::Path::new(root);
|
||||||
|
|
||||||
|
// Try pyproject.toml
|
||||||
|
let pyproject_path = root_path.join("pyproject.toml");
|
||||||
|
if let Ok(content) = std::fs::read_to_string(&pyproject_path) {
|
||||||
|
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 name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: directory basename
|
||||||
|
root_path
|
||||||
|
.canonicalize()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
|
||||||
|
.unwrap_or_else(|| "Project".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_project(root: &str, out: &str) -> Result<()> {
|
pub fn init_project(root: &str, out: &str) -> Result<()> {
|
||||||
println!("{}", "Initializing archdoc project...".cyan().bold());
|
println!("{}", "Initializing archdoc project...".cyan().bold());
|
||||||
|
|
||||||
|
let project_name = detect_project_name(root);
|
||||||
|
|
||||||
let out_path = std::path::Path::new(out);
|
let out_path = std::path::Path::new(out);
|
||||||
std::fs::create_dir_all(out_path)?;
|
std::fs::create_dir_all(out_path)?;
|
||||||
std::fs::create_dir_all(out_path.join("modules"))?;
|
std::fs::create_dir_all(out_path.join("modules"))?;
|
||||||
@@ -95,8 +134,10 @@ pub fn init_project(root: &str, out: &str) -> Result<()> {
|
|||||||
<!-- MANUAL:END -->
|
<!-- MANUAL:END -->
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
let architecture_md_content = architecture_md_content.replace("<PROJECT_NAME>", &project_name);
|
||||||
|
|
||||||
let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md");
|
let architecture_md_path = std::path::Path::new(root).join("ARCHITECTURE.md");
|
||||||
std::fs::write(&architecture_md_path, architecture_md_content)?;
|
std::fs::write(&architecture_md_path, &architecture_md_content)?;
|
||||||
|
|
||||||
let config_toml_content = r#"[project]
|
let config_toml_content = r#"[project]
|
||||||
root = "."
|
root = "."
|
||||||
|
|||||||
@@ -658,9 +658,9 @@ impl PythonAnalyzer {
|
|||||||
let doc_summary = if is_init {
|
let doc_summary = if is_init {
|
||||||
parsed_module.file_docstring.clone()
|
parsed_module.file_docstring.clone()
|
||||||
} else {
|
} else {
|
||||||
// For non-init files, check if there's an __init__.py docstring for this module's parent
|
// For non-init files, use file docstring first, then check __init__.py
|
||||||
init_docstrings.get(&module_id).cloned()
|
parsed_module.file_docstring.clone()
|
||||||
.or_else(|| parsed_module.file_docstring.clone())
|
.or_else(|| init_docstrings.get(&module_id).cloned())
|
||||||
};
|
};
|
||||||
|
|
||||||
let module = Module {
|
let module = Module {
|
||||||
@@ -799,6 +799,25 @@ impl PythonAnalyzer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check if a class symbol is a simple data container (dataclass-like).
|
||||||
|
/// A class is considered a dataclass if it has ≤2 methods (typically __init__ and __repr__/__str__).
|
||||||
|
fn is_dataclass_like(symbol_id: &str, project_model: &ProjectModel) -> bool {
|
||||||
|
let symbol = match project_model.symbols.get(symbol_id) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return false,
|
||||||
|
};
|
||||||
|
if symbol.kind != crate::model::SymbolKind::Class {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Count methods belonging to this class
|
||||||
|
let class_name = &symbol.qualname;
|
||||||
|
let method_prefix = format!("{}::{}.", symbol.module_id, class_name);
|
||||||
|
let method_count = project_model.symbols.values()
|
||||||
|
.filter(|s| s.kind == crate::model::SymbolKind::Method && s.id.starts_with(&method_prefix))
|
||||||
|
.count();
|
||||||
|
method_count <= 2
|
||||||
|
}
|
||||||
|
|
||||||
fn compute_metrics(&self, project_model: &mut ProjectModel) -> Result<(), ArchDocError> {
|
fn compute_metrics(&self, project_model: &mut ProjectModel) -> Result<(), ArchDocError> {
|
||||||
// Collect fan-in/fan-out first to avoid borrow issues
|
// Collect fan-in/fan-out first to avoid borrow issues
|
||||||
let mut metrics: std::collections::HashMap<String, (usize, usize)> = std::collections::HashMap::new();
|
let mut metrics: std::collections::HashMap<String, (usize, usize)> = std::collections::HashMap::new();
|
||||||
@@ -815,12 +834,20 @@ impl PythonAnalyzer {
|
|||||||
metrics.insert(symbol_id.clone(), (fan_in, fan_out));
|
metrics.insert(symbol_id.clone(), (fan_in, fan_out));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-compute which symbols are dataclass-like (need immutable borrow)
|
||||||
|
let dataclass_ids: std::collections::HashSet<String> = metrics.keys()
|
||||||
|
.filter(|id| Self::is_dataclass_like(id, project_model))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
for (symbol_id, (fan_in, fan_out)) in &metrics {
|
for (symbol_id, (fan_in, fan_out)) in &metrics {
|
||||||
if let Some(symbol) = project_model.symbols.get_mut(symbol_id) {
|
if let Some(symbol) = project_model.symbols.get_mut(symbol_id) {
|
||||||
symbol.metrics.fan_in = *fan_in;
|
symbol.metrics.fan_in = *fan_in;
|
||||||
symbol.metrics.fan_out = *fan_out;
|
symbol.metrics.fan_out = *fan_out;
|
||||||
symbol.metrics.is_critical = *fan_in > self.config.thresholds.critical_fan_in
|
// Don't mark dataclass-like classes as critical — they're just data containers
|
||||||
|
let exceeds_threshold = *fan_in > self.config.thresholds.critical_fan_in
|
||||||
|| *fan_out > self.config.thresholds.critical_fan_out;
|
|| *fan_out > self.config.thresholds.critical_fan_out;
|
||||||
|
symbol.metrics.is_critical = exceeds_threshold && !dataclass_ids.contains(symbol_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user