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::utils::reflow::config::ReflowConfig;
15
16/// split_comma_separated_string takes a string and splits it on commas and
17/// trims and filters out empty strings.
18pub 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/// The class that actually gets passed around as a config object.
30// TODO This is not a translation that is particularly accurate.
31#[derive(Debug, PartialEq, Clone)]
32pub struct FluffConfig {
33    pub(crate) indentation: FluffConfigIndentation,
34    pub raw: HashMap<String, Value>,
35    extra_config_path: Option<String>,
36    _configs: HashMap<String, HashMap<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 = kind_to_dialect(&dialect, None)
51            .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    /// from_file creates a config object from a file path. The path is used both
68    /// to read the file content and to resolve relative `_path`/`_dir` values.
69    pub fn from_file(path: &Path) -> FluffConfig {
70        let mut configs = HashMap::new();
71        ConfigLoader::load_config_file(path, &mut configs);
72        FluffConfig::new(configs, None, None)
73    }
74
75    /// from_source creates a config object from a string. This is used for testing and for
76    /// loading a config from a string.
77    ///
78    /// The optional_path_specification is used to specify a path to use for relative paths in the
79    /// config. This is useful for testing.
80    pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
81        let configs = ConfigLoader::from_source(source, optional_path_specification);
82        FluffConfig::new(configs, None, None)
83    }
84
85    pub fn get_section(&self, section: &str) -> &HashMap<String, Value> {
86        self.raw[section].as_map().unwrap()
87    }
88
89    // TODO This is not a translation that is particularly accurate.
90    pub fn new(
91        configs: HashMap<String, Value>,
92        extra_config_path: Option<String>,
93        indentation: Option<FluffConfigIndentation>,
94    ) -> Self {
95        fn nested_combine(
96            mut a: HashMap<String, Value>,
97            b: HashMap<String, Value>,
98        ) -> HashMap<String, Value> {
99            for (key, value_b) in b {
100                match (a.get(&key), value_b) {
101                    (Some(Value::Map(map_a)), Value::Map(map_b)) => {
102                        let combined = nested_combine(map_a.clone(), map_b);
103                        a.insert(key, Value::Map(combined));
104                    }
105                    (_, value) => {
106                        a.insert(key, value);
107                    }
108                }
109            }
110            a
111        }
112
113        let values = ConfigLoader::get_config_elems_from_file(
114            None,
115            include_str!("./default_config.cfg").into(),
116        );
117
118        let mut defaults = HashMap::new();
119        ConfigLoader::incorporate_vals(&mut defaults, values);
120
121        let mut configs = nested_combine(defaults, configs);
122
123        let dialect_kind = match configs
124            .get("core")
125            .and_then(|map| map.as_map().unwrap().get("dialect"))
126        {
127            None => DialectKind::default(),
128            Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
129            _value => DialectKind::default(),
130        };
131
132        // Extract dialect-specific configuration section (e.g., [sqruff:dialect:snowflake])
133        let dialect_config = configs
134            .get("dialect")
135            .and_then(|v| v.as_map())
136            .and_then(|m| m.get(dialect_kind.as_ref()));
137
138        let dialect = kind_to_dialect(&dialect_kind, dialect_config);
139        for (in_key, out_key) in [
140            // Deal with potential ignore & warning parameters
141            ("ignore", "ignore"),
142            ("warnings", "warnings"),
143            ("rules", "rule_allowlist"),
144            // Allowlists and denylistsignore_words
145            ("exclude_rules", "rule_denylist"),
146        ] {
147            match configs["core"].as_map().unwrap().get(in_key) {
148                Some(value) if !value.is_none() => {
149                    let string = value.as_string().unwrap();
150                    let values = split_comma_separated_string(string);
151
152                    configs
153                        .get_mut("core")
154                        .unwrap()
155                        .as_map_mut()
156                        .unwrap()
157                        .insert(out_key.into(), values);
158                }
159                _ => {}
160            }
161        }
162
163        let sql_file_exts = configs["core"]["sql_file_exts"]
164            .as_array()
165            .unwrap()
166            .iter()
167            .map(|it| it.as_string().unwrap().to_owned())
168            .collect();
169
170        let mut this = Self {
171            raw: configs,
172            dialect: dialect
173                .expect("Dialect is disabled. Please enable the corresponding feature."),
174            extra_config_path,
175            _configs: HashMap::new(),
176            indentation: indentation.unwrap_or_default(),
177            sql_file_exts,
178            reflow: ReflowConfig::default(),
179        };
180        this.reflow = ReflowConfig::from_fluff_config(&this);
181        this
182    }
183
184    pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
185        self.sql_file_exts = exts;
186        self
187    }
188
189    /// Loads a config object just based on the root directory.
190    // TODO This is not a translation that is particularly accurate.
191    pub fn from_root(
192        extra_config_path: Option<String>,
193        ignore_local_config: bool,
194        overrides: Option<HashMap<String, String>>,
195    ) -> Result<FluffConfig, SQLFluffUserError> {
196        let loader = ConfigLoader {};
197        let mut config =
198            loader.load_config_up_to_path(".", extra_config_path.clone(), ignore_local_config);
199
200        if let Some(overrides) = overrides
201            && let Some(dialect) = overrides.get("dialect")
202        {
203            let core = config
204                .entry("core".into())
205                .or_insert_with(|| Value::Map(HashMap::new()));
206
207            core.as_map_mut()
208                .unwrap()
209                .insert("dialect".into(), Value::String(dialect.clone().into()));
210        }
211
212        Ok(FluffConfig::new(config, extra_config_path, None))
213    }
214
215    pub fn from_kwargs(
216        config: Option<FluffConfig>,
217        dialect: Option<Dialect>,
218        rules: Option<Vec<String>>,
219    ) -> Self {
220        if (dialect.is_some() || rules.is_some()) && config.is_some() {
221            panic!(
222                "Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
223                 its own dialect and rules."
224            )
225        } else {
226            config.unwrap()
227        }
228    }
229
230    /// Process a full raw file for inline config and update self.
231    pub fn process_raw_file_for_config(&self, raw_str: &str) {
232        // Scan the raw file for config commands
233        for raw_line in raw_str.lines() {
234            if raw_line.to_string().starts_with("-- sqlfluff") {
235                // Found an in-file config command
236                self.process_inline_config(raw_line)
237            }
238        }
239    }
240
241    /// Process an inline config command and update self.
242    pub fn process_inline_config(&self, _config_line: &str) {
243        panic!("Not implemented")
244    }
245
246    /// Check if the config specifies a dialect, raising an error if not.
247    pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
248        if self._configs.get("core")?.get("dialect").is_some() {
249            return None;
250        }
251        // Get list of available dialects for the error message. We must
252        // import here rather than at file scope in order to avoid a circular
253        // import.
254        Some(SQLFluffUserError::new(format!(
255            "No dialect was specified. You must configure a dialect or
256specify one on the command line using --dialect after the
257command. Available dialects: {}",
258            dialect_readout().join(", ").as_str()
259        )))
260    }
261
262    pub fn get_dialect(&self) -> &Dialect {
263        &self.dialect
264    }
265
266    pub fn sql_file_exts(&self) -> &[String] {
267        self.sql_file_exts.as_ref()
268    }
269}
270
271#[derive(Debug, PartialEq, Clone)]
272pub struct FluffConfigIndentation {
273    pub template_blocks_indent: bool,
274}
275
276impl Default for FluffConfigIndentation {
277    fn default() -> Self {
278        Self {
279            template_blocks_indent: true,
280        }
281    }
282}
283
284pub struct ConfigLoader;
285
286impl ConfigLoader {
287    #[allow(unused_variables)]
288    fn iter_config_locations_up_to_path(
289        path: &Path,
290        working_path: Option<&Path>,
291        ignore_local_config: bool,
292    ) -> impl Iterator<Item = PathBuf> {
293        let mut given_path = std::path::absolute(path).unwrap();
294        let working_path = std::env::current_dir().unwrap();
295
296        if !given_path.is_dir() {
297            given_path = given_path.parent().unwrap().into();
298        }
299
300        let common_path = common_path::common_path(&given_path, working_path).unwrap();
301        let mut path_to_visit = common_path;
302
303        let head = Some(given_path.canonicalize().unwrap()).into_iter();
304        let tail = std::iter::from_fn(move || {
305            if path_to_visit != given_path {
306                let path = path_to_visit.canonicalize().unwrap();
307
308                let next_path_to_visit = {
309                    // Convert `path_to_visit` & `given_path` to `Path`
310                    let path_to_visit_as_path = path_to_visit.as_path();
311                    let given_path_as_path = given_path.as_path();
312
313                    // Attempt to create a relative path from `given_path` to `path_to_visit`
314                    match given_path_as_path.strip_prefix(path_to_visit_as_path) {
315                        Ok(relative_path) => {
316                            // Get the first component of the relative path
317                            if let Some(first_part) = relative_path.components().next() {
318                                // Combine `path_to_visit` with the first part of the relative path
319                                path_to_visit.join(first_part.as_os_str())
320                            } else {
321                                // If there are no components in the relative path, return
322                                // `path_to_visit`
323                                path_to_visit.clone()
324                            }
325                        }
326                        Err(_) => {
327                            // If `given_path` is not relative to `path_to_visit`, handle the error
328                            // (e.g., return `path_to_visit`)
329                            // This part depends on how you want to handle the error.
330                            path_to_visit.clone()
331                        }
332                    }
333                };
334
335                if next_path_to_visit == path_to_visit {
336                    return None;
337                }
338
339                path_to_visit = next_path_to_visit;
340
341                Some(path)
342            } else {
343                None
344            }
345        });
346
347        head.chain(tail)
348    }
349
350    pub fn load_config_up_to_path(
351        &self,
352        path: impl AsRef<Path>,
353        extra_config_path: Option<String>,
354        ignore_local_config: bool,
355    ) -> HashMap<String, Value> {
356        let path = path.as_ref();
357
358        let config_stack = if ignore_local_config {
359            extra_config_path
360                .map(|path| vec![self.load_config_at_path(path)])
361                .unwrap_or_default()
362        } else {
363            let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
364            configs
365                .map(|path| self.load_config_at_path(path))
366                .collect_vec()
367        };
368
369        nested_combine(config_stack)
370    }
371
372    pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> HashMap<String, Value> {
373        let path = path.as_ref();
374
375        let filename_options = [
376            /* "setup.cfg", "tox.ini", "pep8.ini", */
377            ".sqlfluff",
378            ".sqruff", /* "pyproject.toml" */
379        ];
380
381        let mut configs = HashMap::new();
382
383        if path.is_dir() {
384            for fname in filename_options {
385                let path = path.join(fname);
386                if path.exists() {
387                    ConfigLoader::load_config_file(path, &mut configs);
388                }
389            }
390        } else if path.is_file() {
391            ConfigLoader::load_config_file(path, &mut configs);
392        };
393
394        configs
395    }
396
397    pub fn from_source(source: &str, path: Option<&Path>) -> HashMap<String, Value> {
398        let mut configs = HashMap::new();
399        let elems = ConfigLoader::get_config_elems_from_file(path, Some(source));
400        ConfigLoader::incorporate_vals(&mut configs, elems);
401        configs
402    }
403
404    pub fn load_config_file(path: impl AsRef<Path>, configs: &mut HashMap<String, Value>) {
405        let elems = ConfigLoader::get_config_elems_from_file(path.as_ref().into(), None);
406        ConfigLoader::incorporate_vals(configs, elems);
407    }
408
409    fn get_config_elems_from_file(
410        config_path: Option<&Path>,
411        config_string: Option<&str>,
412    ) -> Vec<(Vec<String>, Value)> {
413        let mut buff = Vec::new();
414        let mut config = Ini::new();
415
416        let content = match (config_path, config_string) {
417            (None, None) | (Some(_), Some(_)) => {
418                unimplemented!("One of fpath or config_string is required.")
419            }
420            (None, Some(text)) => text.to_owned(),
421            (Some(path), None) => std::fs::read_to_string(path).unwrap(),
422        };
423
424        config.read(content).unwrap();
425
426        for section in config.sections() {
427            let key = if section == "sqlfluff" || section == "sqruff" {
428                vec!["core".to_owned()]
429            } else if let Some(key) = section
430                .strip_prefix("sqlfluff:")
431                .or_else(|| section.strip_prefix("sqruff:"))
432            {
433                key.split(':').map(ToOwned::to_owned).collect()
434            } else {
435                continue;
436            };
437
438            let config_map = config.get_map_ref();
439            if let Some(section) = config_map.get(&section) {
440                for (name, value) in section {
441                    let mut value: Value = value.as_ref().unwrap().parse().unwrap();
442                    let name_lowercase = name.to_lowercase();
443
444                    if name_lowercase == "load_macros_from_path" {
445                        unimplemented!()
446                    } else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir")
447                    {
448                        // if absolute_path, just keep
449                        // if relative path, make it absolute
450                        let path = PathBuf::from(value.as_string().unwrap());
451                        if !path.is_absolute() {
452                            let config_path = config_path.unwrap().parent().unwrap();
453                            // make config path absolute
454                            let current_dir = std::env::current_dir().unwrap();
455                            let config_path = current_dir.join(config_path);
456                            let config_path = std::path::absolute(config_path).unwrap();
457                            let path = config_path.join(path);
458                            let path: String = path.to_string_lossy().into();
459                            value = Value::String(path.into());
460                        }
461                    }
462
463                    let mut key = key.clone();
464                    key.push(name.clone());
465                    buff.push((key, value));
466                }
467            }
468        }
469
470        buff
471    }
472
473    fn incorporate_vals(ctx: &mut HashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
474        for (path, value) in values {
475            let mut current_map = &mut *ctx;
476            for key in path.iter().take(path.len() - 1) {
477                match current_map
478                    .entry(key.to_string())
479                    .or_insert_with(|| Value::Map(HashMap::new()))
480                    .as_map_mut()
481                {
482                    Some(slot) => current_map = slot,
483                    None => panic!("Overriding config value with section! [{path:?}]"),
484                }
485            }
486
487            let last_key = path.last().expect("Expected at least one element in path");
488            current_map.insert(last_key.to_string(), value);
489        }
490    }
491}
492
493fn nested_combine(config_stack: Vec<HashMap<String, Value>>) -> HashMap<String, Value> {
494    let capacity = config_stack.len();
495    let mut result = HashMap::with_capacity(capacity);
496
497    for dict in config_stack {
498        for (key, value) in dict {
499            result.insert(key, value);
500        }
501    }
502
503    result
504}
505
506impl<'a> From<&'a FluffConfig> for Parser<'a> {
507    fn from(config: &'a FluffConfig) -> Self {
508        let dialect = config.get_dialect();
509        let indentation_section = &config.raw["indentation"];
510        let indentation_config =
511            IndentationConfig::from_bool_lookup(|key| indentation_section[key].to_bool());
512        Self::new(dialect, indentation_config)
513    }
514}
515
516#[cfg(test)]
517mod tests {
518    use super::*;
519    use sqruff_lib_core::dialects::init::DialectKind;
520
521    #[test]
522    fn test_dialect_config_section_parsing() {
523        // Test that [sqruff:dialect:snowflake] section is correctly parsed
524        let config = FluffConfig::from_source(
525            r#"
526[sqruff]
527dialect = snowflake
528
529[sqruff:dialect:snowflake]
530some_option = value
531"#,
532            None,
533        );
534
535        // Verify that the dialect config section is accessible
536        let dialect_section = config.raw.get("dialect");
537        assert!(dialect_section.is_some());
538
539        let snowflake_config = dialect_section.unwrap().as_map().unwrap().get("snowflake");
540        assert!(snowflake_config.is_some());
541
542        let snowflake_map = snowflake_config.unwrap().as_map().unwrap();
543        assert_eq!(
544            snowflake_map.get("some_option").unwrap().as_string(),
545            Some("value")
546        );
547    }
548
549    #[test]
550    fn test_dialect_config_empty_section() {
551        // Test that empty [sqruff:dialect:bigquery] section works
552        let config = FluffConfig::from_source(
553            r#"
554[sqruff]
555dialect = bigquery
556
557[sqruff:dialect:bigquery]
558"#,
559            None,
560        );
561
562        // The config should still be valid
563        assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
564    }
565
566    #[test]
567    fn test_dialect_without_config_section() {
568        // Test that a dialect works without a config section
569        let config = FluffConfig::from_source(
570            r#"
571[sqruff]
572dialect = postgres
573"#,
574            None,
575        );
576
577        // The config should still be valid
578        assert_eq!(config.get_dialect().name, DialectKind::Postgres);
579    }
580}