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 LintResult {
58 anchor,
59 fixes,
60 description,
61 source: source.unwrap_or_default(),
62 }
63 }
64
65 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 fn groups(&self) -> &'static [RuleGroups];
152
153 fn force_enable(&self) -> bool {
154 false
155 }
156
157 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 fn targets_templated(&self) -> bool {
184 false
185 }
186
187 fn crawl_behaviour(&self) -> Crawler;
188}
189
190macro_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 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}