rigsql_rules/capitalisation/
cp01.rs1use 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#[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}