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