diff --git a/wtismycode-core/src/python_analyzer.rs b/wtismycode-core/src/python_analyzer.rs index 09c1236..f442b54 100644 --- a/wtismycode-core/src/python_analyzer.rs +++ b/wtismycode-core/src/python_analyzer.rs @@ -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 { diff --git a/wtismycode-core/src/renderer.rs b/wtismycode-core/src/renderer.rs index f2a9837..c747c2c 100644 --- a/wtismycode-core/src/renderer.rs +++ b/wtismycode-core/src/renderer.rs @@ -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 { - // 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 { - // 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,