rscheck_cli/rules/srp_heuristic/
mod.rs1use crate::analysis::Workspace;
2use crate::config::SrpHeuristicConfig;
3use crate::emit::Emitter;
4use crate::report::Finding;
5use crate::rules::{Rule, RuleBackend, RuleContext, RuleFamily, RuleInfo};
6use crate::span::Span;
7use syn::spanned::Spanned;
8
9pub struct SrpHeuristicRule;
10
11impl SrpHeuristicRule {
12 pub fn static_info() -> RuleInfo {
13 RuleInfo {
14 id: "shape.responsibility_split",
15 family: RuleFamily::Shape,
16 backend: RuleBackend::Syntax,
17 summary: "Flags impl blocks with many methods as a responsibility-split heuristic.",
18 default_level: SrpHeuristicConfig::default().level,
19 schema: "level, method_count_threshold",
20 config_example: "[rules.\"shape.responsibility_split\"]\nlevel = \"warn\"\nmethod_count_threshold = 25",
21 fixable: false,
22 }
23 }
24}
25
26impl Rule for SrpHeuristicRule {
27 fn info(&self) -> RuleInfo {
28 Self::static_info()
29 }
30
31 fn run(&self, ws: &Workspace, ctx: &RuleContext<'_>, out: &mut dyn Emitter) {
32 for file in &ws.files {
33 let cfg = match ctx
34 .policy
35 .decode_rule::<SrpHeuristicConfig>(Self::static_info().id, Some(&file.path))
36 {
37 Ok(cfg) => cfg,
38 Err(_) => continue,
39 };
40 let Some(ast) = &file.ast else { continue };
41 for item in &ast.items {
42 let syn::Item::Impl(imp) = item else { continue };
43 let methods = imp
44 .items
45 .iter()
46 .filter(|i| matches!(i, syn::ImplItem::Fn(_)))
47 .count();
48 if methods <= cfg.method_count_threshold {
49 continue;
50 }
51 out.emit(Finding {
52 rule_id: Self::static_info().id.to_string(),
53 family: Some(Self::static_info().family),
54 engine: Some(Self::static_info().backend),
55 severity: cfg.level.to_severity(),
56 message: format!(
57 "impl block has {methods} methods; consider splitting responsibilities"
58 ),
59 primary: Some(Span::from_pm_span(&file.path, imp.span())),
60 secondary: Vec::new(),
61 help: Some(
62 "This is a heuristic; verify SRP boundaries with domain context."
63 .to_string(),
64 ),
65 evidence: None,
66 confidence: None,
67 tags: vec!["design".to_string()],
68 labels: Vec::new(),
69 notes: Vec::new(),
70 fixes: Vec::new(),
71 });
72 }
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests;