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