Skip to main content

sqruff_lib/core/
config.rs

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
17/// split_comma_separated_string takes a string and splits it on commas and
18/// trims and filters out empty strings.
19pub 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/// The class that actually gets passed around as a config object.
31// TODO This is not a translation that is particularly accurate.
32#[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    /// from_file creates a config object from a file path. The path is used both
113    /// to read the file content and to resolve relative `_path`/`_dir` values.
114    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    /// from_source creates a config object from a string. This is used for testing and for
121    /// loading a config from a string.
122    ///
123    /// The optional_path_specification is used to specify a path to use for relative paths in the
124    /// config. This is useful for testing.
125    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    // TODO This is not a translation that is particularly accurate.
151    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        // Extract dialect-specific configuration section (e.g., [sqruff:dialect:snowflake])
187        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            // Deal with potential ignore & warning parameters
192            ("ignore", "ignore"),
193            ("warnings", "warnings"),
194            ("rules", "rule_allowlist"),
195            // Allowlists and denylistsignore_words
196            ("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    /// Loads a config object just based on the root directory.
241    // TODO This is not a translation that is particularly accurate.
242    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    /// Process a full raw file for inline config and update self.
282    pub fn process_raw_file_for_config(&self, raw_str: &str) {
283        // Scan the raw file for config commands
284        for raw_line in raw_str.lines() {
285            if raw_line.to_string().starts_with("-- sqlfluff") {
286                // Found an in-file config command
287                self.process_inline_config(raw_line)
288            }
289        }
290    }
291
292    /// Process an inline config command and update self.
293    pub fn process_inline_config(&self, _config_line: &str) {
294        panic!("Not implemented")
295    }
296
297    /// Check if the config specifies a dialect, raising an error if not.
298    pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
299        if self._configs.get("core")?.get("dialect").is_some() {
300            return None;
301        }
302        // Get list of available dialects for the error message. We must
303        // import here rather than at file scope in order to avoid a circular
304        // import.
305        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                    // Convert `path_to_visit` & `given_path` to `Path`
361                    let path_to_visit_as_path = path_to_visit.as_path();
362                    let given_path_as_path = given_path.as_path();
363
364                    // Attempt to create a relative path from `given_path` to `path_to_visit`
365                    match given_path_as_path.strip_prefix(path_to_visit_as_path) {
366                        Ok(relative_path) => {
367                            // Get the first component of the relative path
368                            if let Some(first_part) = relative_path.components().next() {
369                                // Combine `path_to_visit` with the first part of the relative path
370                                path_to_visit.join(first_part.as_os_str())
371                            } else {
372                                // If there are no components in the relative path, return
373                                // `path_to_visit`
374                                path_to_visit.clone()
375                            }
376                        }
377                        Err(_) => {
378                            // If `given_path` is not relative to `path_to_visit`, handle the error
379                            // (e.g., return `path_to_visit`)
380                            // This part depends on how you want to handle the error.
381                            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            /* "setup.cfg", "tox.ini", "pep8.ini", */
428            ".sqlfluff",
429            ".sqruff", /* "pyproject.toml" */
430        ];
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(&section) {
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                        // if absolute_path, just keep
500                        // if relative path, make it absolute
501                        let path = PathBuf::from(value.as_string().unwrap());
502                        if !path.is_absolute() {
503                            let config_path = config_path.unwrap().parent().unwrap();
504                            // make config path absolute
505                            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        // Test that [sqruff:dialect:snowflake] section is correctly parsed
575        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        // Verify that the dialect config section is accessible
587        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        // Test that empty [sqruff:dialect:bigquery] section works
603        let config = FluffConfig::from_source(
604            r#"
605[sqruff]
606dialect = bigquery
607
608[sqruff:dialect:bigquery]
609"#,
610            None,
611        );
612
613        // The config should still be valid
614        assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
615    }
616
617    #[test]
618    fn test_dialect_without_config_section() {
619        // Test that a dialect works without a config section
620        let config = FluffConfig::from_source(
621            r#"
622[sqruff]
623dialect = postgres
624"#,
625            None,
626        );
627
628        // The config should still be valid
629        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}