sqruff_lib/rules/convention/
cv09.rs

1use 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}