1use std::path::{Path, PathBuf};
2use std::str::FromStr;
3
4use configparser::ini::Ini;
5use hashbrown::HashMap;
6use sqruff_lib_core::dialects::Dialect;
7use sqruff_lib_core::dialects::init::{DialectKind, dialect_readout};
8use sqruff_lib_core::errors::SQLFluffUserError;
9use sqruff_lib_core::parser::{IndentationConfig, Parser};
10pub use sqruff_lib_core::value::Value;
11use sqruff_lib_dialects::kind_to_dialect;
12
13use crate::templaters::TemplaterKind;
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
29fn split_string_or_array(value: &Value) -> Option<Value> {
30 match value {
31 Value::String(raw) => Some(split_comma_separated_string(raw)),
32 Value::Array(values) => Some(Value::Array(
33 values
34 .iter()
35 .map(|value| value.as_string().unwrap())
36 .flat_map(|value| match split_comma_separated_string(value) {
37 Value::Array(values) => values,
38 _ => unreachable!(),
39 })
40 .collect(),
41 )),
42 _ => None,
43 }
44}
45
46#[derive(Debug, PartialEq, Clone)]
49pub struct FluffConfig {
50 pub(crate) indentation: FluffConfigIndentation,
51 pub raw: HashMap<String, Value>,
52 extra_config_path: Option<String>,
53 _configs: HashMap<String, HashMap<String, String>>,
54 pub(crate) dialect: Dialect,
55 sql_file_exts: Vec<String>,
56 reflow: ReflowConfig,
57}
58
59impl Default for FluffConfig {
60 fn default() -> Self {
61 Self::new(<_>::default(), None, None)
62 }
63}
64
65impl FluffConfig {
66 fn configured_dialect_kind_from_raw(configs: &HashMap<String, Value>) -> DialectKind {
67 match configs
68 .get("core")
69 .and_then(|map| map.as_map().unwrap().get("dialect"))
70 {
71 None => DialectKind::default(),
72 Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
73 _value => DialectKind::default(),
74 }
75 }
76
77 fn dialect_section_from_raw(
78 configs: &HashMap<String, Value>,
79 dialect_kind: DialectKind,
80 ) -> Option<&Value> {
81 configs
82 .get("dialect")
83 .and_then(|v| v.as_map())
84 .and_then(|m| m.get(dialect_kind.as_ref()))
85 }
86
87 pub fn override_dialect(&mut self, dialect: DialectKind) -> Result<(), String> {
88 self.dialect = kind_to_dialect(&dialect, None)
89 .ok_or(format!("Invalid dialect: {}", dialect.as_ref()))?;
90 Ok(())
91 }
92
93 pub fn get(&self, key: &str, section: &str) -> &Value {
94 &self.raw[section][key]
95 }
96
97 pub fn reflow(&self) -> &ReflowConfig {
98 &self.reflow
99 }
100
101 fn templater_root_section(&self) -> Option<&HashMap<String, Value>> {
102 self.raw.get("templater").and_then(Value::as_map)
103 }
104
105 pub fn templater_root_value(&self, key: &str) -> Option<&Value> {
106 self.templater_root_section()?.get(key)
107 }
108
109 pub fn templater_section(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
110 self.templater_root_section()?
111 .get(templater.as_str())
112 .and_then(Value::as_map)
113 }
114
115 pub fn templater_value(&self, templater: TemplaterKind, key: &str) -> Option<&Value> {
116 self.templater_section(templater)?.get(key)
117 }
118
119 pub fn templater_context(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
120 self.templater_value(templater, "context")
121 .and_then(Value::as_map)
122 }
123
124 pub fn reload_reflow(&mut self) {
125 self.reflow = ReflowConfig::from_fluff_config(self);
126 }
127
128 pub fn from_file(path: &Path) -> FluffConfig {
131 Self::try_from_file(path).unwrap()
132 }
133
134 pub fn try_from_file(path: &Path) -> Result<FluffConfig, SQLFluffUserError> {
135 let mut configs = HashMap::new();
136 ConfigLoader::try_load_config_file(path, &mut configs)?;
137 Ok(FluffConfig::new(configs, None, None))
138 }
139
140 pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
146 Self::try_from_source(source, optional_path_specification).unwrap()
147 }
148
149 pub fn try_from_source(
150 source: &str,
151 optional_path_specification: Option<&Path>,
152 ) -> Result<FluffConfig, SQLFluffUserError> {
153 let configs = ConfigLoader::try_from_source(source, optional_path_specification)?;
154 Ok(FluffConfig::new(configs, None, None))
155 }
156
157 pub fn get_section(&self, section: &str) -> &HashMap<String, Value> {
158 self.raw[section].as_map().unwrap()
159 }
160
161 pub fn dialect_kind(&self) -> DialectKind {
162 self.dialect.name()
163 }
164
165 pub fn templater_kind(&self) -> Result<TemplaterKind, String> {
166 self.get("templater", "core")
167 .as_string()
168 .map(TemplaterKind::from_name)
169 .transpose()
170 .map(|templater| templater.unwrap_or(TemplaterKind::Raw))
171 }
172
173 pub fn dialect_section(&self, dialect_kind: DialectKind) -> Option<&Value> {
174 Self::dialect_section_from_raw(&self.raw, dialect_kind)
175 }
176
177 pub fn new(
179 configs: HashMap<String, Value>,
180 extra_config_path: Option<String>,
181 indentation: Option<FluffConfigIndentation>,
182 ) -> Self {
183 fn nested_combine(
184 mut a: HashMap<String, Value>,
185 b: HashMap<String, Value>,
186 ) -> HashMap<String, Value> {
187 for (key, value_b) in b {
188 match (a.get(&key), value_b) {
189 (Some(Value::Map(map_a)), Value::Map(map_b)) => {
190 let combined = nested_combine(map_a.clone(), map_b);
191 a.insert(key, Value::Map(combined));
192 }
193 (_, value) => {
194 a.insert(key, value);
195 }
196 }
197 }
198 a
199 }
200
201 let values = ConfigLoader::get_config_elems_from_file(
202 None,
203 include_str!("./default_config.cfg").into(),
204 );
205
206 let mut defaults = HashMap::new();
207 ConfigLoader::incorporate_vals(&mut defaults, values);
208
209 let mut configs = nested_combine(defaults, configs);
210
211 let dialect_kind = Self::configured_dialect_kind_from_raw(&configs);
212
213 let dialect_config = Self::dialect_section_from_raw(&configs, dialect_kind);
215
216 let dialect = kind_to_dialect(&dialect_kind, dialect_config);
217 for (in_key, out_key) in [
218 ("ignore", "ignore"),
220 ("warnings", "warnings"),
221 ("rules", "rule_allowlist"),
222 ("exclude_rules", "rule_denylist"),
224 ] {
225 match configs["core"].as_map().unwrap().get(in_key) {
226 Some(value) if !value.is_none() => {
227 let values = split_string_or_array(value).unwrap();
228
229 configs
230 .get_mut("core")
231 .unwrap()
232 .as_map_mut()
233 .unwrap()
234 .insert(out_key.into(), values);
235 }
236 _ => {}
237 }
238 }
239
240 let sql_file_exts = configs["core"]["sql_file_exts"]
241 .as_array()
242 .unwrap()
243 .iter()
244 .map(|it| it.as_string().unwrap().to_owned())
245 .collect();
246
247 let mut this = Self {
248 raw: configs,
249 dialect: dialect
250 .expect("Dialect is disabled. Please enable the corresponding feature."),
251 extra_config_path,
252 _configs: HashMap::new(),
253 indentation: indentation.unwrap_or_default(),
254 sql_file_exts,
255 reflow: ReflowConfig::default(),
256 };
257 this.reflow = ReflowConfig::from_fluff_config(&this);
258 this
259 }
260
261 pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
262 self.sql_file_exts = exts;
263 self
264 }
265
266 pub fn from_root(
269 extra_config_path: Option<String>,
270 ignore_local_config: bool,
271 overrides: Option<HashMap<String, String>>,
272 ) -> Result<FluffConfig, SQLFluffUserError> {
273 let loader = ConfigLoader {};
274 let mut config = loader.try_load_config_up_to_path(
275 ".",
276 extra_config_path.clone(),
277 ignore_local_config,
278 )?;
279
280 if let Some(overrides) = overrides
281 && let Some(dialect) = overrides.get("dialect")
282 {
283 let core = config
284 .entry("core".into())
285 .or_insert_with(|| Value::Map(HashMap::new()));
286
287 core.as_map_mut()
288 .unwrap()
289 .insert("dialect".into(), Value::String(dialect.clone().into()));
290 }
291
292 Ok(FluffConfig::new(config, extra_config_path, None))
293 }
294
295 pub fn from_kwargs(
296 config: Option<FluffConfig>,
297 dialect: Option<Dialect>,
298 rules: Option<Vec<String>>,
299 ) -> Self {
300 if (dialect.is_some() || rules.is_some()) && config.is_some() {
301 panic!(
302 "Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
303 its own dialect and rules."
304 )
305 } else {
306 config.unwrap()
307 }
308 }
309
310 pub fn process_raw_file_for_config(&self, raw_str: &str) {
312 for raw_line in raw_str.lines() {
314 if raw_line.to_string().starts_with("-- sqlfluff") {
315 self.process_inline_config(raw_line)
317 }
318 }
319 }
320
321 pub fn process_inline_config(&self, _config_line: &str) {
323 panic!("Not implemented")
324 }
325
326 pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
328 if self._configs.get("core")?.get("dialect").is_some() {
329 return None;
330 }
331 Some(SQLFluffUserError::new(format!(
335 "No dialect was specified. You must configure a dialect or
336specify one on the command line using --dialect after the
337command. Available dialects: {}",
338 dialect_readout().join(", ").as_str()
339 )))
340 }
341
342 pub fn get_dialect(&self) -> &Dialect {
343 &self.dialect
344 }
345
346 pub fn sql_file_exts(&self) -> &[String] {
347 self.sql_file_exts.as_ref()
348 }
349}
350
351#[derive(Debug, PartialEq, Clone)]
352pub struct FluffConfigIndentation {
353 pub template_blocks_indent: bool,
354}
355
356impl Default for FluffConfigIndentation {
357 fn default() -> Self {
358 Self {
359 template_blocks_indent: true,
360 }
361 }
362}
363
364pub struct ConfigLoader;
365
366impl ConfigLoader {
367 #[allow(unused_variables)]
368 fn iter_config_locations_up_to_path(
369 path: &Path,
370 working_path: Option<&Path>,
371 ignore_local_config: bool,
372 ) -> impl Iterator<Item = PathBuf> {
373 let mut given_path = std::path::absolute(path).unwrap();
374 let working_path = std::env::current_dir().unwrap();
375
376 if !given_path.is_dir() {
377 given_path = given_path.parent().unwrap().into();
378 }
379
380 let common_path = common_path::common_path(&given_path, working_path).unwrap();
381 let mut path_to_visit = common_path;
382
383 let head = Some(given_path.canonicalize().unwrap()).into_iter();
384 let tail = std::iter::from_fn(move || {
385 if path_to_visit != given_path {
386 let path = path_to_visit.canonicalize().unwrap();
387
388 let next_path_to_visit = {
389 let path_to_visit_as_path = path_to_visit.as_path();
391 let given_path_as_path = given_path.as_path();
392
393 match given_path_as_path.strip_prefix(path_to_visit_as_path) {
395 Ok(relative_path) => {
396 if let Some(first_part) = relative_path.components().next() {
398 path_to_visit.join(first_part.as_os_str())
400 } else {
401 path_to_visit.clone()
404 }
405 }
406 Err(_) => {
407 path_to_visit.clone()
411 }
412 }
413 };
414
415 if next_path_to_visit == path_to_visit {
416 return None;
417 }
418
419 path_to_visit = next_path_to_visit;
420
421 Some(path)
422 } else {
423 None
424 }
425 });
426
427 head.chain(tail)
428 }
429
430 pub fn load_config_up_to_path(
431 &self,
432 path: impl AsRef<Path>,
433 extra_config_path: Option<String>,
434 ignore_local_config: bool,
435 ) -> HashMap<String, Value> {
436 self.try_load_config_up_to_path(path, extra_config_path, ignore_local_config)
437 .unwrap()
438 }
439
440 pub fn try_load_config_up_to_path(
441 &self,
442 path: impl AsRef<Path>,
443 extra_config_path: Option<String>,
444 ignore_local_config: bool,
445 ) -> Result<HashMap<String, Value>, SQLFluffUserError> {
446 let path = path.as_ref();
447
448 let config_stack = if ignore_local_config {
449 if let Some(path) = extra_config_path {
450 vec![self.try_load_config_at_path(path)?]
451 } else {
452 Vec::new()
453 }
454 } else {
455 let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
456 configs
457 .map(|path| self.try_load_config_at_path(path))
458 .collect::<Result<Vec<_>, _>>()?
459 };
460
461 Ok(nested_combine(config_stack))
462 }
463
464 pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> HashMap<String, Value> {
465 self.try_load_config_at_path(path).unwrap()
466 }
467
468 pub fn try_load_config_at_path(
469 &self,
470 path: impl AsRef<Path>,
471 ) -> Result<HashMap<String, Value>, SQLFluffUserError> {
472 let path = path.as_ref();
473
474 let filename_options = [
475 ".sqlfluff",
477 ".sqruff",
478 ".sqruff.ini",
479 "pyproject.toml",
480 "sqruff.toml",
481 ];
482
483 let mut configs = HashMap::new();
484
485 if path.is_dir() {
486 for fname in filename_options {
487 let path = path.join(fname);
488 if path.exists() {
489 ConfigLoader::try_load_config_file(path, &mut configs)?;
490 }
491 }
492 } else if path.is_file() {
493 ConfigLoader::try_load_config_file(path, &mut configs)?;
494 };
495
496 Ok(configs)
497 }
498
499 pub fn from_source(source: &str, path: Option<&Path>) -> HashMap<String, Value> {
500 Self::try_from_source(source, path).unwrap()
501 }
502
503 pub fn try_from_source(
504 source: &str,
505 path: Option<&Path>,
506 ) -> Result<HashMap<String, Value>, SQLFluffUserError> {
507 let mut configs = HashMap::new();
508 let elems = ConfigLoader::try_get_config_elems_from_file(path, Some(source))?;
509 ConfigLoader::incorporate_vals(&mut configs, elems);
510 Ok(configs)
511 }
512
513 pub fn load_config_file(path: impl AsRef<Path>, configs: &mut HashMap<String, Value>) {
514 Self::try_load_config_file(path, configs).unwrap();
515 }
516
517 pub fn try_load_config_file(
518 path: impl AsRef<Path>,
519 configs: &mut HashMap<String, Value>,
520 ) -> Result<(), SQLFluffUserError> {
521 let elems = ConfigLoader::try_get_config_elems_from_file(path.as_ref().into(), None)?;
522 ConfigLoader::incorporate_vals(configs, elems);
523 Ok(())
524 }
525
526 fn get_config_elems_from_file(
527 config_path: Option<&Path>,
528 config_string: Option<&str>,
529 ) -> Vec<(Vec<String>, Value)> {
530 Self::try_get_config_elems_from_file(config_path, config_string).unwrap()
531 }
532
533 fn try_get_config_elems_from_file(
534 config_path: Option<&Path>,
535 config_string: Option<&str>,
536 ) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
537 let content = match (config_path, config_string) {
538 (None, None) => {
539 unimplemented!("One of fpath or config_string is required.")
540 }
541 (_, Some(text)) => text.to_owned(),
542 (Some(path), None) => std::fs::read_to_string(path).map_err(|err| {
543 config_error(config_path, format!("Unable to read config file: {err}"))
544 })?,
545 };
546
547 if is_toml_config(config_path) {
548 return parse_toml_config_elems(&content, config_path);
549 }
550
551 parse_ini_config_elems(&content, config_path)
552 }
553
554 fn incorporate_vals(ctx: &mut HashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
555 for (path, value) in values {
556 let mut current_map = &mut *ctx;
557 for key in path.iter().take(path.len() - 1) {
558 match current_map
559 .entry(key.to_string())
560 .or_insert_with(|| Value::Map(HashMap::new()))
561 .as_map_mut()
562 {
563 Some(slot) => current_map = slot,
564 None => panic!("Overriding config value with section! [{path:?}]"),
565 }
566 }
567
568 let last_key = path.last().expect("Expected at least one element in path");
569 current_map.insert(last_key.to_string(), value);
570 }
571 }
572}
573
574fn is_toml_config(config_path: Option<&Path>) -> bool {
575 config_path.is_some_and(|path| {
576 path.file_name()
577 .and_then(|name| name.to_str())
578 .is_some_and(|name| name == "pyproject.toml" || name.ends_with(".toml"))
579 })
580}
581
582fn config_error(config_path: Option<&Path>, message: impl std::fmt::Display) -> SQLFluffUserError {
583 let location = config_path
584 .map(|path| path.display().to_string())
585 .unwrap_or_else(|| "config source".to_owned());
586 SQLFluffUserError::new(format!("Error loading config from {location}: {}", message))
587}
588
589fn parse_ini_config_elems(
590 content: &str,
591 config_path: Option<&Path>,
592) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
593 let mut buff = Vec::new();
594 let mut config = Ini::new();
595
596 config
597 .read(content.to_owned())
598 .map_err(|err| config_error(config_path, err))?;
599
600 for section in config.sections() {
601 let key = if section == "sqlfluff" || section == "sqruff" {
602 vec!["core".to_owned()]
603 } else if let Some(key) = section
604 .strip_prefix("sqlfluff:")
605 .or_else(|| section.strip_prefix("sqruff:"))
606 {
607 key.split(':').map(ToOwned::to_owned).collect()
608 } else {
609 continue;
610 };
611
612 let config_map = config.get_map_ref();
613 if let Some(section) = config_map.get(§ion) {
614 for (name, value) in section {
615 let mut value: Value = value.as_deref().unwrap_or_default().parse().unwrap();
616 let name_lowercase = name.to_lowercase();
617
618 if name_lowercase == "load_macros_from_path" {
619 unimplemented!()
620 } else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir") {
621 value = resolve_relative_config_path(value, config_path);
622 }
623
624 let mut key = key.clone();
625 key.push(name.clone());
626 buff.push((key, value));
627 }
628 }
629 }
630
631 Ok(buff)
632}
633
634fn parse_toml_config_elems(
635 content: &str,
636 config_path: Option<&Path>,
637) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
638 let root = content
639 .parse::<toml::Table>()
640 .map_err(|err| config_error(config_path, err))?;
641
642 let mut buff = Vec::new();
643
644 for config_root in ["sqlfluff", "sqruff"] {
645 if let Some(table) = root.get(config_root).and_then(toml::Value::as_table) {
646 collect_toml_config_elems(table, Vec::new(), config_path, &mut buff);
647 }
648 }
649
650 if let Some(tool) = root.get("tool").and_then(toml::Value::as_table) {
651 for config_root in ["sqlfluff", "sqruff"] {
652 if let Some(table) = tool.get(config_root).and_then(toml::Value::as_table) {
653 collect_toml_config_elems(table, Vec::new(), config_path, &mut buff);
654 }
655 }
656 }
657
658 Ok(buff)
659}
660
661fn collect_toml_config_elems(
662 table: &toml::Table,
663 section_path: Vec<String>,
664 config_path: Option<&Path>,
665 buff: &mut Vec<(Vec<String>, Value)>,
666) {
667 for (name, value) in table {
668 match value {
669 toml::Value::Table(table) => {
670 let mut section_path = section_path.clone();
671 section_path.push(name.to_owned());
672 collect_toml_config_elems(table, section_path, config_path, buff);
673 }
674 value => {
675 if name == "load_macros_from_path" {
676 unimplemented!()
677 }
678
679 let mut value = toml_value_to_config_value(value);
680 if name.ends_with("_path") || name.ends_with("_dir") {
681 value = resolve_relative_config_path(value, config_path);
682 }
683
684 let key = toml_config_key_path(§ion_path, name);
685 buff.push((key, value));
686 }
687 }
688 }
689}
690
691fn toml_config_key_path(section_path: &[String], key: &str) -> Vec<String> {
692 if section_path.is_empty()
693 || (section_path.len() == 1
694 && section_path
695 .first()
696 .is_some_and(|section| section == "core"))
697 {
698 vec!["core".to_owned(), key.to_owned()]
699 } else if section_path
700 .first()
701 .is_some_and(|section| section == "rules")
702 && section_path.len() > 1
703 {
704 let rest = §ion_path[1..];
705 vec!["rules".to_owned(), rest.join("."), key.to_owned()]
706 } else {
707 section_path
708 .iter()
709 .cloned()
710 .chain(std::iter::once(key.to_owned()))
711 .collect()
712 }
713}
714
715fn toml_value_to_config_value(value: &toml::Value) -> Value {
716 match value {
717 toml::Value::String(value) => Value::String(value.clone().into()),
718 toml::Value::Integer(value) => {
719 Value::Int((*value).try_into().expect("TOML integer out of i32 range"))
720 }
721 toml::Value::Float(value) => Value::Float(*value),
722 toml::Value::Boolean(value) => Value::Bool(*value),
723 toml::Value::Datetime(value) => Value::String(value.to_string().into()),
724 toml::Value::Array(values) => Value::Array(
725 values
726 .iter()
727 .map(toml_value_to_config_value)
728 .collect::<Vec<_>>(),
729 ),
730 toml::Value::Table(values) => Value::Map(
731 values
732 .iter()
733 .map(|(key, value)| (key.clone(), toml_value_to_config_value(value)))
734 .collect(),
735 ),
736 }
737}
738
739fn resolve_relative_config_path(mut value: Value, config_path: Option<&Path>) -> Value {
740 let path = PathBuf::from(value.as_string().unwrap());
741 if !path.is_absolute() {
742 let config_path = config_path.unwrap().parent().unwrap();
743 let current_dir = std::env::current_dir().unwrap();
744 let config_path = current_dir.join(config_path);
745 let config_path = std::path::absolute(config_path).unwrap();
746 let path = config_path.join(path);
747 let path: String = path.to_string_lossy().into();
748 value = Value::String(path.into());
749 }
750 value
751}
752
753fn nested_combine(config_stack: Vec<HashMap<String, Value>>) -> HashMap<String, Value> {
754 let capacity = config_stack.len();
755 let mut result = HashMap::with_capacity(capacity);
756
757 for dict in config_stack {
758 for (key, value) in dict {
759 result.insert(key, value);
760 }
761 }
762
763 result
764}
765
766impl<'a> From<&'a FluffConfig> for Parser<'a> {
767 fn from(config: &'a FluffConfig) -> Self {
768 let dialect = config.get_dialect();
769 let indentation_section = &config.raw["indentation"];
770 let indentation_config =
771 IndentationConfig::from_bool_lookup(|key| indentation_section[key].to_bool());
772 Self::new(dialect, indentation_config)
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use sqruff_lib_core::dialects::init::DialectKind;
780 use std::fs;
781 use std::time::{SystemTime, UNIX_EPOCH};
782
783 fn temp_config_dir(name: &str) -> PathBuf {
784 let nonce = SystemTime::now()
785 .duration_since(UNIX_EPOCH)
786 .unwrap()
787 .as_nanos();
788 let path = std::env::temp_dir().join(format!(
789 "sqruff-config-{name}-{}-{nonce}",
790 std::process::id()
791 ));
792 fs::create_dir_all(&path).unwrap();
793 path
794 }
795
796 #[test]
797 fn test_dialect_config_section_parsing() {
798 let config = FluffConfig::from_source(
800 r#"
801[sqruff]
802dialect = snowflake
803
804[sqruff:dialect:snowflake]
805some_option = value
806"#,
807 None,
808 );
809
810 let dialect_section = config.raw.get("dialect");
812 assert!(dialect_section.is_some());
813
814 let snowflake_config = dialect_section.unwrap().as_map().unwrap().get("snowflake");
815 assert!(snowflake_config.is_some());
816
817 let snowflake_map = snowflake_config.unwrap().as_map().unwrap();
818 assert_eq!(
819 snowflake_map.get("some_option").unwrap().as_string(),
820 Some("value")
821 );
822 }
823
824 #[test]
825 fn test_dialect_config_empty_section() {
826 let config = FluffConfig::from_source(
828 r#"
829[sqruff]
830dialect = bigquery
831
832[sqruff:dialect:bigquery]
833"#,
834 None,
835 );
836
837 assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
839 }
840
841 #[test]
842 fn test_dialect_without_config_section() {
843 let config = FluffConfig::from_source(
845 r#"
846[sqruff]
847dialect = postgres
848"#,
849 None,
850 );
851
852 assert_eq!(config.get_dialect().name, DialectKind::Postgres);
854 }
855
856 #[test]
857 fn test_templater_kind_defaults_to_raw() {
858 let config = FluffConfig::from_source("", None);
859 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Raw);
860 }
861
862 #[test]
863 fn test_templater_kind_parses_placeholder() {
864 let config = FluffConfig::from_source(
865 r#"
866[sqruff]
867templater = placeholder
868"#,
869 None,
870 );
871
872 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
873 }
874
875 #[test]
876 fn test_templater_section_uses_typed_kind() {
877 let config = FluffConfig::from_source(
878 r#"
879[sqruff]
880templater = placeholder
881
882[sqruff:templater:placeholder]
883param_style = colon
884"#,
885 None,
886 );
887
888 let section = config
889 .templater_section(TemplaterKind::Placeholder)
890 .unwrap();
891 assert_eq!(
892 section.get("param_style").unwrap().as_string(),
893 Some("colon")
894 );
895 }
896
897 #[test]
898 fn test_sqruff_toml_parses_sqlfluff_root_config() {
899 let config = FluffConfig::from_source(
900 r#"
901[sqlfluff]
902dialect = "postgres"
903templater = "placeholder"
904max_line_length = 60
905
906[sqlfluff.indentation]
907indented_joins = false
908
909[sqlfluff.layout.type.comma]
910line_position = "trailing"
911"#,
912 Some(Path::new("sqruff.toml")),
913 );
914
915 assert_eq!(config.get_dialect().name, DialectKind::Postgres);
916 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
917 assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(60));
918 assert_eq!(
919 config.raw["indentation"]["indented_joins"].as_bool(),
920 Some(false)
921 );
922 assert_eq!(
923 config.raw["layout"]["type"]["comma"]["line_position"].as_string(),
924 Some("trailing")
925 );
926 }
927
928 #[test]
929 fn test_pyproject_toml_parses_tool_sqlfluff_config() {
930 let config = FluffConfig::from_source(
931 r#"
932[project]
933name = "example"
934
935[tool.sqlfluff.core]
936dialect = "postgres"
937templater = "placeholder"
938max_line_length = 42
939exclude_rules = ["CP01", "LT05"]
940
941[tool.sqlfluff.templater.placeholder]
942param_style = "colon"
943
944[tool.sqlfluff.rules.capitalisation.keywords]
945capitalisation_policy = "upper"
946"#,
947 Some(Path::new("pyproject.toml")),
948 );
949
950 assert_eq!(config.get_dialect().name, DialectKind::Postgres);
951 assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
952 assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(42));
953 assert_eq!(
954 config.raw["core"]["rule_denylist"].as_array().unwrap(),
955 vec![Value::String("CP01".into()), Value::String("LT05".into())]
956 );
957 assert_eq!(
958 config
959 .templater_section(TemplaterKind::Placeholder)
960 .unwrap()
961 .get("param_style")
962 .unwrap()
963 .as_string(),
964 Some("colon")
965 );
966 assert_eq!(
967 config.raw["rules"]["capitalisation.keywords"]["capitalisation_policy"].as_string(),
968 Some("upper")
969 );
970 }
971
972 #[test]
973 fn test_load_config_at_path_discovers_toml_configs() {
974 let dir = temp_config_dir("toml-discovery");
975 fs::write(
976 dir.join("pyproject.toml"),
977 r#"
978[tool.sqlfluff.core]
979max_line_length = 41
980"#,
981 )
982 .unwrap();
983 fs::write(
984 dir.join("sqruff.toml"),
985 r#"
986[sqruff]
987max_line_length = 39
988"#,
989 )
990 .unwrap();
991
992 let config = FluffConfig::new(ConfigLoader {}.load_config_at_path(&dir), None, None);
993 fs::remove_dir_all(&dir).unwrap();
994
995 assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(39));
996 }
997
998 #[test]
999 fn test_load_config_at_path_discovers_sqruff_ini() {
1000 let dir = temp_config_dir("sqruff-ini");
1001 fs::write(
1002 dir.join(".sqruff.ini"),
1003 r#"
1004[sqruff]
1005max_line_length = 44
1006"#,
1007 )
1008 .unwrap();
1009
1010 let config = FluffConfig::new(ConfigLoader {}.load_config_at_path(&dir), None, None);
1011 fs::remove_dir_all(&dir).unwrap();
1012
1013 assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(44));
1014 }
1015
1016 #[test]
1017 fn test_try_from_source_invalid_toml_returns_error() {
1018 let err = FluffConfig::try_from_source(
1019 "[sqlfluff]\ndialect = \"ansi",
1020 Some(Path::new("sqruff.toml")),
1021 )
1022 .unwrap_err();
1023
1024 assert!(
1025 err.to_string()
1026 .contains("Error loading config from sqruff.toml")
1027 );
1028 }
1029
1030 #[cfg(feature = "python")]
1031 #[test]
1032 fn test_templater_context_uses_typed_kind() {
1033 let config = FluffConfig::from_source(
1034 r#"
1035[sqruff]
1036templater = python
1037
1038[sqruff:templater:python:context]
1039blah = foo
1040"#,
1041 None,
1042 );
1043
1044 let context = config.templater_context(TemplaterKind::Python).unwrap();
1045 assert_eq!(context.get("blah").unwrap().as_string(), Some("foo"));
1046 }
1047}