Skip to main content

cha_core/plugins/
naming.rs

1use crate::{AnalysisContext, Finding, Location, Plugin, Severity, SmellCategory};
2
3/// Check naming conventions for functions and classes.
4pub struct NamingAnalyzer {
5    pub min_name_length: usize,
6    pub max_name_length: usize,
7}
8
9impl Default for NamingAnalyzer {
10    fn default() -> Self {
11        Self {
12            min_name_length: 2,
13            max_name_length: 50,
14        }
15    }
16}
17
18impl Plugin for NamingAnalyzer {
19    fn name(&self) -> &str {
20        "naming"
21    }
22
23    fn description(&self) -> &str {
24        "Naming convention violations"
25    }
26
27    fn analyze(&self, ctx: &AnalysisContext) -> Vec<Finding> {
28        let mut findings = Vec::new();
29        self.check_functions(ctx, &mut findings);
30        self.check_classes(ctx, &mut findings);
31        findings
32    }
33}
34
35impl NamingAnalyzer {
36    fn check_functions(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
37        for f in &ctx.model.functions {
38            let check = NameCheck {
39                name: &f.name,
40                kind: "Function",
41                path: &ctx.file.path,
42                start_line: f.start_line,
43                end_line: f.end_line,
44            };
45            if let Some(finding) = check_name(&check, self.min_name_length, self.max_name_length) {
46                findings.push(finding);
47            }
48        }
49    }
50
51    fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
52        for c in &ctx.model.classes {
53            if let Some(f) = check_pascal_case(c, &ctx.file.path) {
54                findings.push(f);
55            }
56            let check = NameCheck {
57                name: &c.name,
58                kind: "Class",
59                path: &ctx.file.path,
60                start_line: c.start_line,
61                end_line: c.end_line,
62            };
63            if let Some(f) = check_name(&check, self.min_name_length, self.max_name_length) {
64                findings.push(f);
65            }
66        }
67    }
68}
69
70/// Check if a class name violates PascalCase convention.
71fn check_pascal_case(c: &crate::ClassInfo, path: &std::path::Path) -> Option<Finding> {
72    if c.name.is_empty() || c.name.chars().next().is_some_and(|ch| ch.is_uppercase()) {
73        return None;
74    }
75    Some(Finding {
76        smell_name: "naming_convention".into(),
77        category: SmellCategory::Bloaters,
78        severity: Severity::Hint,
79        location: Location {
80            path: path.to_path_buf(),
81            start_line: c.start_line,
82            end_line: c.end_line,
83            name: Some(c.name.clone()),
84        },
85        message: format!("Class `{}` should use PascalCase", c.name),
86        suggested_refactorings: vec!["Rename Method".into()],
87        ..Default::default()
88    })
89}
90
91struct NameCheck<'a> {
92    name: &'a str,
93    kind: &'a str,
94    path: &'a std::path::Path,
95    start_line: usize,
96    end_line: usize,
97}
98
99fn check_name(check: &NameCheck, min_len: usize, max_len: usize) -> Option<Finding> {
100    let (smell, severity, qualifier, limit) = if check.name.len() < min_len {
101        ("naming_too_short", Severity::Warning, "short", min_len)
102    } else if check.name.len() > max_len {
103        ("naming_too_long", Severity::Hint, "long", max_len)
104    } else {
105        return None;
106    };
107    let bound_label = if qualifier == "short" { "min" } else { "max" };
108    Some(Finding {
109        smell_name: smell.into(),
110        category: SmellCategory::Bloaters,
111        severity,
112        location: Location {
113            path: check.path.to_path_buf(),
114            start_line: check.start_line,
115            end_line: check.end_line,
116            name: Some(check.name.to_string()),
117        },
118        message: format!(
119            "{} `{}` name is too {} ({} chars, {}: {})",
120            check.kind,
121            check.name,
122            qualifier,
123            check.name.len(),
124            bound_label,
125            limit
126        ),
127        suggested_refactorings: vec!["Rename Method".into()],
128        ..Default::default()
129    })
130}