Skip to main content

sqruff_lib/rules/convention/
cv09.rs

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