sqruff_lib/core/
rules.rs

1pub mod context;
2pub mod crawlers;
3pub mod noqa;
4pub mod reference;
5
6use std::fmt::{self, Debug};
7use std::ops::Deref;
8
9use std::sync::Arc;
10
11use ahash::{AHashMap, AHashSet};
12use itertools::chain;
13use sqruff_lib_core::dialects::Dialect;
14use sqruff_lib_core::dialects::init::DialectKind;
15use sqruff_lib_core::errors::{ErrorStructRule, SQLLintError};
16use sqruff_lib_core::helpers::{Config, IndexMap};
17use sqruff_lib_core::lint_fix::LintFix;
18use sqruff_lib_core::parser::segments::{ErasedSegment, Tables};
19use sqruff_lib_core::templaters::TemplatedFile;
20use strum_macros::AsRefStr;
21
22use crate::core::config::{FluffConfig, Value};
23use crate::core::rules::context::RuleContext;
24use crate::core::rules::crawlers::{BaseCrawler as _, Crawler};
25
26pub struct LintResult {
27    pub anchor: Option<ErasedSegment>,
28    pub fixes: Vec<LintFix>,
29    description: Option<String>,
30    source: String,
31}
32
33#[derive(Debug, Clone, PartialEq, Copy, Hash, Eq, AsRefStr)]
34#[strum(serialize_all = "lowercase")]
35pub enum RuleGroups {
36    All,
37    Core,
38    Aliasing,
39    Ambiguous,
40    Capitalisation,
41    Convention,
42    Jinja,
43    Layout,
44    References,
45    Structure,
46}
47
48impl LintResult {
49    pub fn new(
50        anchor: Option<ErasedSegment>,
51        fixes: Vec<LintFix>,
52        description: Option<String>,
53        source: Option<String>,
54    ) -> Self {
55        // let fixes = fixes.into_iter().filter(|f| !f.is_trivial()).collect();
56
57        LintResult {
58            anchor,
59            fixes,
60            description,
61            source: source.unwrap_or_default(),
62        }
63    }
64
65    pub fn to_linting_error(self, rule: &ErasedRule) -> Option<SQLLintError> {
66        let anchor = self.anchor.clone()?;
67
68        let description = self
69            .description
70            .as_deref()
71            .unwrap_or_else(|| rule.description());
72
73        let is_fixable = rule.is_fix_compatible();
74
75        SQLLintError::new(description, anchor, is_fixable)
76            .config(|this| {
77                this.rule = Some(ErrorStructRule {
78                    name: rule.name(),
79                    code: rule.code(),
80                })
81            })
82            .into()
83    }
84}
85
86impl Debug for LintResult {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match &self.anchor {
89            None => write!(f, "LintResult(<empty>)"),
90            Some(anchor) => {
91                let fix_coda = if !self.fixes.is_empty() {
92                    format!("+{}F", self.fixes.len())
93                } else {
94                    "".to_string()
95                };
96
97                match &self.description {
98                    Some(desc) => {
99                        if !self.source.is_empty() {
100                            write!(
101                                f,
102                                "LintResult({} [{}]: {:?}{})",
103                                desc, self.source, anchor, fix_coda
104                            )
105                        } else {
106                            write!(f, "LintResult({desc}: {anchor:?}{fix_coda})")
107                        }
108                    }
109                    None => write!(f, "LintResult({anchor:?}{fix_coda})"),
110                }
111            }
112        }
113    }
114}
115
116#[derive(Debug, Clone, PartialEq)]
117pub enum LintPhase {
118    Main,
119    Post,
120}
121
122pub trait Rule: Debug + 'static + Send + Sync {
123    fn load_from_config(&self, _config: &AHashMap<String, Value>) -> Result<ErasedRule, String>;
124
125    fn lint_phase(&self) -> LintPhase {
126        LintPhase::Main
127    }
128
129    fn name(&self) -> &'static str;
130
131    fn config_ref(&self) -> &'static str {
132        self.name()
133    }
134
135    fn description(&self) -> &'static str;
136
137    fn long_description(&self) -> &'static str;
138
139    /// All the groups this rule belongs to, including 'all' because that is a
140    /// given. There should be no duplicates and 'all' should be the first
141    /// element.
142    fn groups(&self) -> &'static [RuleGroups];
143
144    fn force_enable(&self) -> bool {
145        false
146    }
147
148    /// Returns the set of dialects for which a particular rule should be
149    /// skipped.
150    fn dialect_skip(&self) -> &'static [DialectKind] {
151        &[]
152    }
153
154    fn code(&self) -> &'static str {
155        let name = std::any::type_name::<Self>();
156        name.split("::")
157            .last()
158            .unwrap()
159            .strip_prefix("Rule")
160            .unwrap_or(name)
161    }
162
163    fn eval(&self, rule_cx: &RuleContext) -> Vec<LintResult>;
164
165    fn is_fix_compatible(&self) -> bool {
166        false
167    }
168
169    fn crawl_behaviour(&self) -> Crawler;
170}
171
172pub struct Exception;
173
174pub fn crawl(
175    rule: &ErasedRule,
176    tables: &Tables,
177    dialect: &Dialect,
178    templated_file: &TemplatedFile,
179    tree: ErasedSegment,
180    config: &FluffConfig,
181    on_violation: &mut impl FnMut(LintResult),
182) -> Result<(), Exception> {
183    let mut root_context = RuleContext::new(tables, dialect, config, tree.clone());
184    root_context.templated_file = Some(templated_file.clone());
185    let mut has_exception = false;
186
187    // TODO Will to return a note that rules were skipped
188    if rule.dialect_skip().contains(&dialect.name) && !rule.force_enable() {
189        return Ok(());
190    }
191
192    rule.crawl_behaviour()
193        .crawl(&mut root_context, &mut |context| {
194            let resp =
195                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| rule.eval(context)));
196
197            let Ok(results) = resp else {
198                has_exception = true;
199                return;
200            };
201
202            for result in results {
203                if !result
204                    .fixes
205                    .iter()
206                    .any(|it| it.has_template_conflicts(templated_file))
207                {
208                    on_violation(result);
209                }
210            }
211        });
212
213    if has_exception {
214        Err(Exception)
215    } else {
216        Ok(())
217    }
218}
219
220#[derive(Debug, Clone)]
221pub struct ErasedRule {
222    erased: Arc<dyn Rule>,
223}
224
225impl PartialEq for ErasedRule {
226    fn eq(&self, _other: &Self) -> bool {
227        unimplemented!()
228    }
229}
230
231impl Deref for ErasedRule {
232    type Target = dyn Rule;
233
234    fn deref(&self) -> &Self::Target {
235        self.erased.as_ref()
236    }
237}
238
239pub trait Erased {
240    type Erased;
241
242    fn erased(self) -> Self::Erased;
243}
244
245impl<T: Rule> Erased for T {
246    type Erased = ErasedRule;
247
248    fn erased(self) -> Self::Erased {
249        ErasedRule {
250            erased: Arc::new(self),
251        }
252    }
253}
254
255pub struct RuleManifest {
256    pub code: &'static str,
257    pub name: &'static str,
258    pub description: &'static str,
259    pub groups: &'static [RuleGroups],
260    pub rule_class: ErasedRule,
261}
262
263#[derive(Clone)]
264pub struct RulePack {
265    pub(crate) rules: Vec<ErasedRule>,
266    _reference_map: AHashMap<&'static str, AHashSet<&'static str>>,
267}
268
269impl RulePack {
270    pub fn rules(&self) -> Vec<ErasedRule> {
271        self.rules.clone()
272    }
273}
274
275pub struct RuleSet {
276    pub(crate) register: IndexMap<&'static str, RuleManifest>,
277}
278
279impl RuleSet {
280    fn rule_reference_map(&self) -> AHashMap<&'static str, AHashSet<&'static str>> {
281        let valid_codes: AHashSet<_> = self.register.keys().copied().collect();
282
283        let reference_map: AHashMap<_, AHashSet<_>> = valid_codes
284            .iter()
285            .map(|&code| (code, AHashSet::from([code])))
286            .collect();
287
288        let name_map = {
289            let mut name_map = AHashMap::new();
290            for manifest in self.register.values() {
291                name_map
292                    .entry(manifest.name)
293                    .or_insert_with(AHashSet::new)
294                    .insert(manifest.code);
295            }
296            name_map
297        };
298
299        let name_collisions: AHashSet<_> = {
300            let name_keys: AHashSet<_> = name_map.keys().copied().collect();
301            name_keys.intersection(&valid_codes).copied().collect()
302        };
303
304        if !name_collisions.is_empty() {
305            log::warn!(
306                "The following defined rule names were found which collide with codes. Those \
307                 names will not be available for selection: {name_collisions:?}",
308            );
309        }
310
311        let reference_map: AHashMap<_, _> = chain(name_map, reference_map).collect();
312
313        let mut group_map: AHashMap<_, AHashSet<&'static str>> = AHashMap::new();
314        for manifest in self.register.values() {
315            for group in manifest.groups {
316                let group = group.as_ref();
317                if let Some(codes) = reference_map.get(group) {
318                    log::warn!(
319                        "Rule {} defines group '{}' which is already defined as a name or code of \
320                         {:?}. This group will not be available for use as a result of this \
321                         collision.",
322                        manifest.code,
323                        group,
324                        codes
325                    );
326                } else {
327                    group_map
328                        .entry(group)
329                        .or_insert_with(AHashSet::new)
330                        .insert(manifest.code);
331                }
332            }
333        }
334
335        chain(group_map, reference_map).collect()
336    }
337
338    fn expand_rule_refs(
339        &self,
340        glob_list: Vec<String>,
341        reference_map: &AHashMap<&'static str, AHashSet<&'static str>>,
342    ) -> AHashSet<&'static str> {
343        let mut expanded_rule_set = AHashSet::new();
344
345        for r in glob_list {
346            if reference_map.contains_key(r.as_str()) {
347                expanded_rule_set.extend(reference_map[r.as_str()].clone());
348            } else {
349                panic!("Rule {r} not found in rule reference map");
350            }
351        }
352
353        expanded_rule_set
354    }
355
356    pub(crate) fn get_rulepack(&self, config: &FluffConfig) -> RulePack {
357        let reference_map = self.rule_reference_map();
358        let rules = config.get_section("rules");
359        let keylist = self.register.keys();
360        let mut instantiated_rules = Vec::with_capacity(keylist.len());
361
362        let allowlist: Vec<String> = match config.get("rule_allowlist", "core").as_array() {
363            Some(array) => array
364                .iter()
365                .map(|it| it.as_string().unwrap().to_owned())
366                .collect(),
367            None => self.register.keys().map(|it| it.to_string()).collect(),
368        };
369
370        let denylist: Vec<String> = match config.get("rule_denylist", "core").as_array() {
371            Some(array) => array
372                .iter()
373                .map(|it| it.as_string().unwrap().to_owned())
374                .collect(),
375            None => Vec::new(),
376        };
377
378        let expanded_allowlist = self.expand_rule_refs(allowlist, &reference_map);
379        let expanded_denylist = self.expand_rule_refs(denylist, &reference_map);
380
381        let keylist: Vec<_> = keylist
382            .into_iter()
383            .filter(|&&r| expanded_allowlist.contains(r) && !expanded_denylist.contains(r))
384            .collect();
385
386        for code in keylist {
387            let rule = self.register[code].rule_class.clone();
388            let rule_config_ref = rule.config_ref();
389
390            let tmp = AHashMap::new();
391
392            let specific_rule_config = rules
393                .get(rule_config_ref)
394                .and_then(|section| section.as_map())
395                .unwrap_or(&tmp);
396
397            // TODO fail the rulepack if any need unwrapping
398            instantiated_rules.push(rule.load_from_config(specific_rule_config).unwrap());
399        }
400
401        RulePack {
402            rules: instantiated_rules,
403            _reference_map: reference_map,
404        }
405    }
406}