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: 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}