Skip to main content

cha_core/plugins/
api_surface.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Analyze the ratio of exported (public) API surface.
4pub struct ApiSurfaceAnalyzer {
5    pub max_exported_ratio: f64,
6    pub max_exported_count: usize,
7}
8
9impl Default for ApiSurfaceAnalyzer {
10    fn default() -> Self {
11        Self {
12            max_exported_ratio: 0.8,
13            max_exported_count: 20,
14        }
15    }
16}
17
18impl Plugin for ApiSurfaceAnalyzer {
19    fn name(&self) -> &str {
20        "api_surface"
21    }
22
23    fn smells(&self) -> Vec<String> {
24        vec!["large_api_surface".into()]
25    }
26
27    fn description(&self) -> &str {
28        "Exported ratio too high, narrow the public API"
29    }
30
31    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
32        let total = ctx.model.functions.len() + ctx.model.classes.len();
33        if total < 5 {
34            return vec![];
35        }
36
37        let exported = count_exported(ctx);
38        let ratio = exported as f64 / total as f64;
39
40        if exported > self.max_exported_count || ratio > self.max_exported_ratio {
41            vec![self.make_finding(ctx, exported, total, ratio)]
42        } else {
43            vec![]
44        }
45    }
46}
47
48/// Count total exported functions and classes.
49fn count_exported(ctx: &AnalysisContext) -> usize {
50    let fns = ctx.model.functions.iter().filter(|f| f.is_exported).count();
51    let cls = ctx.model.classes.iter().filter(|c| c.is_exported).count();
52    fns + cls
53}
54
55impl ApiSurfaceAnalyzer {
56    /// Build the large API surface finding.
57    fn make_finding(
58        &self,
59        ctx: &AnalysisContext,
60        exported: usize,
61        total: usize,
62        ratio: f64,
63    ) -> Finding {
64        Finding {
65            smell_name: "large_api_surface".into(),
66            category: SmellCategory::Bloaters,
67            severity: Severity::Warning,
68            location: Location {
69                path: ctx.file.path.clone(),
70                start_line: 1,
71                end_line: 1,
72                name: None,
73                ..Default::default()
74            },
75            message: format!(
76                "File exports {}/{} items ({:.0}%), consider narrowing the public API",
77                exported,
78                total,
79                ratio * 100.0
80            ),
81            suggested_refactorings: vec!["Hide Method".into(), "Extract Class".into()],
82            actual_value: Some(ratio),
83            threshold: Some(self.max_exported_ratio),
84            risk_score: None,
85        }
86    }
87}