#![allow(dead_code)]
use crate::graph::{EdgeKind, NodeKind};
use crate::graph::GraphQueryExt;
use std::collections::{HashMap, HashSet};
use tracing::{debug, info};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ClassRole {
FrameworkCore,
Facade,
Orchestrator,
EntryPoint,
Utility,
DataClass,
Application,
}
impl ClassRole {
pub fn allows_large_size(&self) -> bool {
matches!(
self,
ClassRole::FrameworkCore
| ClassRole::Facade
| ClassRole::Orchestrator
| ClassRole::EntryPoint
)
}
pub fn severity_multiplier(&self) -> f64 {
match self {
ClassRole::FrameworkCore => 0.0, ClassRole::Facade => 0.3, ClassRole::Orchestrator => 0.3, ClassRole::EntryPoint => 0.5, ClassRole::Utility => 0.7, ClassRole::DataClass => 0.6, ClassRole::Application => 1.0, }
}
}
const FRAMEWORK_CORE_NAMES: &[&str] = &[
"Flask",
"Sanic",
"FastAPI",
"Django",
"Bottle",
"Tornado",
"Application",
"App",
"Blueprint",
"Scaffold",
"Express",
"Koa",
"Hapi",
"Fastify",
"NestFactory",
"SpringApplication",
"Application",
"Gin",
"Echo",
"Fiber",
"Mux",
"Server",
"Router",
"Server",
"Gateway",
"Proxy",
];
const FRAMEWORK_PATTERNS: &[&str] = &["Application", "Framework", "Server", "Gateway", "Router"];
const FRAMEWORK_CORE_SUFFIXES: &[&str] = &[
"SchemaEditor", "Autodetector", "Compiler", "Admin",
"Manager", "Registry", "Dispatcher",
];
const ORCHESTRATOR_NAME_PATTERNS: &[&str] = &[
"Controller",
"Router",
"Handler",
"Dispatcher",
"Orchestrator",
"Coordinator",
"Mediator",
"Presenter",
"Endpoint",
"Resource", "ViewSet", "Viewset",
"View", "Resolver", "Middleware",
];
const ORCHESTRATOR_PATH_PATTERNS: &[&str] = &[
"/controllers/",
"/controller/",
"/routers/",
"/router/",
"/handlers/",
"/handler/",
"/dispatchers/",
"/endpoints/",
"/resources/",
"/views/",
"/viewsets/",
"/resolvers/",
"/middleware/",
"/routes/",
];
#[derive(Debug, Clone)]
pub struct ClassContext {
pub qualified_name: String,
pub name: String,
pub file_path: String,
pub method_count: usize,
pub loc: usize,
pub complexity: usize,
pub avg_method_complexity: f64,
pub delegating_methods: usize,
pub delegation_ratio: f64,
pub public_methods: usize,
pub external_dependencies: usize,
pub usages: usize,
pub role: ClassRole,
pub is_test: bool,
pub is_framework_path: bool,
pub role_reason: String,
}
impl ClassContext {
pub fn skip_god_class(&self) -> bool {
self.role == ClassRole::FrameworkCore || self.is_framework_path
}
pub fn adjusted_thresholds(&self, base_methods: usize, base_loc: usize) -> (usize, usize) {
match self.role {
ClassRole::FrameworkCore => (usize::MAX, usize::MAX),
ClassRole::Facade => (base_methods * 3, base_loc * 3),
ClassRole::Orchestrator => (base_methods * 3, base_loc * 3), ClassRole::EntryPoint => (base_methods * 2, base_loc * 2),
ClassRole::Utility => (
(base_methods as f64 * 1.5) as usize,
(base_loc as f64 * 1.5) as usize,
),
ClassRole::DataClass => (base_methods * 2, base_loc * 2), ClassRole::Application => (base_methods, base_loc),
}
}
}
pub type ClassContextMap = HashMap<String, ClassContext>;
pub struct ClassContextBuilder<'a> {
graph: &'a dyn crate::graph::GraphQuery,
thin_wrapper_complexity: f64,
facade_delegation_ratio: f64,
}
impl<'a> ClassContextBuilder<'a> {
pub fn new(graph: &'a dyn crate::graph::GraphQuery) -> Self {
let _i = graph.interner();
Self {
graph,
thin_wrapper_complexity: 3.0, facade_delegation_ratio: 0.6, }
}
pub fn build(&self) -> ClassContextMap {
let i = self.graph.interner();
let start = std::time::Instant::now();
let class_idxs = self.graph.classes_idx();
if class_idxs.is_empty() {
let classes = self.graph.get_classes_shared();
if classes.is_empty() {
return HashMap::new();
}
return self.build_legacy(i, &classes);
}
let class_count = class_idxs.len();
info!("Building class context for {} classes", class_count);
let call_edges = self.graph.all_call_edges();
let call_map: HashMap<&str, HashSet<&str>> = {
let mut map: HashMap<&str, HashSet<&str>> = HashMap::new();
for &(caller_idx, callee_idx) in call_edges {
if let (Some(caller), Some(callee)) = (self.graph.node_idx(caller_idx), self.graph.node_idx(callee_idx)) {
map.entry(caller.qn(i))
.or_default()
.insert(callee.qn(i));
}
}
map
};
let class_methods: HashMap<&str, Vec<crate::graph::store_models::CodeNode>> = {
let mut map: HashMap<&str, Vec<crate::graph::store_models::CodeNode>> = HashMap::new();
let mut classes_by_file: HashMap<&str, Vec<&crate::graph::store_models::CodeNode>> =
HashMap::new();
for &class_idx in class_idxs {
if let Some(class) = self.graph.node_idx(class_idx) {
classes_by_file
.entry(class.path(i))
.or_default()
.push(class);
}
}
for (file_path, file_classes) in &classes_by_file {
let file_func_idxs = self.graph.functions_in_file_idx(file_path);
for class in file_classes {
let methods: Vec<_> = file_func_idxs
.iter()
.filter_map(|&idx| self.graph.node_idx(idx))
.filter(|f| {
f.line_start >= class.line_start && f.line_end <= class.line_end
})
.copied()
.collect();
if !methods.is_empty() {
map.insert(class.qn(i), methods);
}
}
}
map
};
let class_usages: HashMap<&str, usize> = {
let mut method_to_class: HashMap<&str, &str> = HashMap::new();
for (class_qn, methods) in &class_methods {
for method in methods {
method_to_class.insert(method.qn(i), class_qn);
}
}
let mut class_pair_seen: HashSet<(&str, &str)> = HashSet::new();
let mut usages: HashMap<&str, usize> = HashMap::new();
for &(caller_idx, callee_idx) in call_edges {
if let (Some(caller), Some(callee)) = (self.graph.node_idx(caller_idx), self.graph.node_idx(callee_idx)) {
let caller_class = method_to_class.get(caller.qn(i));
let callee_class = method_to_class.get(callee.qn(i));
if let (Some(&from_class), Some(&to_class)) = (caller_class, callee_class) {
if from_class != to_class && class_pair_seen.insert((from_class, to_class)) {
*usages.entry(to_class).or_insert(0) += 1;
}
}
}
}
usages
};
let mut contexts = ClassContextMap::new();
for &class_idx in class_idxs {
let Some(class) = self.graph.node_idx(class_idx) else { continue };
let qn = class.qn(i);
let methods = class_methods.get(qn).cloned().unwrap_or_default();
let method_count = class
.get_i64("methodCount")
.map(|n| n as usize)
.unwrap_or_else(|| methods.len());
let total_complexity: i64 = methods.iter().filter_map(|m| m.complexity_opt()).sum();
let avg_complexity = if method_count > 0 {
total_complexity as f64 / method_count as f64
} else {
0.0
};
let mut delegating_count = 0;
let mut external_deps: HashSet<String> = HashSet::new();
for method in &methods {
if let Some(callees) = call_map.get(method.qn(i)) {
let external_calls: Vec<_> = callees
.iter()
.filter(|c| !methods.iter().any(|m| m.qn(i) == **c))
.collect();
if !external_calls.is_empty() {
delegating_count += 1;
for ext in external_calls {
if let Some(module) = ext.rsplit("::").nth(1) {
external_deps.insert(module.to_string());
}
}
}
}
}
let delegation_ratio = if method_count > 0 {
delegating_count as f64 / method_count as f64
} else {
0.0
};
let public_methods = methods.iter().filter(|m| !m.node_name(i).starts_with('_')).count();
let usages = *class_usages.get(qn).unwrap_or(&0);
let is_test = self.is_test_path(class.path(i));
let is_framework_path = self.is_framework_path(class.path(i));
let (role, role_reason) = self.infer_role(
class.node_name(i),
class.path(i),
method_count,
avg_complexity,
delegation_ratio,
external_deps.len(),
usages,
is_test,
is_framework_path,
);
contexts.insert(
qn.to_string(),
ClassContext {
qualified_name: qn.to_string(),
name: class.node_name(i).to_string(),
file_path: class.path(i).to_string(),
method_count,
loc: class.loc() as usize,
complexity: total_complexity as usize,
avg_method_complexity: avg_complexity,
delegating_methods: delegating_count,
delegation_ratio,
public_methods,
external_dependencies: external_deps.len(),
usages,
role,
is_test,
is_framework_path,
role_reason,
},
);
}
let elapsed = start.elapsed();
info!("Built class context in {:?}", elapsed);
let mut role_counts: HashMap<ClassRole, usize> = HashMap::new();
for ctx in contexts.values() {
*role_counts.entry(ctx.role).or_insert(0) += 1;
}
debug!("Class role distribution: {:?}", role_counts);
contexts
}
fn build_legacy(
&self,
i: &crate::graph::interner::StringInterner,
classes: &std::sync::Arc<[crate::graph::store_models::CodeNode]>,
) -> ClassContextMap {
let start = std::time::Instant::now();
info!("Building class context for {} classes (legacy)", classes.len());
let calls = self.graph.get_calls_shared();
let call_map: HashMap<&str, HashSet<&str>> = {
let mut map: HashMap<&str, HashSet<&str>> = HashMap::new();
for (caller, callee) in calls.iter() {
map.entry(i.resolve(*caller))
.or_default()
.insert(i.resolve(*callee));
}
map
};
let class_methods: HashMap<&str, Vec<crate::graph::store_models::CodeNode>> = {
let mut map: HashMap<&str, Vec<crate::graph::store_models::CodeNode>> = HashMap::new();
let mut classes_by_file: HashMap<&str, Vec<&crate::graph::store_models::CodeNode>> =
HashMap::new();
for class in classes.iter() {
classes_by_file
.entry(class.path(i))
.or_default()
.push(class);
}
for (file_path, file_classes) in &classes_by_file {
let file_funcs = self.graph.get_functions_in_file(file_path);
for class in file_classes {
let methods: Vec<_> = file_funcs
.iter()
.filter(|f| f.line_start >= class.line_start && f.line_end <= class.line_end)
.cloned()
.collect();
if !methods.is_empty() {
map.insert(class.qn(i), methods);
}
}
}
map
};
let class_usages: HashMap<&str, usize> = {
let mut method_to_class: HashMap<&str, &str> = HashMap::new();
for (class_qn, methods) in &class_methods {
for method in methods {
method_to_class.insert(method.qn(i), class_qn);
}
}
let mut class_pair_seen: HashSet<(&str, &str)> = HashSet::new();
let mut usages: HashMap<&str, usize> = HashMap::new();
for (caller, callee) in calls.iter() {
let caller_class = method_to_class.get(i.resolve(*caller));
let callee_class = method_to_class.get(i.resolve(*callee));
if let (Some(&from_class), Some(&to_class)) = (caller_class, callee_class) {
if from_class != to_class && class_pair_seen.insert((from_class, to_class)) {
*usages.entry(to_class).or_insert(0) += 1;
}
}
}
usages
};
let mut contexts = ClassContextMap::new();
for class in classes.iter() {
let qn = class.qn(i);
let methods = class_methods.get(qn).cloned().unwrap_or_default();
let method_count = class.get_i64("methodCount").map(|n| n as usize).unwrap_or(methods.len());
let total_complexity: i64 = methods.iter().filter_map(|m| m.complexity_opt()).sum();
let avg_complexity = if method_count > 0 { total_complexity as f64 / method_count as f64 } else { 0.0 };
let mut delegating_count = 0;
let mut external_deps: HashSet<String> = HashSet::new();
for method in &methods {
if let Some(callees) = call_map.get(method.qn(i)) {
let ext: Vec<_> = callees.iter().filter(|c| !methods.iter().any(|m| m.qn(i) == **c)).collect();
if !ext.is_empty() {
delegating_count += 1;
for e in ext { if let Some(m) = e.rsplit("::").nth(1) { external_deps.insert(m.to_string()); } }
}
}
}
let delegation_ratio = if method_count > 0 { delegating_count as f64 / method_count as f64 } else { 0.0 };
let public_methods = methods.iter().filter(|m| !m.node_name(i).starts_with('_')).count();
let usages = *class_usages.get(qn).unwrap_or(&0);
let is_test = self.is_test_path(class.path(i));
let is_framework_path = self.is_framework_path(class.path(i));
let (role, role_reason) = self.infer_role(
class.node_name(i), class.path(i), method_count, avg_complexity,
delegation_ratio, external_deps.len(), usages, is_test, is_framework_path,
);
contexts.insert(qn.to_string(), ClassContext {
qualified_name: qn.to_string(),
name: class.node_name(i).to_string(),
file_path: class.path(i).to_string(),
method_count,
loc: class.loc() as usize,
complexity: total_complexity as usize,
avg_method_complexity: avg_complexity,
delegating_methods: delegating_count,
delegation_ratio,
public_methods,
external_dependencies: external_deps.len(),
usages,
role,
is_test,
is_framework_path,
role_reason,
});
}
let elapsed = start.elapsed();
info!("Built class context (legacy) in {:?}", elapsed);
contexts
}
fn infer_role(
&self,
name: &str,
file_path: &str,
method_count: usize,
avg_complexity: f64,
delegation_ratio: f64,
external_dependencies: usize,
usages: usize,
_is_test: bool,
is_framework_path: bool,
) -> (ClassRole, String) {
if FRAMEWORK_CORE_NAMES.contains(&name) {
return (
ClassRole::FrameworkCore,
format!("Known framework class: {}", name),
);
}
if FRAMEWORK_PATTERNS.iter().any(|p| name.contains(p)) {
return (
ClassRole::FrameworkCore,
format!("Framework pattern in name: {}", name),
);
}
if is_framework_path {
return (
ClassRole::FrameworkCore,
"In framework/vendor path".to_string(),
);
}
if let Some(pattern) = ORCHESTRATOR_NAME_PATTERNS
.iter()
.find(|p| name.contains(**p))
{
return (
ClassRole::Orchestrator,
format!(
"Orchestrator pattern '{}' in name: {} ({} methods, {:.0}% delegate, {} external deps)",
pattern, name, method_count, delegation_ratio * 100.0, external_dependencies
),
);
}
if FRAMEWORK_CORE_SUFFIXES.iter().any(|s| name.ends_with(s)) {
return (
ClassRole::FrameworkCore,
"Name ends with framework suffix".to_string(),
);
}
let path_lower = file_path.to_lowercase();
if let Some(pattern) = ORCHESTRATOR_PATH_PATTERNS
.iter()
.find(|p| path_lower.contains(**p))
{
return (
ClassRole::Orchestrator,
format!(
"In orchestrator path '{}': {} ({} methods, {:.0}% delegate)",
pattern, name, method_count, delegation_ratio * 100.0
),
);
}
if method_count >= 5
&& delegation_ratio >= 0.6
&& external_dependencies >= 4
&& avg_complexity <= self.thin_wrapper_complexity
{
return (
ClassRole::Orchestrator,
format!(
"Orchestrator pattern (metrics): {} methods, avg complexity {:.1}, {:.0}% delegate, {} external deps",
method_count, avg_complexity, delegation_ratio * 100.0, external_dependencies
),
);
}
if method_count >= 10
&& avg_complexity <= self.thin_wrapper_complexity
&& delegation_ratio >= self.facade_delegation_ratio
{
return (
ClassRole::Facade,
format!(
"Facade pattern: {} methods, avg complexity {:.1}, {:.0}% delegate",
method_count,
avg_complexity,
delegation_ratio * 100.0
),
);
}
if usages >= 5 && method_count >= 10 {
return (
ClassRole::EntryPoint,
format!("Entry point: used by {} other classes", usages),
);
}
if avg_complexity <= 1.5 && method_count <= 20 {
return (
ClassRole::DataClass,
format!("Data class: avg complexity {:.1}", avg_complexity),
);
}
if method_count <= 15 && usages >= 3 {
return (
ClassRole::Utility,
format!(
"Utility class: {} methods, used by {} others",
method_count, usages
),
);
}
(
ClassRole::Application,
"Standard application class".to_string(),
)
}
fn is_test_path(&self, path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/test/")
|| lower.contains("/tests/")
|| lower.contains("/__tests__/")
|| lower.contains("/spec/")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.py")
|| lower.ends_with(".test.ts")
|| lower.ends_with(".test.js")
|| lower.ends_with(".spec.ts")
|| lower.ends_with(".spec.js")
|| lower.starts_with("tests/")
|| lower.starts_with("test/")
|| lower.starts_with("__tests__/")
|| lower.starts_with("spec/")
}
fn is_framework_path(&self, path: &str) -> bool {
let lower = path.to_lowercase();
lower.contains("/node_modules/")
|| lower.contains("/site-packages/")
|| lower.contains("/vendor/")
|| lower.contains("/.venv/")
|| lower.contains("/venv/")
|| lower.contains("/dist-packages/")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_framework_core_detection() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, _) = builder.infer_role("Flask", "src/app.py", 50, 5.0, 0.8, 3, 10, false, false);
assert_eq!(role, ClassRole::FrameworkCore);
let (role, _) = builder.infer_role("MyApplication", "src/app.py", 30, 3.0, 0.5, 2, 5, false, false);
assert_eq!(role, ClassRole::FrameworkCore);
}
#[test]
fn test_facade_detection() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, _) = builder.infer_role("ApiClient", "src/client.py", 20, 2.0, 0.7, 3, 2, false, false);
assert_eq!(role, ClassRole::Facade);
}
#[test]
fn test_data_class_detection() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, _) = builder.infer_role("UserDTO", "src/models.py", 10, 1.0, 0.1, 0, 2, false, false);
assert_eq!(role, ClassRole::DataClass);
}
#[test]
fn test_orchestrator_detection_by_name() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, reason) = builder.infer_role("UserController", "src/api.py", 15, 2.0, 0.8, 5, 3, false, false);
assert_eq!(role, ClassRole::Orchestrator, "Controller should be Orchestrator: {}", reason);
let (role, _) = builder.infer_role("RequestHandler", "src/server.py", 10, 1.5, 0.7, 3, 2, false, false);
assert_eq!(role, ClassRole::Orchestrator);
let (role, _) = builder.infer_role("EventDispatcher", "src/events.py", 8, 2.0, 0.6, 4, 1, false, false);
assert_eq!(role, ClassRole::Orchestrator);
let (role, _) = builder.infer_role("WorkflowOrchestrator", "src/workflows.py", 12, 1.0, 0.9, 6, 2, false, false);
assert_eq!(role, ClassRole::Orchestrator);
}
#[test]
fn test_orchestrator_detection_by_path() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, _) = builder.infer_role("Users", "src/controllers/users.py", 10, 2.0, 0.5, 3, 2, false, false);
assert_eq!(role, ClassRole::Orchestrator);
let (role, _) = builder.infer_role("Auth", "src/handlers/auth.ts", 8, 1.5, 0.4, 2, 1, false, false);
assert_eq!(role, ClassRole::Orchestrator);
}
#[test]
fn test_orchestrator_detection_by_metrics() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, reason) = builder.infer_role(
"OrderService", "src/services/orders.py",
8, 2.0, 0.7, 5, 2, false, false,
);
assert_eq!(role, ClassRole::Orchestrator, "Metric-based orchestrator: {}", reason);
}
#[test]
fn test_orchestrator_not_triggered_for_low_delegation() {
let store = crate::graph::GraphBuilder::new().freeze();
let builder = ClassContextBuilder::new(&store);
let (role, _) = builder.infer_role(
"OrderService", "src/services/orders.py",
8, 1.0, 0.2, 1, 2, false, false,
);
assert_ne!(role, ClassRole::Orchestrator);
}
#[test]
fn test_orchestrator_severity_multiplier() {
assert_eq!(ClassRole::Orchestrator.severity_multiplier(), 0.3);
}
#[test]
fn test_orchestrator_allows_large_size() {
assert!(ClassRole::Orchestrator.allows_large_size());
}
#[test]
fn test_role_severity_multipliers() {
assert_eq!(ClassRole::FrameworkCore.severity_multiplier(), 0.0);
assert_eq!(ClassRole::Facade.severity_multiplier(), 0.3);
assert_eq!(ClassRole::Orchestrator.severity_multiplier(), 0.3);
assert_eq!(ClassRole::Application.severity_multiplier(), 1.0);
}
}