fix: sort modules index and critical points, fix per-symbol fan-in/fan-out metrics
- Fix fan-in/fan-out always being 0: qualify edge from/to IDs with module name - Add resolve_callee_to_symbol_id for cross-module call resolution - Sort modules index alphabetically in ARCHITECTURE.md - Sort high fan-in/fan-out tables by count descending - Fix duplicate unsorted render methods (render_modules_index_section, render_critical_points_section)
This commit is contained in:
@@ -868,11 +868,20 @@ impl PythonAnalyzer {
|
||||
}
|
||||
|
||||
for parsed_module in parsed_modules {
|
||||
let module_id = self.compute_module_path(&parsed_module.path);
|
||||
for call in &parsed_module.calls {
|
||||
let callee_expr = call.callee_expr.clone();
|
||||
// Qualify from_id with module to match symbol IDs (module::symbol)
|
||||
let from_id = format!("{}::{}", module_id, call.caller_symbol);
|
||||
|
||||
// Try to resolve callee to a qualified symbol ID
|
||||
// If callee_expr is "module.func", try to find it as "resolved_module::func"
|
||||
let to_id = self.resolve_callee_to_symbol_id(
|
||||
&call.callee_expr, &module_id, project_model
|
||||
);
|
||||
|
||||
let edge = crate::model::Edge {
|
||||
from_id: call.caller_symbol.clone(),
|
||||
to_id: callee_expr,
|
||||
from_id,
|
||||
to_id,
|
||||
edge_type: crate::model::EdgeType::SymbolCall,
|
||||
meta: None,
|
||||
};
|
||||
@@ -883,6 +892,34 @@ impl PythonAnalyzer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve a callee expression to a qualified symbol ID.
|
||||
/// E.g., "SomeClass.method" or "func" -> "module::func"
|
||||
fn resolve_callee_to_symbol_id(&self, callee_expr: &str, from_module: &str, model: &ProjectModel) -> String {
|
||||
// First try: exact match as qualified ID in the same module
|
||||
let same_module_id = format!("{}::{}", from_module, callee_expr);
|
||||
if model.symbols.contains_key(&same_module_id) {
|
||||
return same_module_id;
|
||||
}
|
||||
|
||||
// Try: callee might be "func" and exist in another module via imports
|
||||
// Check all symbols for a match on the bare name
|
||||
let parts: Vec<&str> = callee_expr.splitn(2, '.').collect();
|
||||
let bare_name = parts[0];
|
||||
|
||||
// Look through imports of from_module to find resolved target
|
||||
if let Some(module) = model.modules.get(from_module) {
|
||||
for outbound in &module.outbound_modules {
|
||||
let candidate = format!("{}::{}", outbound, bare_name);
|
||||
if model.symbols.contains_key(&candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return qualified with current module
|
||||
same_module_id
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
|
||||
@@ -333,9 +333,11 @@ impl Renderer {
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect module items for template
|
||||
// Collect module items for template (sorted alphabetically)
|
||||
let mut modules_list = Vec::new();
|
||||
for (module_id, module) in &model.modules {
|
||||
let mut sorted_modules: Vec<_> = model.modules.iter().collect();
|
||||
sorted_modules.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
for (module_id, module) in sorted_modules {
|
||||
modules_list.push(serde_json::json!({
|
||||
"name": module_id,
|
||||
"symbol_count": module.symbols.len(),
|
||||
@@ -345,26 +347,29 @@ impl Renderer {
|
||||
}));
|
||||
}
|
||||
|
||||
// Collect critical points
|
||||
let mut high_fan_in = Vec::new();
|
||||
let mut high_fan_out = Vec::new();
|
||||
// Collect critical points as tuples (count, symbol_id, is_critical) for sorting
|
||||
let mut fan_in_tuples: Vec<(usize, &str, bool)> = Vec::new();
|
||||
let mut fan_out_tuples: Vec<(usize, &str, bool)> = Vec::new();
|
||||
for (symbol_id, symbol) in &model.symbols {
|
||||
if symbol.metrics.fan_in > 5 {
|
||||
high_fan_in.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_in,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
fan_in_tuples.push((symbol.metrics.fan_in, symbol_id, symbol.metrics.is_critical));
|
||||
}
|
||||
if symbol.metrics.fan_out > 5 {
|
||||
high_fan_out.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_out,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
fan_out_tuples.push((symbol.metrics.fan_out, symbol_id, symbol.metrics.is_critical));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by count descending
|
||||
fan_in_tuples.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
fan_out_tuples.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
let high_fan_in: Vec<_> = fan_in_tuples.iter().map(|(count, sym, crit)| {
|
||||
serde_json::json!({"symbol": sym, "count": count, "critical": crit})
|
||||
}).collect();
|
||||
let high_fan_out: Vec<_> = fan_out_tuples.iter().map(|(count, sym, crit)| {
|
||||
serde_json::json!({"symbol": sym, "count": count, "critical": crit})
|
||||
}).collect();
|
||||
|
||||
let cycles: Vec<_> = cycle_detector::detect_cycles(model)
|
||||
.iter()
|
||||
.map(|cycle| {
|
||||
@@ -637,10 +642,12 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn render_modules_index_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect module information
|
||||
// Collect module information (sorted alphabetically)
|
||||
let mut modules = Vec::new();
|
||||
let mut sorted_modules: Vec<_> = model.modules.iter().collect();
|
||||
sorted_modules.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
|
||||
for (module_id, module) in &model.modules {
|
||||
for (module_id, module) in sorted_modules {
|
||||
modules.push(serde_json::json!({
|
||||
"name": module_id,
|
||||
"symbol_count": module.symbols.len(),
|
||||
@@ -674,27 +681,29 @@ impl Renderer {
|
||||
}
|
||||
|
||||
pub fn render_critical_points_section(&self, model: &ProjectModel) -> Result<String, anyhow::Error> {
|
||||
// Collect critical points information
|
||||
let mut high_fan_in = Vec::new();
|
||||
let mut high_fan_out = Vec::new();
|
||||
// Collect and sort critical points by count descending
|
||||
let mut fan_in_items: Vec<(usize, &str, bool)> = Vec::new();
|
||||
let mut fan_out_items: Vec<(usize, &str, bool)> = Vec::new();
|
||||
|
||||
for (symbol_id, symbol) in &model.symbols {
|
||||
if symbol.metrics.fan_in > 5 { // Threshold for high fan-in
|
||||
high_fan_in.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_in,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
if symbol.metrics.fan_in > 5 {
|
||||
fan_in_items.push((symbol.metrics.fan_in, symbol_id, symbol.metrics.is_critical));
|
||||
}
|
||||
if symbol.metrics.fan_out > 5 { // Threshold for high fan-out
|
||||
high_fan_out.push(serde_json::json!({
|
||||
"symbol": symbol_id,
|
||||
"count": symbol.metrics.fan_out,
|
||||
"critical": symbol.metrics.is_critical,
|
||||
}));
|
||||
if symbol.metrics.fan_out > 5 {
|
||||
fan_out_items.push((symbol.metrics.fan_out, symbol_id, symbol.metrics.is_critical));
|
||||
}
|
||||
}
|
||||
|
||||
fan_in_items.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
fan_out_items.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
let high_fan_in: Vec<_> = fan_in_items.iter().map(|(count, sym, crit)| {
|
||||
serde_json::json!({"symbol": sym, "count": count, "critical": crit})
|
||||
}).collect();
|
||||
let high_fan_out: Vec<_> = fan_out_items.iter().map(|(count, sym, crit)| {
|
||||
serde_json::json!({"symbol": sym, "count": count, "critical": crit})
|
||||
}).collect();
|
||||
|
||||
// Prepare data for critical points section
|
||||
let data = serde_json::json!({
|
||||
"high_fan_in": high_fan_in,
|
||||
|
||||
Reference in New Issue
Block a user