Skip to main content

rigsql_rules/capitalisation/
cp05.rs

1use rigsql_core::SegmentType;
2
3use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
4use crate::utils::check_capitalisation;
5use crate::violation::LintViolation;
6
7/// CP05: Data type names must be consistently capitalised.
8///
9/// By default expects upper case (INT, VARCHAR, etc.).
10#[derive(Debug)]
11pub struct RuleCP05 {
12    pub policy: DataTypeCapPolicy,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DataTypeCapPolicy {
17    Upper,
18    Lower,
19}
20
21impl Default for RuleCP05 {
22    fn default() -> Self {
23        Self {
24            policy: DataTypeCapPolicy::Upper,
25        }
26    }
27}
28
29impl Rule for RuleCP05 {
30    fn code(&self) -> &'static str {
31        "CP05"
32    }
33    fn name(&self) -> &'static str {
34        "capitalisation.types"
35    }
36    fn description(&self) -> &'static str {
37        "Data type names must be consistently capitalised."
38    }
39    fn explanation(&self) -> &'static str {
40        "Data type names (INT, VARCHAR, TEXT, etc.) should use consistent capitalisation. \
41         Most style guides recommend upper case for data types to distinguish them from column names."
42    }
43    fn groups(&self) -> &[RuleGroup] {
44        &[RuleGroup::Capitalisation]
45    }
46    fn is_fixable(&self) -> bool {
47        true
48    }
49
50    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
51        if let Some(val) = settings.get("capitalisation_policy") {
52            self.policy = match val.as_str() {
53                "lower" => DataTypeCapPolicy::Lower,
54                _ => DataTypeCapPolicy::Upper,
55            };
56        }
57    }
58
59    fn crawl_type(&self) -> CrawlType {
60        CrawlType::Segment(vec![SegmentType::DataType])
61    }
62
63    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
64        // DataType node may contain keyword tokens (INT, VARCHAR, etc.)
65        let tokens = ctx.segment.tokens();
66        let mut violations = Vec::new();
67
68        for token in tokens {
69            let text = token.text.as_str();
70            // Skip numeric parts like VARCHAR(255)
71            if text
72                .chars()
73                .all(|c| c.is_ascii_digit() || c == '(' || c == ')' || c == ',')
74            {
75                continue;
76            }
77
78            let (expected, policy_name) = match self.policy {
79                DataTypeCapPolicy::Upper => (text.to_ascii_uppercase(), "upper"),
80                DataTypeCapPolicy::Lower => (text.to_ascii_lowercase(), "lower"),
81            };
82
83            if let Some(v) = check_capitalisation(
84                self.code(),
85                "Data type",
86                text,
87                &expected,
88                policy_name,
89                token.span,
90            ) {
91                violations.push(v);
92            }
93        }
94
95        violations
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102    use crate::test_utils::lint_sql;
103
104    #[test]
105    fn test_cp05_flags_lowercase_type() {
106        let violations = lint_sql("SELECT CAST(1 AS int)", RuleCP05::default());
107        assert_eq!(violations.len(), 1);
108    }
109
110    #[test]
111    fn test_cp05_accepts_uppercase_type() {
112        let violations = lint_sql("SELECT CAST(1 AS INT)", RuleCP05::default());
113        assert_eq!(violations.len(), 0);
114    }
115
116    #[test]
117    fn test_cp05_lower_policy() {
118        let rule = RuleCP05 {
119            policy: DataTypeCapPolicy::Lower,
120        };
121        let violations = lint_sql("SELECT CAST(1 AS INT)", rule);
122        assert_eq!(violations.len(), 1);
123    }
124}