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