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 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 fn groups(&self) -> &'static [RuleGroups];
143
144 fn force_enable(&self) -> bool {
145 false
146 }
147
148 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 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 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}