sqruff_lib/rules/capitalisation/
cp02.rs

1use ahash::AHashMap;
2use regex::Regex;
3use sqruff_lib_core::dialects::init::DialectKind;
4use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
5
6use super::cp01::RuleCP01;
7use crate::core::config::Value;
8use crate::core::rules::base::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
9use crate::core::rules::context::RuleContext;
10use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
11use crate::utils::identifers::identifiers_policy_applicable;
12
13#[derive(Clone, Debug)]
14pub struct RuleCP02 {
15    base: RuleCP01,
16    unquoted_identifiers_policy: Option<String>,
17}
18
19impl Default for RuleCP02 {
20    fn default() -> Self {
21        Self {
22            base: RuleCP01 {
23                cap_policy_name: "extended_capitalisation_policy".into(),
24                description_elem: "Unquoted identifiers",
25                ..Default::default()
26            },
27            unquoted_identifiers_policy: None,
28        }
29    }
30}
31
32impl Rule for RuleCP02 {
33    fn load_from_config(&self, config: &AHashMap<String, Value>) -> Result<ErasedRule, String> {
34        Ok(RuleCP02 {
35            base: RuleCP01 {
36                capitalisation_policy: config["extended_capitalisation_policy"]
37                    .as_string()
38                    .unwrap()
39                    .into(),
40                cap_policy_name: "extended_capitalisation_policy".into(),
41                description_elem: "Unquoted identifiers",
42                ignore_words: config["ignore_words"]
43                    .map(|it| {
44                        it.as_array()
45                            .unwrap()
46                            .iter()
47                            .map(|it| it.as_string().unwrap().to_lowercase())
48                            .collect()
49                    })
50                    .unwrap_or_default(),
51                ignore_words_regex: config["ignore_words_regex"]
52                    .map(|it| {
53                        it.as_array()
54                            .unwrap()
55                            .iter()
56                            .map(|it| Regex::new(it.as_string().unwrap()).unwrap())
57                            .collect()
58                    })
59                    .unwrap_or_default(),
60
61                ..Default::default()
62            },
63            unquoted_identifiers_policy: config
64                .get("unquoted_identifiers_policy")
65                .and_then(|it| it.as_string())
66                .map(ToString::to_string),
67        }
68        .erased())
69    }
70
71    fn name(&self) -> &'static str {
72        "capitalisation.identifiers"
73    }
74
75    fn description(&self) -> &'static str {
76        "Inconsistent capitalisation of unquoted identifiers."
77    }
78
79    fn long_description(&self) -> &'static str {
80        r#"
81**Anti-pattern**
82
83In this example, unquoted identifier `a` is in lower-case but `B` is in upper-case.
84
85```sql
86select
87    a,
88    B
89from foo
90```
91
92**Best practice**
93
94Ensure all unquoted identifiers are either in upper-case or in lower-case.
95
96```sql
97select
98    a,
99    b
100from foo
101
102-- Also good
103
104select
105    A,
106    B
107from foo
108```
109"#
110    }
111
112    fn groups(&self) -> &'static [RuleGroups] {
113        &[
114            RuleGroups::All,
115            RuleGroups::Core,
116            RuleGroups::Capitalisation,
117        ]
118    }
119
120    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
121        // TODO: add databricks
122        if context.dialect.name == DialectKind::Sparksql
123            && context
124                .parent_stack
125                .last()
126                .is_some_and(|it| it.get_type() == SyntaxKind::PropertyNameIdentifier)
127            && context.segment.raw() == "enableChangeDataFeed"
128        {
129            return Vec::new();
130        }
131
132        let policy = self
133            .unquoted_identifiers_policy
134            .as_deref()
135            .unwrap_or_else(|| {
136                context.config.raw["rules"]["unquoted_identifiers_policy"]
137                    .as_string()
138                    .unwrap()
139            });
140        if identifiers_policy_applicable(policy, &context.parent_stack) {
141            self.base.eval(context)
142        } else {
143            vec![LintResult::new(None, Vec::new(), None, None)]
144        }
145    }
146
147    fn is_fix_compatible(&self) -> bool {
148        true
149    }
150
151    fn crawl_behaviour(&self) -> Crawler {
152        SegmentSeekerCrawler::new(
153            const {
154                SyntaxSet::new(&[
155                    SyntaxKind::NakedIdentifier,
156                    SyntaxKind::PropertiesNakedIdentifier,
157                ])
158            },
159        )
160        .into()
161    }
162}