Skip to main content

rigsql_rules/capitalisation/
cp01.rs

1use rigsql_core::{Segment, SegmentType, TokenKind};
2use rigsql_lexer::is_keyword;
3
4use crate::rule::{CrawlType, Rule, RuleContext, RuleGroup};
5use crate::violation::{LintViolation, SourceEdit};
6
7/// CP01: Keywords must be consistently capitalised.
8///
9/// By default, expects UPPER case keywords.
10#[derive(Debug)]
11pub struct RuleCP01 {
12    pub policy: CapitalisationPolicy,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum CapitalisationPolicy {
17    Upper,
18    Lower,
19    Capitalise,
20}
21
22impl Default for RuleCP01 {
23    fn default() -> Self {
24        Self {
25            policy: CapitalisationPolicy::Upper,
26        }
27    }
28}
29
30impl Rule for RuleCP01 {
31    fn code(&self) -> &'static str {
32        "CP01"
33    }
34    fn name(&self) -> &'static str {
35        "capitalisation.keywords"
36    }
37    fn description(&self) -> &'static str {
38        "Keywords must be consistently capitalised."
39    }
40    fn explanation(&self) -> &'static str {
41        "SQL keywords like SELECT, FROM, WHERE should use consistent capitalisation. \
42         Mixed case reduces readability. Most style guides recommend UPPER case keywords \
43         to distinguish them from identifiers."
44    }
45    fn groups(&self) -> &[RuleGroup] {
46        &[RuleGroup::Capitalisation]
47    }
48    fn is_fixable(&self) -> bool {
49        true
50    }
51
52    fn crawl_type(&self) -> CrawlType {
53        CrawlType::Segment(vec![SegmentType::Keyword, SegmentType::Unparsable])
54    }
55
56    fn configure(&mut self, settings: &std::collections::HashMap<String, String>) {
57        if let Some(policy) = settings.get("capitalisation_policy") {
58            self.policy = match policy.as_str() {
59                "lower" => CapitalisationPolicy::Lower,
60                "capitalise" | "capitalize" => CapitalisationPolicy::Capitalise,
61                _ => CapitalisationPolicy::Upper,
62            };
63        }
64    }
65
66    fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
67        let Segment::Token(t) = ctx.segment else {
68            return vec![];
69        };
70        if t.token.kind != TokenKind::Word {
71            return vec![];
72        }
73        if !is_keyword(&t.token.text) {
74            return vec![];
75        }
76
77        let text = t.token.text.as_str();
78        let expected = match self.policy {
79            CapitalisationPolicy::Upper => text.to_ascii_uppercase(),
80            CapitalisationPolicy::Lower => text.to_ascii_lowercase(),
81            CapitalisationPolicy::Capitalise => capitalise(text),
82        };
83
84        if text != expected {
85            vec![LintViolation::with_fix(
86                self.code(),
87                format!(
88                    "Keywords must be {} case. Found '{}' instead of '{}'.",
89                    match self.policy {
90                        CapitalisationPolicy::Upper => "upper",
91                        CapitalisationPolicy::Lower => "lower",
92                        CapitalisationPolicy::Capitalise => "capitalised",
93                    },
94                    text,
95                    expected
96                ),
97                t.token.span,
98                vec![SourceEdit::replace(t.token.span, expected.clone())],
99            )]
100        } else {
101            vec![]
102        }
103    }
104}
105
106fn capitalise(s: &str) -> String {
107    let mut chars = s.chars();
108    match chars.next() {
109        Some(c) => c.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
110        None => String::new(),
111    }
112}