sqruff_lib/rules/capitalisation/
cp02.rs1use 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 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}