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