sqruff_lib/rules/convention/
cv09.rs1use hashbrown::{HashMap, HashSet};
2use smol_str::StrExt;
3use sqruff_lib_core::dialects::syntax::SyntaxKind;
4
5use crate::core::config::Value;
6use crate::core::rules::context::RuleContext;
7use crate::core::rules::crawlers::{Crawler, TokenSeekerCrawler};
8use crate::core::rules::{Erased, ErasedRule, LintResult, Rule, RuleGroups};
9
10#[derive(Default, Clone, Debug)]
11pub struct RuleCV09 {
12 blocked_words: HashSet<String>,
13 blocked_regex: Vec<regex::Regex>,
14 match_source: bool,
15}
16
17impl Rule for RuleCV09 {
18 fn load_from_config(&self, config: &HashMap<String, Value>) -> Result<ErasedRule, String> {
19 let blocked_words = config["blocked_words"]
20 .as_array()
21 .unwrap_or_default()
22 .into_iter()
23 .map(|word| {
24 word.as_string()
25 .map(|word| word.to_string().to_uppercase())
26 .ok_or_else(|| "blocked_words must be a string or array of strings".to_string())
27 })
28 .collect::<Result<HashSet<_>, _>>()?;
29 let blocked_regex = config["blocked_regex"]
30 .as_array()
31 .unwrap_or_default()
32 .into_iter()
33 .map(|regex| {
34 let regex = regex.as_string();
35 if let Some(regex) = regex {
36 Ok(regex::Regex::new(regex).map_err(|e| e.to_string())?)
37 } else {
38 Err("blocked_regex must be an array of strings".to_string())
39 }
40 })
41 .collect::<Result<Vec<_>, _>>()?;
42 let match_source = config["match_source"].as_bool().unwrap_or_default();
43 Ok(RuleCV09 {
44 blocked_words,
45 blocked_regex,
46 match_source,
47 }
48 .erased())
49 }
50
51 fn name(&self) -> &'static str {
52 "convention.blocked_words"
53 }
54
55 fn description(&self) -> &'static str {
56 "Block a list of configurable words from being used."
57 }
58
59 fn long_description(&self) -> &'static str {
60 r#"
61This generic rule can be useful to prevent certain keywords, functions, or objects
62from being used. Only whole words can be blocked, not phrases, nor parts of words.
63
64This block list is case insensitive.
65
66**Example use cases**
67
68* We prefer ``BOOL`` over ``BOOLEAN`` and there is no existing rule to enforce
69 this. Until such a rule is written, we can add ``BOOLEAN`` to the deny list
70 to cause a linting error to flag this.
71* We have deprecated a schema/table/function and want to prevent it being used
72 in future. We can add that to the denylist and then add a ``-- noqa: CV09`` for
73 the few exceptions that still need to be in the code base for now.
74
75**Anti-pattern**
76
77If the ``blocked_words`` config is set to ``deprecated_table,bool`` then the following will flag:
78
79```sql
80SELECT * FROM deprecated_table WHERE 1 = 1;
81CREATE TABLE myschema.t1 (a BOOL);
82```
83
84**Best practice**
85
86Do not used any blocked words.
87
88```sql
89SELECT * FROM my_table WHERE 1 = 1;
90CREATE TABLE myschema.t1 (a BOOL);
91```
92"#
93 }
94
95 fn groups(&self) -> &'static [RuleGroups] {
96 &[RuleGroups::All, RuleGroups::Convention]
97 }
98
99 fn eval(&self, context: &RuleContext) -> Vec<LintResult> {
100 if matches!(
101 context.segment.get_type(),
102 SyntaxKind::Comment | SyntaxKind::InlineComment | SyntaxKind::BlockComment
103 ) || self.blocked_words.is_empty() && self.blocked_regex.is_empty()
104 {
105 return vec![];
106 }
107
108 let raw_upper = context.segment.raw().to_uppercase();
109
110 if self.blocked_words.contains(&raw_upper) {
111 return vec![LintResult::new(
112 Some(context.segment.clone()),
113 vec![],
114 Some(format!("Use of blocked word '{raw_upper}'.")),
115 None,
116 )];
117 }
118
119 for regex in &self.blocked_regex {
120 if regex.is_match(&raw_upper) {
121 return vec![LintResult::new(
122 Some(context.segment.clone()),
123 vec![],
124 Some(format!("Use of blocked regex '{raw_upper}'.")),
125 None,
126 )];
127 }
128
129 if self.match_source {
130 for (segment, _) in context.segment.raw_segments_with_ancestors() {
131 if regex.is_match(segment.raw().to_uppercase_smolstr().as_str()) {
132 return vec![LintResult::new(
133 Some(context.segment.clone()),
134 vec![],
135 Some(format!("Use of blocked regex '{raw_upper}'.")),
136 None,
137 )];
138 }
139 }
140 }
141 }
142
143 vec![]
144 }
145
146 fn crawl_behaviour(&self) -> Crawler {
147 TokenSeekerCrawler.into()
148 }
149}