sqruff_lib/rules/references/
rf06.rs

1use 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); // Adding (?i) for case insensitivity
260    let regex = fancy_regex::Regex::new(&full_pattern).unwrap();
261    regex.is_match(text).unwrap()
262}