sqruff_lib/rules/references/
rf06.rs1use regex::Regex;
2use sqruff_lib_core::dialects::init::DialectKind;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4use sqruff_lib_core::lint_fix::LintFix;
5use sqruff_lib_core::parser::segments::base::SegmentBuilder;
6
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::functional::context::FunctionalContext;
12
13#[derive(Default, Debug, Clone)]
14pub struct RuleRF06 {
15 prefer_quoted_identifiers: bool,
16 prefer_quoted_keywords: bool,
17 ignore_words: Vec<String>,
18 ignore_words_regex: Vec<Regex>,
19 force_enable: bool,
20}
21
22impl Rule for RuleRF06 {
23 fn load_from_config(
24 &self,
25 config: &ahash::AHashMap<String, Value>,
26 ) -> Result<ErasedRule, String> {
27 Ok(Self {
28 prefer_quoted_identifiers: config["prefer_quoted_identifiers"].as_bool().unwrap(),
29 prefer_quoted_keywords: config["prefer_quoted_keywords"].as_bool().unwrap(),
30 ignore_words: config["ignore_words"]
31 .map(|it| {
32 it.as_array()
33 .unwrap()
34 .iter()
35 .map(|it| it.as_string().unwrap().to_lowercase())
36 .collect()
37 })
38 .unwrap_or_default(),
39 ignore_words_regex: config["ignore_words_regex"]
40 .map(|it| {
41 it.as_array()
42 .unwrap()
43 .iter()
44 .map(|it| Regex::new(it.as_string().unwrap()).unwrap())
45 .collect()
46 })
47 .unwrap_or_default(),
48 force_enable: config["force_enable"].as_bool().unwrap(),
49 }
50 .erased())
51 }
52
53 fn name(&self) -> &'static str {
54 "references.quoting"
55 }
56
57 fn description(&self) -> &'static str {
58 "Unnecessary quoted identifier."
59 }
60
61 fn long_description(&self) -> &'static str {
62 r#"
63**Anti-pattern**
64
65In this example, a valid unquoted identifier, that is also not a reserved keyword, is needlessly quoted.
66
67```sql
68SELECT 123 as "foo"
69```
70
71**Best practice**
72
73Use unquoted identifiers where possible.
74
75```sql
76SELECT 123 as foo
77```
78
79When `prefer_quoted_identifiers = True`, the quotes are always necessary, no matter if the identifier is valid, a reserved keyword, or contains special characters.
80
81> **Note**
82> Note due to different quotes being used by different dialects supported by `SQLFluff`, and those quotes meaning different things in different contexts, this mode is not `sqlfluff fix` compatible.
83
84**Anti-pattern**
85
86In this example, a valid unquoted identifier, that is also not a reserved keyword, is required to be quoted.
87
88```sql
89SELECT 123 as foo
90```
91
92**Best practice**
93
94Use quoted identifiers.
95
96```sql
97SELECT 123 as "foo" -- For ANSI, ...
98-- or
99SELECT 123 as `foo` -- For BigQuery, MySql, ...
100```"#
101 }
102
103 fn groups(&self) -> &'static [RuleGroups] {
104 &[RuleGroups::All, RuleGroups::References]
105 }
106
107 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
108 if matches!(
109 context.dialect.name,
110 DialectKind::Postgres | DialectKind::Snowflake
111 ) && !self.force_enable
112 {
113 return Vec::new();
114 }
115
116 if FunctionalContext::new(context)
117 .parent_stack()
118 .any(Some(|it| {
119 [SyntaxKind::PasswordAuth, SyntaxKind::ExecuteAsClause]
120 .into_iter()
121 .any(|ty| it.is_type(ty))
122 }))
123 {
124 return Vec::new();
125 }
126
127 let identifier_is_quoted =
128 !lazy_regex::regex_is_match!(r#"^[^"\'\[].+[^"\'\]]$"#, context.segment.raw().as_ref());
129
130 let identifier_contents = context.segment.raw();
131 let identifier_contents = if identifier_is_quoted {
132 identifier_contents
133 .get(1..identifier_contents.len() - 1)
134 .map(ToOwned::to_owned)
135 .unwrap_or_default()
136 } else {
137 identifier_contents.to_string()
138 };
139
140 let identifier_is_keyword = context
141 .dialect
142 .sets("reserved_keywords")
143 .contains(identifier_contents.to_uppercase().as_str())
144 || context
145 .dialect
146 .sets("unreserved_keywords")
147 .contains(identifier_contents.to_uppercase().as_str());
148
149 let context_policy = if self.prefer_quoted_identifiers {
150 SyntaxKind::NakedIdentifier
151 } else {
152 SyntaxKind::QuotedIdentifier
153 };
154
155 if self
156 .ignore_words
157 .contains(&identifier_contents.to_lowercase())
158 {
159 return Vec::new();
160 }
161
162 if self
163 .ignore_words_regex
164 .iter()
165 .any(|regex| regex.is_match(identifier_contents.as_ref()))
166 {
167 return Vec::new();
168 }
169
170 if self.prefer_quoted_keywords && identifier_is_keyword {
171 return if !identifier_is_quoted {
172 vec![LintResult::new(
173 context.segment.clone().into(),
174 Vec::new(),
175 Some(format!(
176 "Missing quoted keyword identifier {identifier_contents}."
177 )),
178 None,
179 )]
180 } else {
181 Vec::new()
182 };
183 }
184
185 if !context.segment.is_type(context_policy)
186 || context
187 .segment
188 .raw()
189 .eq_ignore_ascii_case("quoted_identifier")
190 || context
191 .segment
192 .raw()
193 .eq_ignore_ascii_case("naked_identifier")
194 {
195 return Vec::new();
196 }
197
198 if self.prefer_quoted_identifiers {
199 return vec![LintResult::new(
200 context.segment.clone().into(),
201 Vec::new(),
202 Some(format!("Missing quoted identifier {identifier_contents}.")),
203 None,
204 )];
205 }
206
207 let owned = context.dialect.grammar("NakedIdentifierSegment");
208
209 let naked_identifier_parser = owned.as_regex().unwrap();
210
211 if is_full_match(
212 naked_identifier_parser.template.as_str(),
213 &identifier_contents,
214 ) && naked_identifier_parser
215 .anti_template
216 .as_ref()
217 .is_none_or(|anti_template| {
218 !is_full_match(anti_template.as_str(), &identifier_contents)
219 })
220 {
221 return vec![LintResult::new(
222 context.segment.clone().into(),
223 vec![LintFix::replace(
224 context.segment.clone(),
225 vec![
226 SegmentBuilder::token(
227 context.tables.next_id(),
228 &identifier_contents,
229 SyntaxKind::NakedIdentifier,
230 )
231 .finish(),
232 ],
233 None,
234 )],
235 Some(format!(
236 "Unnecessary quoted identifier {}.",
237 context.segment.raw()
238 )),
239 None,
240 )];
241 }
242
243 Vec::new()
244 }
245
246 fn is_fix_compatible(&self) -> bool {
247 true
248 }
249
250 fn crawl_behaviour(&self) -> Crawler {
251 SegmentSeekerCrawler::new(
252 const { SyntaxSet::new(&[SyntaxKind::QuotedIdentifier, SyntaxKind::NakedIdentifier]) },
253 )
254 .into()
255 }
256}
257
258fn is_full_match(pattern: &str, text: &str) -> bool {
259 let full_pattern = format!("(?i)^{}$", pattern); let regex = fancy_regex::Regex::new(&full_pattern).unwrap();
261 regex.is_match(text).unwrap()
262}