sqruff_lib/rules/references/
rf04.rs1use 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 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}