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 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
44fn 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 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: 1,
68 name: None,
69 ..Default::default()
70 },
71 message: format!(
72 "File exports {}/{} items ({:.0}%), consider narrowing the public API",
73 exported,
74 total,
75 ratio * 100.0
76 ),
77 suggested_refactorings: vec!["Hide Method".into(), "Extract Class".into()],
78 actual_value: Some(ratio),
79 threshold: Some(self.max_exported_ratio),
80 }
81 }
82}