cha_core/plugins/
api_surface.rs1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3pub 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
48fn 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 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 }
85 }
86}