sqruff_lib/rules/references/
rf04.rs

1use itertools::Itertools;
2use regex::Regex;
3use sqruff_lib_core::dialects::syntax::{SyntaxKind, SyntaxSet};
4
5use crate::core::rules::base::{CloneRule, ErasedRule, LintResult, Rule, RuleGroups};
6use crate::core::rules::context::RuleContext;
7use crate::core::rules::crawlers::{Crawler, SegmentSeekerCrawler};
8use crate::utils::identifers::identifiers_policy_applicable;
9
10#[derive(Debug, Clone, Default)]
11pub struct RuleRF04 {
12    unquoted_identifiers_policy: String,
13    quoted_identifiers_policy: Option<String>,
14    ignore_words: Vec<String>,
15    ignore_words_regex: Vec<Regex>,
16}
17
18impl Rule for RuleRF04 {
19    fn load_from_config(
20        &self,
21        config: &ahash::AHashMap<String, crate::core::config::Value>,
22    ) -> Result<ErasedRule, String> {
23        Ok(RuleRF04 {
24            unquoted_identifiers_policy: config["unquoted_identifiers_policy"]
25                .as_string()
26                .unwrap()
27                .to_owned(),
28            quoted_identifiers_policy: config["quoted_identifiers_policy"]
29                .map(|it| it.as_string().unwrap().to_string()),
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_vec()
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_vec()
46                })
47                .unwrap_or_default(),
48        }
49        .erased())
50    }
51
52    fn name(&self) -> &'static str {
53        "references.keywords"
54    }
55
56    fn description(&self) -> &'static str {
57        "Keywords should not be used as identifiers."
58    }
59
60    fn long_description(&self) -> &'static str {
61        r#"
62**Anti-pattern**
63
64In this example, `SUM` (a built-in function) is used as an alias.
65
66```sql
67SELECT
68    sum.a
69FROM foo AS sum
70```
71
72**Best practice**
73
74Avoid using keywords as the name of an alias.
75
76```sql
77SELECT
78    vee.a
79FROM foo AS vee
80```
81"#
82    }
83
84    fn groups(&self) -> &'static [RuleGroups] {
85        &[RuleGroups::All, RuleGroups::References]
86    }
87
88    fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
89        if context.segment.raw().len() == 1
90            || self
91                .ignore_words
92                .contains(&context.segment.raw().to_lowercase())
93            || self
94                .ignore_words_regex
95                .iter()
96                .any(|regex| regex.is_match(context.segment.raw()))
97        {
98            return vec![LintResult::new(None, Vec::new(), None, None)];
99        }
100
101        let raw_segment = context.segment.raw();
102        let upper_segment = {
103            if context.segment.is_type(SyntaxKind::NakedIdentifier) {
104                raw_segment.to_uppercase()
105            } else {
106                raw_segment[1..raw_segment.len() - 1].to_uppercase()
107            }
108        };
109
110        // FIXME: simplify the condition
111        if (context.segment.is_type(SyntaxKind::NakedIdentifier)
112            && identifiers_policy_applicable(
113                &self.unquoted_identifiers_policy,
114                &context.parent_stack,
115            )
116            && context
117                .dialect
118                .sets("unreserved_keywords")
119                .contains(context.segment.raw().to_uppercase().as_str()))
120            || (context.segment.is_type(SyntaxKind::QuotedIdentifier)
121                && self.quoted_identifiers_policy.as_ref().is_some_and(
122                    |quoted_identifiers_policy| {
123                        identifiers_policy_applicable(
124                            quoted_identifiers_policy,
125                            &context.parent_stack,
126                        )
127                    },
128                )
129                && context
130                    .dialect
131                    .sets("unreserved_keywords")
132                    .contains(upper_segment.as_str())
133                || context
134                    .dialect
135                    .sets("reserved_keywords")
136                    .contains(upper_segment.as_str()))
137        {
138            vec![LintResult::new(
139                Some(context.segment.clone()),
140                Vec::new(),
141                None,
142                None,
143            )]
144        } else {
145            Vec::new()
146        }
147    }
148
149    fn crawl_behaviour(&self) -> Crawler {
150        SegmentSeekerCrawler::new(
151            const { SyntaxSet::new(&[SyntaxKind::NakedIdentifier, SyntaxKind::QuotedIdentifier]) },
152        )
153        .into()
154    }
155}