Skip to main content

rigsql_rules/capitalisation/
cp05.rs

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