sqruff_lib/core/
config.rs

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