Skip to main content

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 hashbrown::{HashMap, HashSet};
12use itertools::chain;
13use sqruff_lib_core::dialects::Dialect;
14use sqruff_lib_core::dialects::init::DialectKind;
15use sqruff_lib_core::errors::{ErrorStructRule, SQLFluffUserError, 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    /// Whether the anchor sits in a template-generated (non-literal) region.
66    pub fn anchor_in_templated_section(&self) -> bool {
67        self.anchor.as_ref().is_some_and(|anchor| {
68            anchor
69                .get_position_marker()
70                .is_some_and(|marker| !marker.is_literal())
71        })
72    }
73
74    pub fn to_linting_error(self, rule: &ErasedRule) -> Option<SQLLintError> {
75        let anchor = self.anchor.clone()?;
76
77        let description = self
78            .description
79            .as_deref()
80            .unwrap_or_else(|| rule.description());
81
82        let is_fixable = rule.is_fix_compatible();
83
84        SQLLintError::new(description, anchor, is_fixable)
85            .config(|this| {
86                this.rule = Some(ErrorStructRule {
87                    name: rule.name(),
88                    code: rule.code(),
89                })
90            })
91            .into()
92    }
93}
94
95impl Debug for LintResult {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match &self.anchor {
98            None => write!(f, "LintResult(<empty>)"),
99            Some(anchor) => {
100                let fix_coda = if !self.fixes.is_empty() {
101                    format!("+{}F", self.fixes.len())
102                } else {
103                    "".to_string()
104                };
105
106                match &self.description {
107                    Some(desc) => {
108                        if !self.source.is_empty() {
109                            write!(
110                                f,
111                                "LintResult({} [{}]: {:?}{})",
112                                desc, self.source, anchor, fix_coda
113                            )
114                        } else {
115                            write!(f, "LintResult({desc}: {anchor:?}{fix_coda})")
116                        }
117                    }
118                    None => write!(f, "LintResult({anchor:?}{fix_coda})"),
119                }
120            }
121        }
122    }
123}
124
125#[derive(Debug, Clone, PartialEq)]
126pub enum LintPhase {
127    Main,
128    Post,
129}
130
131pub trait Rule: Debug + 'static + Send + Sync {
132    fn load_from_config(&self, _config: &HashMap<String, Value>) -> Result<ErasedRule, String>;
133
134    fn lint_phase(&self) -> LintPhase {
135        LintPhase::Main
136    }
137
138    fn name(&self) -> &'static str;
139
140    fn config_ref(&self) -> &'static str {
141        self.name()
142    }
143
144    fn description(&self) -> &'static str;
145
146    fn long_description(&self) -> &'static str;
147
148    /// All the groups this rule belongs to, including 'all' because that is a
149    /// given. There should be no duplicates and 'all' should be the first
150    /// element.
151    fn groups(&self) -> &'static [RuleGroups];
152
153    fn force_enable(&self) -> bool {
154        false
155    }
156
157    /// Returns the set of dialects for which a particular rule should be
158    /// skipped.
159    fn dialect_skip(&self) -> &'static [DialectKind] {
160        &[]
161    }
162
163    fn code(&self) -> &'static str {
164        let name = std::any::type_name::<Self>();
165        name.split("::")
166            .last()
167            .unwrap()
168            .strip_prefix("Rule")
169            .unwrap_or(name)
170    }
171
172    fn eval(&self, rule_cx: &RuleContext) -> Vec<LintResult>;
173
174    fn is_fix_compatible(&self) -> bool {
175        false
176    }
177
178    /// Whether this rule is designed to operate on template-generated regions.
179    ///
180    /// When `false` (the default) and `ignore_templated_areas` is enabled, any
181    /// lint result whose anchor falls in a non-literal (templated) section is
182    /// suppressed, matching SQLFluff's behaviour.
183    fn targets_templated(&self) -> bool {
184        false
185    }
186
187    fn crawl_behaviour(&self) -> Crawler;
188}
189
190/// Emits a `targets_templated` override returning `true`, for rules that
191/// operate on template-generated regions (SQLFluff's `targets_templated`).
192macro_rules! targets_templated {
193    () => {
194        fn targets_templated(&self) -> bool {
195            true
196        }
197    };
198}
199pub(crate) use targets_templated;
200
201pub struct Exception;
202
203pub fn crawl(
204    rule: &ErasedRule,
205    tables: &Tables,
206    dialect: &Dialect,
207    templated_file: &TemplatedFile,
208    tree: ErasedSegment,
209    config: &FluffConfig,
210    on_violation: &mut impl FnMut(LintResult),
211) -> Result<(), Exception> {
212    let mut root_context = RuleContext::new(tables, dialect, config, tree.clone());
213    root_context.templated_file = Some(templated_file.clone());
214    let mut has_exception = false;
215
216    // TODO Will to return a note that rules were skipped
217    if rule.dialect_skip().contains(&dialect.name) && !rule.force_enable() {
218        return Ok(());
219    }
220
221    rule.crawl_behaviour()
222        .crawl(&mut root_context, &mut |context| {
223            let resp =
224                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| rule.eval(context)));
225
226            let Ok(results) = resp else {
227                has_exception = true;
228                return;
229            };
230
231            for result in results {
232                if !result
233                    .fixes
234                    .iter()
235                    .any(|it| it.has_template_conflicts(templated_file))
236                {
237                    on_violation(result);
238                }
239            }
240        });
241
242    if has_exception {
243        Err(Exception)
244    } else {
245        Ok(())
246    }
247}
248
249#[derive(Debug, Clone)]
250pub struct ErasedRule {
251    erased: Arc<dyn Rule>,
252}
253
254impl PartialEq for ErasedRule {
255    fn eq(&self, _other: &Self) -> bool {
256        unimplemented!()
257    }
258}
259
260impl Deref for ErasedRule {
261    type Target = dyn Rule;
262
263    fn deref(&self) -> &Self::Target {
264        self.erased.as_ref()
265    }
266}
267
268pub trait Erased {
269    type Erased;
270
271    fn erased(self) -> Self::Erased;
272}
273
274impl<T: Rule> Erased for T {
275    type Erased = ErasedRule;
276
277    fn erased(self) -> Self::Erased {
278        ErasedRule {
279            erased: Arc::new(self),
280        }
281    }
282}
283
284pub struct RuleManifest {
285    pub code: &'static str,
286    pub name: &'static str,
287    pub description: &'static str,
288    pub groups: &'static [RuleGroups],
289    pub rule_class: ErasedRule,
290}
291
292#[derive(Clone)]
293pub struct RulePack {
294    pub(crate) rules: Vec<ErasedRule>,
295    _reference_map: HashMap<&'static str, HashSet<&'static str>>,
296}
297
298impl RulePack {
299    pub fn rules(&self) -> Vec<ErasedRule> {
300        self.rules.clone()
301    }
302}
303
304pub struct RuleSet {
305    pub(crate) register: IndexMap<&'static str, RuleManifest>,
306}
307
308impl RuleSet {
309    fn rule_reference_map(&self) -> HashMap<&'static str, HashSet<&'static str>> {
310        let valid_codes: HashSet<_> = self.register.keys().copied().collect();
311
312        let reference_map: HashMap<_, HashSet<_>> = valid_codes
313            .iter()
314            .map(|&code| (code, HashSet::from([code])))
315            .collect();
316
317        let name_map = {
318            let mut name_map = HashMap::new();
319            for manifest in self.register.values() {
320                name_map
321                    .entry(manifest.name)
322                    .or_insert_with(HashSet::new)
323                    .insert(manifest.code);
324            }
325            name_map
326        };
327
328        let name_collisions: HashSet<_> = {
329            let name_keys: HashSet<_> = name_map.keys().copied().collect();
330            name_keys.intersection(&valid_codes).copied().collect()
331        };
332
333        if !name_collisions.is_empty() {
334            log::warn!(
335                "The following defined rule names were found which collide with codes. Those \
336                 names will not be available for selection: {name_collisions:?}",
337            );
338        }
339
340        let reference_map: HashMap<_, _> = chain(name_map, reference_map).collect();
341
342        let mut group_map: HashMap<_, HashSet<&'static str>> = HashMap::new();
343        for manifest in self.register.values() {
344            for group in manifest.groups {
345                let group = group.as_ref();
346                if let Some(codes) = reference_map.get(group) {
347                    log::warn!(
348                        "Rule {} defines group '{}' which is already defined as a name or code of \
349                         {:?}. This group will not be available for use as a result of this \
350                         collision.",
351                        manifest.code,
352                        group,
353                        codes
354                    );
355                } else {
356                    group_map
357                        .entry(group)
358                        .or_insert_with(HashSet::new)
359                        .insert(manifest.code);
360                }
361            }
362        }
363
364        chain(group_map, reference_map).collect()
365    }
366
367    fn expand_rule_refs(
368        &self,
369        glob_list: Vec<String>,
370        reference_map: &HashMap<&'static str, HashSet<&'static str>>,
371    ) -> Result<HashSet<&'static str>, SQLFluffUserError> {
372        let mut expanded_rule_set = HashSet::new();
373        let mut unknown_rules = Vec::new();
374
375        for r in glob_list {
376            if reference_map.contains_key(r.as_str()) {
377                expanded_rule_set.extend(reference_map[r.as_str()].clone());
378            } else {
379                unknown_rules.push(r);
380            }
381        }
382
383        if !unknown_rules.is_empty() {
384            let mut available_rules: Vec<_> = reference_map.keys().copied().collect();
385            available_rules.sort();
386            return Err(SQLFluffUserError::new(format!(
387                "Unknown rule(s) in configuration: {}. Available rules are: {}",
388                unknown_rules.join(", "),
389                available_rules.join(", ")
390            )));
391        }
392
393        Ok(expanded_rule_set)
394    }
395
396    pub(crate) fn get_rulepack(&self, config: &FluffConfig) -> Result<RulePack, SQLFluffUserError> {
397        let reference_map = self.rule_reference_map();
398        let rules = config.get_section("rules");
399        let keylist = self.register.keys();
400        let mut instantiated_rules = Vec::with_capacity(keylist.len());
401
402        let allowlist: Vec<String> = match config.get("rule_allowlist", "core").as_array() {
403            Some(array) => array
404                .iter()
405                .map(|it| it.as_string().unwrap().to_owned())
406                .collect(),
407            None => self.register.keys().map(|it| it.to_string()).collect(),
408        };
409
410        let denylist: Vec<String> = match config.get("rule_denylist", "core").as_array() {
411            Some(array) => array
412                .iter()
413                .map(|it| it.as_string().unwrap().to_owned())
414                .collect(),
415            None => Vec::new(),
416        };
417
418        let expanded_allowlist = self.expand_rule_refs(allowlist, &reference_map)?;
419        let expanded_denylist = self.expand_rule_refs(denylist, &reference_map)?;
420
421        let keylist: Vec<_> = keylist
422            .into_iter()
423            .filter(|&&r| expanded_allowlist.contains(r) && !expanded_denylist.contains(r))
424            .collect();
425
426        for code in keylist {
427            let rule = self.register[code].rule_class.clone();
428            let rule_config_ref = rule.config_ref();
429
430            let tmp = HashMap::new();
431
432            let specific_rule_config = rules
433                .get(rule_config_ref)
434                .and_then(|section| section.as_map())
435                .unwrap_or(&tmp);
436
437            instantiated_rules.push(
438                rule.load_from_config(specific_rule_config)
439                    .map_err(SQLFluffUserError::new)?,
440            );
441        }
442
443        Ok(RulePack {
444            rules: instantiated_rules,
445            _reference_map: reference_map,
446        })
447    }
448}