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                start_col: f.name_col,
44                end_line: f.start_line,
45                end_col: f.name_end_col,
46            };
47            if let Some(finding) = check_name(&check, self.min_name_length, self.max_name_length) {
48                findings.push(finding);
49            }
50        }
51    }
52
53    fn check_classes(&self, ctx: &AnalysisContext, findings: &mut Vec<Finding>) {
54        for c in &ctx.model.classes {
55            if let Some(f) = check_pascal_case(c, &ctx.file.path) {
56                findings.push(f);
57            }
58            let check = NameCheck {
59                name: &c.name,
60                kind: "Class",
61                path: &ctx.file.path,
62                start_line: c.start_line,
63                start_col: c.name_col,
64                end_line: c.start_line,
65                end_col: c.name_end_col,
66            };
67            if let Some(f) = check_name(&check, self.min_name_length, self.max_name_length) {
68                findings.push(f);
69            }
70        }
71    }
72}
73
74/// Check if a class name violates PascalCase convention.
75fn check_pascal_case(c: &crate::ClassInfo, path: &std::path::Path) -> Option<Finding> {
76    if c.name.is_empty() || c.name.chars().next().is_some_and(|ch| ch.is_uppercase()) {
77        return None;
78    }
79    Some(Finding {
80        smell_name: "naming_convention".into(),
81        category: SmellCategory::Bloaters,
82        severity: Severity::Hint,
83        location: Location {
84            path: path.to_path_buf(),
85            start_line: c.start_line,
86            start_col: c.name_col,
87            end_line: c.start_line,
88            end_col: c.name_end_col,
89            name: Some(c.name.clone()),
90        },
91        message: format!("Class `{}` should use PascalCase", c.name),
92        suggested_refactorings: vec!["Rename Method".into()],
93        ..Default::default()
94    })
95}
96
97struct NameCheck<'a> {
98    name: &'a str,
99    kind: &'a str,
100    path: &'a std::path::Path,
101    start_line: usize,
102    start_col: usize,
103    end_line: usize,
104    end_col: usize,
105}
106
107fn check_name(check: &NameCheck, min_len: usize, max_len: usize) -> Option<Finding> {
108    let (smell, severity, qualifier, limit) = if check.name.len() < min_len {
109        ("naming_too_short", Severity::Warning, "short", min_len)
110    } else if check.name.len() > max_len {
111        ("naming_too_long", Severity::Hint, "long", max_len)
112    } else {
113        return None;
114    };
115    let bound_label = if qualifier == "short" { "min" } else { "max" };
116    Some(Finding {
117        smell_name: smell.into(),
118        category: SmellCategory::Bloaters,
119        severity,
120        location: Location {
121            path: check.path.to_path_buf(),
122            start_line: check.start_line,
123            start_col: check.start_col,
124            end_line: check.end_line,
125            end_col: check.end_col,
126            name: Some(check.name.to_string()),
127        },
128        message: format!(
129            "{} `{}` name is too {} ({} chars, {}: {})",
130            check.kind,
131            check.name,
132            qualifier,
133            check.name.len(),
134            bound_label,
135            limit
136        ),
137        suggested_refactorings: vec!["Rename Method".into()],
138        ..Default::default()
139    })
140}