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
16pub fn split_comma_separated_string(raw_str: &str) -> Value {
19 let values = raw_str
20 .split(',')
21 .filter_map(|x| {
22 let trimmed = x.trim();
23 (!trimmed.is_empty()).then(|| Value::String(trimmed.into()))
24 })
25 .collect();
26 Value::Array(values)
27}
28
29#[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 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 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 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 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 ("ignore", "ignore"),
142 ("warnings", "warnings"),
143 ("rules", "rule_allowlist"),
144 ("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 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 pub fn process_raw_file_for_config(&self, raw_str: &str) {
232 for raw_line in raw_str.lines() {
234 if raw_line.to_string().starts_with("-- sqlfluff") {
235 self.process_inline_config(raw_line)
237 }
238 }
239 }
240
241 pub fn process_inline_config(&self, _config_line: &str) {
243 panic!("Not implemented")
244 }
245
246 pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
248 if self._configs.get("core")?.get("dialect").is_some() {
249 return None;
250 }
251 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 let path_to_visit_as_path = path_to_visit.as_path();
311 let given_path_as_path = given_path.as_path();
312
313 match given_path_as_path.strip_prefix(path_to_visit_as_path) {
315 Ok(relative_path) => {
316 if let Some(first_part) = relative_path.components().next() {
318 path_to_visit.join(first_part.as_os_str())
320 } else {
321 path_to_visit.clone()
324 }
325 }
326 Err(_) => {
327 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 ".sqlfluff",
378 ".sqruff", ];
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(§ion) {
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 let path = PathBuf::from(value.as_string().unwrap());
451 if !path.is_absolute() {
452 let config_path = config_path.unwrap().parent().unwrap();
453 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 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 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 let config = FluffConfig::from_source(
553 r#"
554[sqruff]
555dialect = bigquery
556
557[sqruff:dialect:bigquery]
558"#,
559 None,
560 );
561
562 assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
564 }
565
566 #[test]
567 fn test_dialect_without_config_section() {
568 let config = FluffConfig::from_source(
570 r#"
571[sqruff]
572dialect = postgres
573"#,
574 None,
575 );
576
577 assert_eq!(config.get_dialect().name, DialectKind::Postgres);
579 }
580}