Skip to main content

rscheck_cli/rules/srp_heuristic/
mod.rs

1use 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;