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 description(&self) -> &str {
24        "Exported ratio too high, narrow the public API"
25    }
26
27    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
28        let total = ctx.model.functions.len() + ctx.model.classes.len();
29        if total < 5 {
30            return vec![];
31        }
32
33        let exported = count_exported(ctx);
34        let ratio = exported as f64 / total as f64;
35
36        if exported > self.max_exported_count || ratio > self.max_exported_ratio {
37            vec![self.make_finding(ctx, exported, total, ratio)]
38        } else {
39            vec![]
40        }
41    }
42}
43
44/// Count total exported functions and classes.
45fn count_exported(ctx: &AnalysisContext) -> usize {
46    let fns = ctx.model.functions.iter().filter(|f| f.is_exported).count();
47    let cls = ctx.model.classes.iter().filter(|c| c.is_exported).count();
48    fns + cls
49}
50
51impl ApiSurfaceAnalyzer {
52    /// Build the large API surface finding.
53    fn make_finding(
54        &self,
55        ctx: &AnalysisContext,
56        exported: usize,
57        total: usize,
58        ratio: f64,
59    ) -> Finding {
60        Finding {
61            smell_name: "large_api_surface".into(),
62            category: SmellCategory::Bloaters,
63            severity: Severity::Warning,
64            location: Location {
65                path: ctx.file.path.clone(),
66                start_line: 1,
67                end_line: ctx.model.total_lines,
68                name: None,
69            },
70            message: format!(
71                "File exports {}/{} items ({:.0}%), consider narrowing the public API",
72                exported,
73                total,
74                ratio * 100.0
75            ),
76            suggested_refactorings: vec!["Hide Method".into(), "Extract Class".into()],
77            actual_value: Some(ratio),
78            threshold: Some(self.max_exported_ratio),
79        }
80    }
81}