rigsql_rules/capitalisation/
cp02.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 RuleCP02 {
12 pub policy: IdentifierPolicy,
13}
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum IdentifierPolicy {
17 Lower,
18 Upper,
19 Consistent,
20}
21
22impl Default for RuleCP02 {
23 fn default() -> Self {
24 Self {
25 policy: IdentifierPolicy::Consistent,
26 }
27 }
28}
29
30impl Rule for RuleCP02 {
31 fn code(&self) -> &'static str {
32 "CP02"
33 }
34 fn name(&self) -> &'static str {
35 "capitalisation.identifiers"
36 }
37 fn description(&self) -> &'static str {
38 "Unquoted identifiers must be consistently capitalised."
39 }
40 fn explanation(&self) -> &'static str {
41 "Unquoted identifiers (table names, column names) should use consistent capitalisation. \
42 Most SQL style guides recommend lower_snake_case for identifiers."
43 }
44 fn groups(&self) -> &[RuleGroup] {
45 &[RuleGroup::Capitalisation]
46 }
47 fn is_fixable(&self) -> bool {
48 true
49 }
50
51 fn crawl_type(&self) -> CrawlType {
52 CrawlType::Segment(vec![SegmentType::Identifier])
53 }
54
55 fn eval(&self, ctx: &RuleContext) -> Vec<LintViolation> {
56 let Segment::Token(t) = ctx.segment else {
57 return vec![];
58 };
59 if t.token.kind != TokenKind::Word {
60 return vec![];
61 }
62 if is_keyword(&t.token.text) {
64 return vec![];
65 }
66 if let Some(parent) = ctx.parent {
68 if parent.segment_type() == rigsql_core::SegmentType::FunctionCall {
69 return vec![];
70 }
71 }
72
73 let text = t.token.text.as_str();
74
75 if !text.is_ascii() {
78 return vec![];
79 }
80
81 let expected = match self.policy {
82 IdentifierPolicy::Lower => text.to_ascii_lowercase(),
83 IdentifierPolicy::Upper => text.to_ascii_uppercase(),
84 IdentifierPolicy::Consistent => return vec![], };
86
87 if text != expected {
88 vec![LintViolation::with_fix(
89 self.code(),
90 format!(
91 "Unquoted identifiers must be {} case. Found '{}'.",
92 match self.policy {
93 IdentifierPolicy::Lower => "lower",
94 IdentifierPolicy::Upper => "upper",
95 IdentifierPolicy::Consistent => "consistent",
96 },
97 text
98 ),
99 t.token.span,
100 vec![SourceEdit::replace(t.token.span, expected.clone())],
101 )]
102 } else {
103 vec![]
104 }
105 }
106}