Skip to main content

cha_core/plugins/
hub_like.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Detect Hub-like modules with excessive fan-out (too many imports).
4///
5/// A module that imports too many other modules acts as a "hub" in the
6/// dependency graph, coupling itself to a large portion of the system.
7///
8/// ## References
9///
10/// [1] F. Arcelli Fontana, I. Pigazzini, R. Roveda, and M. Zanoni,
11///     "Architectural Smells Detected by Tools: a Catalogue Proposal,"
12///     in Proc. 13th European Conf. Software Architecture (ECSA), 2019.
13///     doi: 10.1145/3344948.3344982.
14///
15/// [2] R. C. Martin, "Agile Software Development: Principles, Patterns,
16///     and Practices," Prentice Hall, 2003. ISBN: 978-0135974445.
17///     Chapter 20: Stable Dependencies Principle.
18pub struct HubLikeDependencyAnalyzer {
19    pub max_imports: usize,
20}
21
22impl Default for HubLikeDependencyAnalyzer {
23    fn default() -> Self {
24        Self { max_imports: 20 }
25    }
26}
27
28impl Plugin for HubLikeDependencyAnalyzer {
29    fn name(&self) -> &str {
30        "hub_like_dependency"
31    }
32
33    fn description(&self) -> &str {
34        "Hub-like module with too many imports"
35    }
36
37    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
38        let count = ctx
39            .model
40            .imports
41            .iter()
42            .filter(|i| !i.is_module_decl)
43            .count();
44        if count <= self.max_imports {
45            return vec![];
46        }
47        let first = ctx.model.imports.first().map(|i| i.line).unwrap_or(1);
48        let last = ctx.model.imports.last().map(|i| i.line).unwrap_or(1);
49        vec![Finding {
50            smell_name: "hub_like_dependency".into(),
51            category: SmellCategory::Couplers,
52            severity: Severity::Warning,
53            location: Location {
54                path: ctx.file.path.clone(),
55                start_line: first,
56                end_line: last,
57                name: None,
58            },
59            message: format!(
60                "File has {} imports (threshold: {}), acting as a hub — consider splitting",
61                count, self.max_imports
62            ),
63            suggested_refactorings: vec!["Extract Module".into(), "Facade Pattern".into()],
64            actual_value: Some(count as f64),
65            threshold: Some(self.max_imports as f64),
66        }]
67    }
68}