1use std::path::{Path, PathBuf};
2use std::str::FromStr;
3
4use configparser::ini::Ini;
5use hashbrown::HashMap;
6use itertools::Itertools;
7use sqruff_lib_core::dialects::Dialect;
8use sqruff_lib_core::dialects::init::{DialectKind, dialect_readout};
9use sqruff_lib_core::errors::SQLFluffUserError;
10use sqruff_lib_core::parser::{IndentationConfig, Parser};
11pub use sqruff_lib_core::value::Value;
12use sqruff_lib_dialects::kind_to_dialect;
13
14use crate::templaters::TemplaterKind;
15use crate::utils::reflow::config::ReflowConfig;
16
17pub fn split_comma_separated_string(raw_str: &str) -> Value {
20 let values = raw_str
21 .split(',')
22 .filter_map(|x| {
23 let trimmed = x.trim();
24 (!trimmed.is_empty()).then(|| Value::String(trimmed.into()))
25 })
26 .collect();
27 Value::Array(values)
28}
29
30#[derive(Debug, PartialEq, Clone)]
33pub struct FluffConfig {
34 pub(crate) indentation: FluffConfigIndentation,
35 pub raw: HashMap<String, Value>,
36 extra_config_path: Option<String>,
37 _configs: HashMap<String, HashMap<String, String>>,
38 pub(crate) dialect: Dialect,
39 sql_file_exts: Vec<String>,
40 reflow: ReflowConfig,
41}
42
43impl Default for FluffConfig {
44 fn default() -> Self {
45 Self::new(<_>::default(), None, None)
46 }
47}
48
49impl FluffConfig {
50 fn configured_dialect_kind_from_raw(configs: &HashMap<String, Value>) -> DialectKind {
51 match configs
52 .get("core")
53 .and_then(|map| map.as_map().unwrap().get("dialect"))
54 {
55 None => DialectKind::default(),
56 Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
57 _value => DialectKind::default(),
58 }
59 }
60
61 fn dialect_section_from_raw(
62 configs: &HashMap<String, Value>,
63 dialect_kind: DialectKind,
64 ) -> Option<&Value> {
65 configs
66 .get("dialect")
67 .and_then(|v| v.as_map())
68 .and_then(|m| m.get(dialect_kind.as_ref()))
69 }
70
71 pub fn override_dialect(&mut self, dialect: DialectKind) -> Result<(), String> {
72 self.dialect = kind_to_dialect(&dialect, None)
73 .ok_or(format!("Invalid dialect: {}", dialect.as_ref()))?;
74 Ok(())
75 }
76
77 pub fn get(&self, key: &str, section: &str) -> &Value {
78 &self.raw[section][key]
79 }
80
81 pub fn reflow(&self) -> &ReflowConfig {
82 &self.reflow
83 }
84
85 fn templater_root_section(&self) -> Option<&HashMap<String, Value>> {
86 self.raw.get("templater").and_then(Value::as_map)
87 }
88
89 pub fn templater_root_value(&self, key: &str) -> Option<&Value> {
90 self.templater_root_section()?.get(key)
91 }
92
93 pub fn templater_section(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
94 self.templater_root_section()?
95 .get(templater.as_str())
96 .and_then(Value::as_map)
97 }
98
99 pub fn templater_value(&self, templater: TemplaterKind, key: &str) -> Option<&Value> {
100 self.templater_section(templater)?.get(key)
101 }
102
103 pub fn templater_context(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
104 self.templater_value(templater, "context")
105 .and_then(Value::as_map)
106 }
107
108 pub fn reload_reflow(&mut self) {
109 self.reflow = ReflowConfig::from_fluff_config(self);
110 }
111
112 pub fn from_file(path: &Path) -> FluffConfig {
115 let mut configs = HashMap::new();
116 ConfigLoader::load_config_file(path, &mut configs);
117 FluffConfig::new(configs, None, None)
118 }
119
120 pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
126 let configs = ConfigLoader::from_source(source, optional_path_specification);
127 FluffConfig::new(configs, None, None)
128 }
129
130 pub fn get_section(&self, section: &str) -> &HashMap<String, Value> {
131 self.raw[section].as_map().unwrap()
132 }
133
134 pub fn dialect_kind(&self) -> DialectKind {
135 self.dialect.name()
136 }
137
138 pub fn templater_kind(&self) -> Result<TemplaterKind, String> {
139 self.get("templater", "core")
140 .as_string()
141 .map(TemplaterKind::from_name)
142 .transpose()
143 .map(|templater| templater.unwrap_or(TemplaterKind::Raw))
144 }
145
146 pub fn dialect_section(&self, dialect_kind: DialectKind) -> Option<&Value> {
147 Self::dialect_section_from_raw(&self.raw, dialect_kind)
148 }
149
150 pub fn new(
152 configs: HashMap<String, Value>,
153 extra_config_path: Option<String>,
154 indentation: Option<FluffConfigIndentation>,
155 ) -> Self {
156 fn nested_combine(
157 mut a: HashMap<String, Value>,
158 b: HashMap<String, Value>,
159 ) -> HashMap<String, Value> {
160 for (key, value_b) in b {
161 match (a.get(&key), value_b) {
162 (Some(Value::Map(map_a)), Value::Map(map_b)) => {
163 let combined = nested_combine(map_a.clone(), map_b);
164 a.insert(key, Value::Map(combined));
165 }
166 (_, value) => {
167 a.insert(key, value);
168 }
169 }
170 }
171 a
172 }
173
174 let values = ConfigLoader::get_config_elems_from_file(
175 None,
176 include_str!("./default_config.cfg").into(),
177 );
178
179 let mut defaults = HashMap::new();
180 ConfigLoader::incorporate_vals(&mut defaults, values);
181
182 let mut configs = nested_combine(defaults, configs);
183
184 let dialect_kind = Self::configured_dialect_kind_from_raw(&configs);
185
186 let dialect_config = Self::dialect_section_from_raw(&configs, dialect_kind);
188
189 let dialect = kind_to_dialect(&dialect_kind, dialect_config);
190 for (in_key, out_key) in [
191 ("ignore", "ignore"),
193 ("warnings", "warnings"),
194 ("rules", "rule_allowlist"),
195 ("exclude_rules", "rule_denylist"),
197 ] {
198 match configs["core"].as_map().unwrap().get(in_key) {
199 Some(value) if !value.is_none() => {
200 let string = value.as_string().unwrap();
201 let values = split_comma_separated_string(string);
202
203 configs
204 .get_mut("core")
205 .unwrap()
206 .as_map_mut()
207 .unwrap()
208 .insert(out_key.into(), values);
209 }
210 _ => {}
211 }
212 }
213
214 let sql_file_exts = configs["core"]["sql_file_exts"]
215 .as_array()
216 .unwrap()
217 .iter()
218 .map(|it| it.as_string().unwrap().to_owned())
219 .collect();
220
221 let mut this = Self {
222 raw: configs,
223 dialect: dialect
224 .expect("Dialect is disabled. Please enable the corresponding feature."),
225 extra_config_path,
226 _configs: HashMap::new(),
227 indentation: indentation.unwrap_or_default(),
228 sql_file_exts,
229 reflow: ReflowConfig::default(),
230 };
231 this.reflow = ReflowConfig::from_fluff_config(&this);
232 this
233 }
234
235 pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
236 self.sql_file_exts = exts;
237 self
238 }
239
240 pub fn from_root(
243 extra_config_path: Option<String>,
244 ignore_local_config: bool,
245 overrides: Option<HashMap<String, String>>,
246 ) -> Result<FluffConfig, SQLFluffUserError> {
247 let loader = ConfigLoader {};
248 let mut config =
249 loader.load_config_up_to_path(".", extra_config_path.clone(), ignore_local_config);
250
251 if let Some(overrides) = overrides
252 && let Some(dialect) = overrides.get("dialect")
253 {
254 let core = config
255 .entry("core".into())
256 .or_insert_with(|| Value::Map(HashMap::new()));
257
258 core.as_map_mut()
259 .unwrap()
260 .insert("dialect".into(), Value::String(dialect.clone().into()));
261 }
262
263 Ok(FluffConfig::new(config, extra_config_path, None))
264 }
265
266 pub fn from_kwargs(
267 config: Option<FluffConfig>,
268 dialect: Option<Dialect>,
269 rules: Option<Vec<String>>,
270 ) -> Self {
271 if (dialect.is_some() || rules.is_some()) && config.is_some() {
272 panic!(
273 "Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
274 its own dialect and rules."
275 )
276 } else {
277 config.unwrap()
278 }
279 }
280
281 pub fn process_raw_file_for_config(&self, raw_str: &str) {
283 for raw_line in raw_str.lines() {
285 if raw_line.to_string().starts_with("-- sqlfluff") {
286 self.process_inline_config(raw_line)
288 }
289 }
290 }
291
292 pub fn process_inline_config(&self, _config_line: &str) {
294 panic!("Not implemented")
295 }
296
297 pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
299 if self._configs.get("core")?.get("dialect").is_some() {
300 return None;
301 }
302 Some(SQLFluffUserError::new(format!(
306 "No dialect was specified. You must configure a dialect or
307specify one on the command line using --dialect after the
308command. Available dialects: {}",
309 dialect_readout().join(", ").as_str()
310 )))
311 }
312
313 pub fn get_dialect(&self) -> &Dialect {
314 &self.dialect
315 }
316
317 pub fn sql_file_exts(&self) -> &[String] {
318 self.sql_file_exts.as_ref()
319 }
320}
321
322#[derive(Debug, PartialEq, Clone)]
323pub struct FluffConfigIndentation {
324 pub template_blocks_indent: bool,
325}
326
327impl Default for FluffConfigIndentation {
328 fn default() -> Self {
329 Self {
330 template_blocks_indent: true,
331 }
332 }
333}
334
335pub struct ConfigLoader;
336
337impl ConfigLoader {
338 #[allow(unused_variables)]
339 fn iter_config_locations_up_to_path(
340 path: &Path,
341 working_path: Option<&Path>,
342 ignore_local_config: bool,
343 ) -> impl Iterator<Item = PathBuf> {
344 let mut given_path = std::path::absolute(path).unwrap();
345 let working_path = std::env::current_dir().unwrap();
346
347 if !given_path.is_dir() {
348 given_path = given_path.parent().unwrap().into();
349 }
350
351 let common_path = common_path::common_path(&given_path, working_path).unwrap();
352 let mut path_to_visit = common_path;
353
354 let head = Some(given_path.canonicalize().unwrap()).into_iter();
355 let tail = std::iter::from_fn(move || {
356 if path_to_visit != given_path {
357 let path = path_to_visit.canonicalize().unwrap();
358
359 let next_path_to_visit = {
360 let path_to_visit_as_path = path_to_visit.as_path();
362 let given_path_as_path = given_path.as_path();
363
364 match given_path_as_path.strip_prefix(path_to_visit_as_path) {
366 Ok(relative_path) => {
367 if let Some(first_part) = relative_path.components().next() {
369 path_to_visit.join(first_part.as_os_str())
371 } else {
372 path_to_visit.clone()
375 }
376 }
377 Err(_) => {
378 path_to_visit.clone()
382 }
383 }
384 };
385
386 if next_path_to_visit == path_to_visit {
387 return None;
388 }
389
390 path_to_visit = next_path_to_visit;
391
392 Some(path)
393 } else {
394 None
395 }
396 });
397
398 head.chain(tail)
399 }
400
401 pub fn load_config_up_to_path(
402 &self,
403 path: impl AsRef<Path>,
404 extra_config_path: Option<String>,
405 ignore_local_config: bool,
406 ) -> HashMap<String, Value> {
407 let path = path.as_ref();
408
409 let config_stack = if ignore_local_config {
410 extra_config_path
411 .map(|path| vec![self.load_config_at_path(path)])
412 .unwrap_or_default()
413 } else {
414 let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
415 configs
416 .map(|path| self.load_config_at_path(path))
417 .collect_vec()
418 };
419
420 nested_combine(config_stack)
421 }
422
423 pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> HashMap<String, Value> {
424 let path = path.as_ref();
425
426 let filename_options = [
427 ".sqlfluff",
429 ".sqruff", ];
431
432 let mut configs = HashMap::new();
433
434 if path.is_dir() {
435 for fname in filename_options {
436 let path = path.join(fname);
437 if path.exists() {
438 ConfigLoader::load_config_file(path, &mut configs);
439 }
440 }
441 } else if path.is_file() {
442 ConfigLoader::load_config_file(path, &mut configs);
443 };
444
445 configs
446 }
447
448 pub fn from_source(source: &str, path: Option<&Path>) -> HashMap<String, Value> {
449 let mut configs = HashMap::new();
450 let elems = ConfigLoader::get_config_elems_from_file(path, Some(source));
451 ConfigLoader::incorporate_vals(&mut configs, elems);
452 configs
453 }
454
455 pub fn load_config_file(path: impl AsRef<Path>, configs: &mut HashMap<String, Value>) {
456 let elems = ConfigLoader::get_config_elems_from_file(path.as_ref().into(), None);
457 ConfigLoader::incorporate_vals(configs, elems);
458 }
459
460 fn get_config_elems_from_file(
461 config_path: Option<&Path>,
462 config_string: Option<&str>,
463 ) -> Vec<(Vec<String>, Value)> {
464 let mut buff = Vec::new();
465 let mut config = Ini::new();
466
467 let content = match (config_path, config_string) {
468 (None, None) | (Some(_), Some(_)) => {
469 unimplemented!("One of fpath or config_string is required.")
470 }
471 (None, Some(text)) => text.to_owned(),
472 (Some(path), None) => std::fs::read_to_string(path).unwrap(),
473 };
474
475 config.read(content).unwrap();
476
477 for section in config.sections() {
478 let key = if section == "sqlfluff" || section == "sqruff" {
479 vec!["core".to_owned()]
480 } else if let Some(key) = section
481 .strip_prefix("sqlfluff:")
482 .or_else(|| section.strip_prefix("sqruff:"))
483 {
484 key.split(':').map(ToOwned::to_owned).collect()
485 } else {
486 continue;
487 };
488
489 let config_map = config.get_map_ref();
490 if let Some(section) = config_map.get(§ion) {
491 for (name, value) in section {
492 let mut value: Value = value.as_deref().unwrap_or_default().parse().unwrap();
493 let name_lowercase = name.to_lowercase();
494
495 if name_lowercase == "load_macros_from_path" {
496 unimplemented!()
497 } else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir")
498 {
499 let path = PathBuf::from(value.as_string().unwrap());
502 if !path.is_absolute() {
503 let config_path = config_path.unwrap().parent().unwrap();
504 let current_dir = std::env::current_dir().unwrap();
506 let config_path = current_dir.join(config_path);
507 let config_path = std::path::absolute(config_path).unwrap();
508 let path = config_path.join(path);
509 let path: String = path.to_string_lossy().into();
510 value = Value::String(path.into());
511 }
512 }
513
514 let mut key = key.clone();
515 key.push(name.clone());
516 buff.push((key, value));
517 }
518 }
519 }
520
521 buff
522 }
523
524 fn incorporate_vals(ctx: &mut HashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
525 for (path, value) in values {
526 let mut current_map = &mut *ctx;
527 for key in path.iter().take(path.len() - 1) {
528 match current_map
529 .entry(key.to_string())
530 .or_insert_with(|| Value::Map(HashMap::new()))
531 .as_map_mut()
532 {
533 Some(slot) => current_map = slot,
534 None => panic!("Overriding config value with section! [{path:?}]"),
535 }
536 }
537
538 let last_key = path.last().expect("Expected at least one element in path");
539 current_map.insert(last_key.to_string(), value);
540 }
541 }
542}
543
544fn nested_combine(config_stack: Vec<HashMap<String, Value>>) -> HashMap<String, Value> {
545 let capacity = config_stack.len();
546 let mut result = HashMap::with_capacity(capacity);
547
548 for dict in config_stack {
549 for (key, value) in dict {
550 result.insert(key, value);
551 }
552 }
553
554 result
555}
556
557impl<'a> From<&'a FluffConfig> for Parser<'a> {
558 fn from(config: &'a FluffConfig) -> Self {
559 let dialect = config.get_dialect();
560 let indentation_section = &config.raw["indentation"];
561 let indentation_config =
562 IndentationConfig::from_bool_lookup(|key| indentation_section[key].to_bool());
563 Self::new(dialect, indentation_config)
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use super::*;
570 use sqruff_lib_core::dialects::init::DialectKind;
571
572 #[test]
573 fn test_dialect_config_section_parsing() {
574 let config = FluffConfig::from_source(
576 r#"
577[sqruff]
578dialect = snowflake
579
580[sqruff:dialect:snowflake]
581some_option = value
582"#,
583 None,
584 );
585
586 let dialect_section = config.raw.get("dialect");
588 assert!(dialect_section.is_some());
589
590 let snowflake_config = dialect_section.unwrap().as_map().unwrap().get("snowflake");
591 assert!(snowflake_config.is_some());
592
593 let snowflake_map = snowflake_config.unwrap().as_map().unwrap();
594 assert_eq!(
595 snowflake_map.get("some_option").unwrap().as_string(),
596 Some("value")
597 );
598 }
599
600 #[test]
601 fn test_dialect_config_empty_section() {
602 let config = FluffConfig::from_source(
604 r#"
605[sqruff]
606dialect = bigquery
607
608[sqruff:dialect:bigquery]
609"#,
610 None,
611 );
612
613 assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
615 }
616
617 #[test]
618 fn test_dialect_without_config_section() {
619 let config = FluffConfig::from_source(
621 r#"
622[sqruff]
623dialect = postgres
624"#,
625 None,
626 );
627
628 assert_eq!(config.get_dialect().name, DialectKind::Postgres);
630 }
631
632 #[test]
633 fn test_templater_kind_defaults_to_raw() {
634 let config = FluffConfig::from_source("", None);
635 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Raw);
636 }
637
638 #[test]
639 fn test_templater_kind_parses_placeholder() {
640 let config = FluffConfig::from_source(
641 r#"
642[sqruff]
643templater = placeholder
644"#,
645 None,
646 );
647
648 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
649 }
650
651 #[test]
652 fn test_templater_section_uses_typed_kind() {
653 let config = FluffConfig::from_source(
654 r#"
655[sqruff]
656templater = placeholder
657
658[sqruff:templater:placeholder]
659param_style = colon
660"#,
661 None,
662 );
663
664 let section = config
665 .templater_section(TemplaterKind::Placeholder)
666 .unwrap();
667 assert_eq!(
668 section.get("param_style").unwrap().as_string(),
669 Some("colon")
670 );
671 }
672
673 #[cfg(feature = "python")]
674 #[test]
675 fn test_templater_context_uses_typed_kind() {
676 let config = FluffConfig::from_source(
677 r#"
678[sqruff]
679templater = python
680
681[sqruff:templater:python:context]
682blah = foo
683"#,
684 None,
685 );
686
687 let context = config.templater_context(TemplaterKind::Python).unwrap();
688 assert_eq!(context.get("blah").unwrap().as_string(), Some("foo"));
689 }
690}