1use std::ops::Index;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use ahash::AHashMap;
6use configparser::ini::Ini;
7use itertools::Itertools;
8use sqruff_lib_core::dialects::base::Dialect;
9use sqruff_lib_core::dialects::init::{DialectKind, dialect_readout};
10use sqruff_lib_core::errors::SQLFluffUserError;
11use sqruff_lib_core::parser::parser::Parser;
12use sqruff_lib_dialects::kind_to_dialect;
13
14use crate::utils::reflow::config::ReflowConfig;
15
16pub fn split_comma_separated_string(raw_str: &str) -> Value {
19 let values = raw_str
20 .split(',')
21 .filter_map(|x| {
22 let trimmed = x.trim();
23 (!trimmed.is_empty()).then(|| Value::String(trimmed.into()))
24 })
25 .collect();
26 Value::Array(values)
27}
28
29#[derive(Debug, PartialEq, Clone)]
32pub struct FluffConfig {
33 pub(crate) indentation: FluffConfigIndentation,
34 pub raw: AHashMap<String, Value>,
35 extra_config_path: Option<String>,
36 _configs: AHashMap<String, AHashMap<String, String>>,
37 pub(crate) dialect: Dialect,
38 sql_file_exts: Vec<String>,
39 reflow: ReflowConfig,
40}
41
42impl Default for FluffConfig {
43 fn default() -> Self {
44 Self::new(<_>::default(), None, None)
45 }
46}
47
48impl FluffConfig {
49 pub fn get(&self, key: &str, section: &str) -> &Value {
50 &self.raw[section][key]
51 }
52
53 pub fn reflow(&self) -> &ReflowConfig {
54 &self.reflow
55 }
56
57 pub fn reload_reflow(&mut self) {
58 self.reflow = ReflowConfig::from_fluff_config(self);
59 }
60
61 pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
67 let configs = ConfigLoader::from_source(source, optional_path_specification);
68 FluffConfig::new(configs, None, None)
69 }
70
71 pub fn get_section(&self, section: &str) -> &AHashMap<String, Value> {
72 self.raw[section].as_map().unwrap()
73 }
74
75 pub fn new(
77 configs: AHashMap<String, Value>,
78 extra_config_path: Option<String>,
79 indentation: Option<FluffConfigIndentation>,
80 ) -> Self {
81 fn nested_combine(
82 mut a: AHashMap<String, Value>,
83 b: AHashMap<String, Value>,
84 ) -> AHashMap<String, Value> {
85 for (key, value_b) in b {
86 match (a.get(&key), value_b) {
87 (Some(Value::Map(map_a)), Value::Map(map_b)) => {
88 let combined = nested_combine(map_a.clone(), map_b);
89 a.insert(key, Value::Map(combined));
90 }
91 (_, value) => {
92 a.insert(key, value);
93 }
94 }
95 }
96 a
97 }
98
99 let values = ConfigLoader::get_config_elems_from_file(
100 None,
101 include_str!("./default_config.cfg").into(),
102 );
103
104 let mut defaults = AHashMap::new();
105 ConfigLoader::incorporate_vals(&mut defaults, values);
106
107 let mut configs = nested_combine(defaults, configs);
108
109 let dialect = match configs
110 .get("core")
111 .and_then(|map| map.as_map().unwrap().get("dialect"))
112 {
113 None => DialectKind::default(),
114 Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
115 _value => DialectKind::default(),
116 };
117
118 let dialect = kind_to_dialect(&dialect);
119 for (in_key, out_key) in [
120 ("ignore", "ignore"),
122 ("warnings", "warnings"),
123 ("rules", "rule_allowlist"),
124 ("exclude_rules", "rule_denylist"),
126 ] {
127 match configs["core"].as_map().unwrap().get(in_key) {
128 Some(value) if !value.is_none() => {
129 let string = value.as_string().unwrap();
130 let values = split_comma_separated_string(string);
131
132 configs
133 .get_mut("core")
134 .unwrap()
135 .as_map_mut()
136 .unwrap()
137 .insert(out_key.into(), values);
138 }
139 _ => {}
140 }
141 }
142
143 let sql_file_exts = configs["core"]["sql_file_exts"]
144 .as_array()
145 .unwrap()
146 .iter()
147 .map(|it| it.as_string().unwrap().to_owned())
148 .collect();
149
150 let mut this = Self {
151 raw: configs,
152 dialect: dialect
153 .expect("Dialect is disabled. Please enable the corresponding feature."),
154 extra_config_path,
155 _configs: AHashMap::new(),
156 indentation: indentation.unwrap_or_default(),
157 sql_file_exts,
158 reflow: ReflowConfig::default(),
159 };
160 this.reflow = ReflowConfig::from_fluff_config(&this);
161 this
162 }
163
164 pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
165 self.sql_file_exts = exts;
166 self
167 }
168
169 pub fn from_root(
172 extra_config_path: Option<String>,
173 ignore_local_config: bool,
174 overrides: Option<AHashMap<String, String>>,
175 ) -> Result<FluffConfig, SQLFluffUserError> {
176 let loader = ConfigLoader {};
177 let mut config =
178 loader.load_config_up_to_path(".", extra_config_path.clone(), ignore_local_config);
179
180 if let Some(overrides) = overrides {
181 if let Some(dialect) = overrides.get("dialect") {
182 let core = config
183 .entry("core".into())
184 .or_insert_with(|| Value::Map(AHashMap::new()));
185
186 core.as_map_mut()
187 .unwrap()
188 .insert("dialect".into(), Value::String(dialect.clone().into()));
189 }
190 }
191
192 Ok(FluffConfig::new(config, extra_config_path, None))
193 }
194
195 pub fn from_kwargs(
196 config: Option<FluffConfig>,
197 dialect: Option<Dialect>,
198 rules: Option<Vec<String>>,
199 ) -> Self {
200 if (dialect.is_some() || rules.is_some()) && config.is_some() {
201 panic!(
202 "Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
203 its own dialect and rules."
204 )
205 } else {
206 config.unwrap()
207 }
208 }
209
210 pub fn process_raw_file_for_config(&self, raw_str: &str) {
212 for raw_line in raw_str.lines() {
214 if raw_line.to_string().starts_with("-- sqlfluff") {
215 self.process_inline_config(raw_line)
217 }
218 }
219 }
220
221 pub fn process_inline_config(&self, _config_line: &str) {
223 panic!("Not implemented")
224 }
225
226 pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
228 if self._configs.get("core")?.get("dialect").is_some() {
229 return None;
230 }
231 Some(SQLFluffUserError::new(format!(
235 "No dialect was specified. You must configure a dialect or
236specify one on the command line using --dialect after the
237command. Available dialects: {}",
238 dialect_readout().join(", ").as_str()
239 )))
240 }
241
242 pub fn get_dialect(&self) -> &Dialect {
243 &self.dialect
244 }
245
246 pub fn sql_file_exts(&self) -> &[String] {
247 self.sql_file_exts.as_ref()
248 }
249}
250
251#[derive(Debug, PartialEq, Clone)]
252pub struct FluffConfigIndentation {
253 pub template_blocks_indent: bool,
254}
255
256impl Default for FluffConfigIndentation {
257 fn default() -> Self {
258 Self {
259 template_blocks_indent: true,
260 }
261 }
262}
263
264pub struct ConfigLoader;
265
266impl ConfigLoader {
267 #[allow(unused_variables)]
268 fn iter_config_locations_up_to_path(
269 path: &Path,
270 working_path: Option<&Path>,
271 ignore_local_config: bool,
272 ) -> impl Iterator<Item = PathBuf> {
273 let mut given_path = std::path::absolute(path).unwrap();
274 let working_path = std::env::current_dir().unwrap();
275
276 if !given_path.is_dir() {
277 given_path = given_path.parent().unwrap().into();
278 }
279
280 let common_path = common_path::common_path(&given_path, working_path).unwrap();
281 let mut path_to_visit = common_path;
282
283 let head = Some(given_path.canonicalize().unwrap()).into_iter();
284 let tail = std::iter::from_fn(move || {
285 if path_to_visit != given_path {
286 let path = path_to_visit.canonicalize().unwrap();
287
288 let next_path_to_visit = {
289 let path_to_visit_as_path = path_to_visit.as_path();
291 let given_path_as_path = given_path.as_path();
292
293 match given_path_as_path.strip_prefix(path_to_visit_as_path) {
295 Ok(relative_path) => {
296 if let Some(first_part) = relative_path.components().next() {
298 path_to_visit.join(first_part.as_os_str())
300 } else {
301 path_to_visit.clone()
304 }
305 }
306 Err(_) => {
307 path_to_visit.clone()
311 }
312 }
313 };
314
315 if next_path_to_visit == path_to_visit {
316 return None;
317 }
318
319 path_to_visit = next_path_to_visit;
320
321 Some(path)
322 } else {
323 None
324 }
325 });
326
327 head.chain(tail)
328 }
329
330 pub fn load_config_up_to_path(
331 &self,
332 path: impl AsRef<Path>,
333 extra_config_path: Option<String>,
334 ignore_local_config: bool,
335 ) -> AHashMap<String, Value> {
336 let path = path.as_ref();
337
338 let config_stack = if ignore_local_config {
339 extra_config_path
340 .map(|path| vec![self.load_config_at_path(path)])
341 .unwrap_or_default()
342 } else {
343 let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
344 configs
345 .map(|path| self.load_config_at_path(path))
346 .collect_vec()
347 };
348
349 nested_combine(config_stack)
350 }
351
352 pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> AHashMap<String, Value> {
353 let path = path.as_ref();
354
355 let filename_options = [
356 ".sqlfluff",
358 ".sqruff", ];
360
361 let mut configs = AHashMap::new();
362
363 if path.is_dir() {
364 for fname in filename_options {
365 let path = path.join(fname);
366 if path.exists() {
367 ConfigLoader::load_config_file(path, &mut configs);
368 }
369 }
370 } else if path.is_file() {
371 ConfigLoader::load_config_file(path, &mut configs);
372 };
373
374 configs
375 }
376
377 pub fn from_source(source: &str, path: Option<&Path>) -> AHashMap<String, Value> {
378 let mut configs = AHashMap::new();
379 let elems = ConfigLoader::get_config_elems_from_file(path, Some(source));
380 ConfigLoader::incorporate_vals(&mut configs, elems);
381 configs
382 }
383
384 pub fn load_config_file(path: impl AsRef<Path>, configs: &mut AHashMap<String, Value>) {
385 let elems = ConfigLoader::get_config_elems_from_file(path.as_ref().into(), None);
386 ConfigLoader::incorporate_vals(configs, elems);
387 }
388
389 fn get_config_elems_from_file(
390 config_path: Option<&Path>,
391 config_string: Option<&str>,
392 ) -> Vec<(Vec<String>, Value)> {
393 let mut buff = Vec::new();
394 let mut config = Ini::new();
395
396 let content = match (config_path, config_string) {
397 (None, None) | (Some(_), Some(_)) => {
398 unimplemented!("One of fpath or config_string is required.")
399 }
400 (None, Some(text)) => text.to_owned(),
401 (Some(path), None) => std::fs::read_to_string(path).unwrap(),
402 };
403
404 config.read(content).unwrap();
405
406 for section in config.sections() {
407 let key = if section == "sqlfluff" || section == "sqruff" {
408 vec!["core".to_owned()]
409 } else if let Some(key) = section
410 .strip_prefix("sqlfluff:")
411 .or_else(|| section.strip_prefix("sqruff:"))
412 {
413 key.split(':').map(ToOwned::to_owned).collect()
414 } else {
415 continue;
416 };
417
418 let config_map = config.get_map_ref();
419 if let Some(section) = config_map.get(§ion) {
420 for (name, value) in section {
421 let mut value: Value = value.as_ref().unwrap().parse().unwrap();
422 let name_lowercase = name.to_lowercase();
423
424 if name_lowercase == "load_macros_from_path" {
425 unimplemented!()
426 } else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir")
427 {
428 let path = PathBuf::from(value.as_string().unwrap());
431 if !path.is_absolute() {
432 let config_path = config_path.unwrap().parent().unwrap();
433 let current_dir = std::env::current_dir().unwrap();
435 let config_path = current_dir.join(config_path);
436 let config_path = std::path::absolute(config_path).unwrap();
437 let path = config_path.join(path);
438 let path: String = path.to_string_lossy().into();
439 value = Value::String(path.into());
440 }
441 }
442
443 let mut key = key.clone();
444 key.push(name.clone());
445 buff.push((key, value));
446 }
447 }
448 }
449
450 buff
451 }
452
453 fn incorporate_vals(ctx: &mut AHashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
454 for (path, value) in values {
455 let mut current_map = &mut *ctx;
456 for key in path.iter().take(path.len() - 1) {
457 match current_map
458 .entry(key.to_string())
459 .or_insert_with(|| Value::Map(AHashMap::new()))
460 .as_map_mut()
461 {
462 Some(slot) => current_map = slot,
463 None => panic!("Overriding config value with section! [{path:?}]"),
464 }
465 }
466
467 let last_key = path.last().expect("Expected at least one element in path");
468 current_map.insert(last_key.to_string(), value);
469 }
470 }
471}
472
473#[derive(Debug, Clone, PartialEq, Default, serde::Deserialize)]
474#[serde(untagged)]
475pub enum Value {
476 Int(i32),
477 Bool(bool),
478 Float(f64),
479 String(Box<str>),
480 Map(AHashMap<String, Value>),
481 Array(Vec<Value>),
482 #[default]
483 None,
484}
485
486impl Value {
487 pub fn is_none(&self) -> bool {
488 matches!(self, Value::None)
489 }
490
491 pub fn as_array(&self) -> Option<Vec<Value>> {
492 match self {
493 Self::Array(v) => Some(v.clone()),
494 Self::String(q) => {
495 let xs = q
496 .split(',')
497 .map(|it| Value::String(it.into()))
498 .collect_vec();
499 Some(xs)
500 }
501 Self::Bool(b) => Some(vec![Value::String(b.to_string().into())]),
502 _ => None,
503 }
504 }
505}
506
507impl Index<&str> for Value {
508 type Output = Value;
509
510 fn index(&self, index: &str) -> &Self::Output {
511 match self {
512 Value::Map(map) => map.get(index).unwrap_or(&Value::None),
513 _ => unreachable!(),
514 }
515 }
516}
517
518impl Value {
519 pub fn to_bool(&self) -> bool {
520 match *self {
521 Value::Int(v) => v != 0,
522 Value::Bool(v) => v,
523 Value::Float(v) => v != 0.0,
524 Value::String(ref v) => !v.is_empty(),
525 Value::Map(ref v) => !v.is_empty(),
526 Value::None => false,
527 Value::Array(ref v) => !v.is_empty(),
528 }
529 }
530
531 pub fn map<T>(&self, f: impl Fn(&Self) -> T) -> Option<T> {
532 if self == &Value::None {
533 return None;
534 }
535
536 Some(f(self))
537 }
538 pub fn as_map(&self) -> Option<&AHashMap<String, Value>> {
539 if let Self::Map(map) = self {
540 Some(map)
541 } else {
542 None
543 }
544 }
545
546 pub fn as_map_mut(&mut self) -> Option<&mut AHashMap<String, Value>> {
547 if let Self::Map(map) = self {
548 Some(map)
549 } else {
550 None
551 }
552 }
553
554 pub fn as_int(&self) -> Option<i32> {
555 if let Self::Int(v) = self {
556 Some(*v)
557 } else {
558 None
559 }
560 }
561
562 pub fn as_string(&self) -> Option<&str> {
563 if let Self::String(v) = self {
564 Some(v)
565 } else {
566 None
567 }
568 }
569
570 pub fn as_bool(&self) -> Option<bool> {
571 if let Self::Bool(v) = self {
572 Some(*v)
573 } else {
574 None
575 }
576 }
577}
578
579impl FromStr for Value {
580 type Err = ();
581
582 fn from_str(s: &str) -> Result<Self, Self::Err> {
583 use unicase::UniCase;
584
585 static KEYWORDS: phf::Map<UniCase<&'static str>, Value> = phf::phf_map! {
586 UniCase::ascii("true") => Value::Bool(true),
587 UniCase::ascii("false") => Value::Bool(false),
588 UniCase::ascii("none") => Value::None,
589 };
590
591 if let Ok(value) = s.parse() {
592 return Ok(Value::Int(value));
593 }
594
595 if let Ok(value) = s.parse() {
596 return Ok(Value::Float(value));
597 }
598
599 let key = UniCase::ascii(s);
600 let value = KEYWORDS
601 .get(&key)
602 .cloned()
603 .unwrap_or_else(|| Value::String(Box::from(s)));
604
605 Ok(value)
606 }
607}
608
609fn nested_combine(config_stack: Vec<AHashMap<String, Value>>) -> AHashMap<String, Value> {
610 let capacity = config_stack.len();
611 let mut result = AHashMap::with_capacity(capacity);
612
613 for dict in config_stack {
614 for (key, value) in dict {
615 result.insert(key, value);
616 }
617 }
618
619 result
620}
621
622impl<'a> From<&'a FluffConfig> for Parser<'a> {
623 fn from(config: &'a FluffConfig) -> Self {
624 let dialect = config.get_dialect();
625 let indentation_config = config.raw["indentation"].as_map().unwrap();
626 let indentation_config: AHashMap<_, _> = indentation_config
627 .iter()
628 .map(|(key, value)| (key.clone(), value.to_bool()))
629 .collect();
630 Self::new(dialect, indentation_config)
631 }
632}