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 smells(&self) -> Vec<String> {
34        vec!["hub_like_dependency".into()]
35    }
36
37    fn description(&self) -> &str {
38        "Hub-like module with too many imports"
39    }
40
41    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
42        let count = ctx
43            .model
44            .imports
45            .iter()
46            .filter(|i| !i.is_module_decl)
47            .count();
48        if count <= self.max_imports {
49            return vec![];
50        }
51        let first = ctx.model.imports.first().map(|i| i.line).unwrap_or(1);
52        let first_col = ctx.model.imports.first().map(|i| i.col).unwrap_or(0);
53        let last = ctx.model.imports.last().map(|i| i.line).unwrap_or(1);
54        vec![Finding {
55            smell_name: "hub_like_dependency".into(),
56            category: SmellCategory::Couplers,
57            severity: Severity::Warning,
58            location: Location {
59                path: ctx.file.path.clone(),
60                start_line: first,
61                start_col: first_col,
62                end_line: last,
63                name: None,
64                ..Default::default()
65            },
66            message: format!(
67                "File has {} imports (threshold: {}), acting as a hub — consider splitting",
68                count, self.max_imports
69            ),
70            suggested_refactorings: vec!["Extract Module".into(), "Facade Pattern".into()],
71            actual_value: Some(count as f64),
72            threshold: Some(self.max_imports as f64),
73        }]
74    }
75}