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 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: &HashMap<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: HashMap<&'static str, HashSet<&'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) -> HashMap<&'static str, HashSet<&'static str>> {
281 let valid_codes: HashSet<_> = self.register.keys().copied().collect();
282
283 let reference_map: HashMap<_, HashSet<_>> = valid_codes
284 .iter()
285 .map(|&code| (code, HashSet::from([code])))
286 .collect();
287
288 let name_map = {
289 let mut name_map = HashMap::new();
290 for manifest in self.register.values() {
291 name_map
292 .entry(manifest.name)
293 .or_insert_with(HashSet::new)
294 .insert(manifest.code);
295 }
296 name_map
297 };
298
299 let name_collisions: HashSet<_> = {
300 let name_keys: HashSet<_> = 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: HashMap<_, _> = chain(name_map, reference_map).collect();
312
313 let mut group_map: HashMap<_, HashSet<&'static str>> = HashMap::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(HashSet::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: &HashMap<&'static str, HashSet<&'static str>>,
342 ) -> Result<HashSet<&'static str>, SQLFluffUserError> {
343 let mut expanded_rule_set = HashSet::new();
344 let mut unknown_rules = Vec::new();
345
346 for r in glob_list {
347 if reference_map.contains_key(r.as_str()) {
348 expanded_rule_set.extend(reference_map[r.as_str()].clone());
349 } else {
350 unknown_rules.push(r);
351 }
352 }
353
354 if !unknown_rules.is_empty() {
355 let mut available_rules: Vec<_> = reference_map.keys().copied().collect();
356 available_rules.sort();
357 return Err(SQLFluffUserError::new(format!(
358 "Unknown rule(s) in configuration: {}. Available rules are: {}",
359 unknown_rules.join(", "),
360 available_rules.join(", ")
361 )));
362 }
363
364 Ok(expanded_rule_set)
365 }
366
367 pub(crate) fn get_rulepack(&self, config: &FluffConfig) -> Result<RulePack, SQLFluffUserError> {
368 let reference_map = self.rule_reference_map();
369 let rules = config.get_section("rules");
370 let keylist = self.register.keys();
371 let mut instantiated_rules = Vec::with_capacity(keylist.len());
372
373 let allowlist: Vec<String> = match config.get("rule_allowlist", "core").as_array() {
374 Some(array) => array
375 .iter()
376 .map(|it| it.as_string().unwrap().to_owned())
377 .collect(),
378 None => self.register.keys().map(|it| it.to_string()).collect(),
379 };
380
381 let denylist: Vec<String> = match config.get("rule_denylist", "core").as_array() {
382 Some(array) => array
383 .iter()
384 .map(|it| it.as_string().unwrap().to_owned())
385 .collect(),
386 None => Vec::new(),
387 };
388
389 let expanded_allowlist = self.expand_rule_refs(allowlist, &reference_map)?;
390 let expanded_denylist = self.expand_rule_refs(denylist, &reference_map)?;
391
392 let keylist: Vec<_> = keylist
393 .into_iter()
394 .filter(|&&r| expanded_allowlist.contains(r) && !expanded_denylist.contains(r))
395 .collect();
396
397 for code in keylist {
398 let rule = self.register[code].rule_class.clone();
399 let rule_config_ref = rule.config_ref();
400
401 let tmp = HashMap::new();
402
403 let specific_rule_config = rules
404 .get(rule_config_ref)
405 .and_then(|section| section.as_map())
406 .unwrap_or(&tmp);
407
408 instantiated_rules.push(rule.load_from_config(specific_rule_config).unwrap());
410 }
411
412 Ok(RulePack {
413 rules: instantiated_rules,
414 _reference_map: reference_map,
415 })
416 }
417}