use std::path::{Path, PathBuf};
use std::str::FromStr;
use configparser::ini::Ini;
use hashbrown::HashMap;
use sqruff_lib_core::dialects::Dialect;
use sqruff_lib_core::dialects::init::{DialectKind, dialect_readout};
use sqruff_lib_core::errors::SQLFluffUserError;
use sqruff_lib_core::parser::{IndentationConfig, Parser};
pub use sqruff_lib_core::value::Value;
use sqruff_lib_dialects::kind_to_dialect;
use crate::templaters::TemplaterKind;
use crate::utils::reflow::config::ReflowConfig;
pub fn split_comma_separated_string(raw_str: &str) -> Value {
let values = raw_str
.split(',')
.filter_map(|x| {
let trimmed = x.trim();
(!trimmed.is_empty()).then(|| Value::String(trimmed.into()))
})
.collect();
Value::Array(values)
}
fn split_string_or_array(value: &Value) -> Option<Value> {
match value {
Value::String(raw) => Some(split_comma_separated_string(raw)),
Value::Array(values) => Some(Value::Array(
values
.iter()
.map(|value| value.as_string().unwrap())
.flat_map(|value| match split_comma_separated_string(value) {
Value::Array(values) => values,
_ => unreachable!(),
})
.collect(),
)),
_ => None,
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluffConfig {
pub(crate) indentation: FluffConfigIndentation,
pub raw: HashMap<String, Value>,
extra_config_path: Option<String>,
_configs: HashMap<String, HashMap<String, String>>,
pub(crate) dialect: Dialect,
sql_file_exts: Vec<String>,
reflow: ReflowConfig,
}
impl Default for FluffConfig {
fn default() -> Self {
Self::new(<_>::default(), None, None)
}
}
impl FluffConfig {
fn configured_dialect_kind_from_raw(configs: &HashMap<String, Value>) -> DialectKind {
match configs
.get("core")
.and_then(|map| map.as_map().unwrap().get("dialect"))
{
None => DialectKind::default(),
Some(Value::String(std)) => DialectKind::from_str(std).unwrap(),
_value => DialectKind::default(),
}
}
fn dialect_section_from_raw(
configs: &HashMap<String, Value>,
dialect_kind: DialectKind,
) -> Option<&Value> {
configs
.get("dialect")
.and_then(|v| v.as_map())
.and_then(|m| m.get(dialect_kind.as_ref()))
}
pub fn override_dialect(&mut self, dialect: DialectKind) -> Result<(), String> {
self.dialect = kind_to_dialect(&dialect, None)
.ok_or(format!("Invalid dialect: {}", dialect.as_ref()))?;
Ok(())
}
pub fn get(&self, key: &str, section: &str) -> &Value {
&self.raw[section][key]
}
pub fn reflow(&self) -> &ReflowConfig {
&self.reflow
}
fn templater_root_section(&self) -> Option<&HashMap<String, Value>> {
self.raw.get("templater").and_then(Value::as_map)
}
pub fn templater_root_value(&self, key: &str) -> Option<&Value> {
self.templater_root_section()?.get(key)
}
pub fn templater_section(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
self.templater_root_section()?
.get(templater.as_str())
.and_then(Value::as_map)
}
pub fn templater_value(&self, templater: TemplaterKind, key: &str) -> Option<&Value> {
self.templater_section(templater)?.get(key)
}
pub fn templater_context(&self, templater: TemplaterKind) -> Option<&HashMap<String, Value>> {
self.templater_value(templater, "context")
.and_then(Value::as_map)
}
pub fn reload_reflow(&mut self) {
self.reflow = ReflowConfig::from_fluff_config(self);
}
pub fn from_file(path: &Path) -> FluffConfig {
Self::try_from_file(path).unwrap()
}
pub fn try_from_file(path: &Path) -> Result<FluffConfig, SQLFluffUserError> {
let mut configs = HashMap::new();
ConfigLoader::try_load_config_file(path, &mut configs)?;
Ok(FluffConfig::new(configs, None, None))
}
pub fn from_source(source: &str, optional_path_specification: Option<&Path>) -> FluffConfig {
Self::try_from_source(source, optional_path_specification).unwrap()
}
pub fn try_from_source(
source: &str,
optional_path_specification: Option<&Path>,
) -> Result<FluffConfig, SQLFluffUserError> {
let configs = ConfigLoader::try_from_source(source, optional_path_specification)?;
Ok(FluffConfig::new(configs, None, None))
}
pub fn get_section(&self, section: &str) -> &HashMap<String, Value> {
self.raw[section].as_map().unwrap()
}
pub fn dialect_kind(&self) -> DialectKind {
self.dialect.name()
}
pub fn templater_kind(&self) -> Result<TemplaterKind, String> {
self.get("templater", "core")
.as_string()
.map(TemplaterKind::from_name)
.transpose()
.map(|templater| templater.unwrap_or(TemplaterKind::Raw))
}
pub fn dialect_section(&self, dialect_kind: DialectKind) -> Option<&Value> {
Self::dialect_section_from_raw(&self.raw, dialect_kind)
}
pub fn new(
configs: HashMap<String, Value>,
extra_config_path: Option<String>,
indentation: Option<FluffConfigIndentation>,
) -> Self {
fn nested_combine(
mut a: HashMap<String, Value>,
b: HashMap<String, Value>,
) -> HashMap<String, Value> {
for (key, value_b) in b {
match (a.get(&key), value_b) {
(Some(Value::Map(map_a)), Value::Map(map_b)) => {
let combined = nested_combine(map_a.clone(), map_b);
a.insert(key, Value::Map(combined));
}
(_, value) => {
a.insert(key, value);
}
}
}
a
}
let values = ConfigLoader::get_config_elems_from_file(
None,
include_str!("./default_config.cfg").into(),
);
let mut defaults = HashMap::new();
ConfigLoader::incorporate_vals(&mut defaults, values);
let mut configs = nested_combine(defaults, configs);
let dialect_kind = Self::configured_dialect_kind_from_raw(&configs);
let dialect_config = Self::dialect_section_from_raw(&configs, dialect_kind);
let dialect = kind_to_dialect(&dialect_kind, dialect_config);
for (in_key, out_key) in [
("ignore", "ignore"),
("warnings", "warnings"),
("rules", "rule_allowlist"),
("exclude_rules", "rule_denylist"),
] {
match configs["core"].as_map().unwrap().get(in_key) {
Some(value) if !value.is_none() => {
let values = split_string_or_array(value).unwrap();
configs
.get_mut("core")
.unwrap()
.as_map_mut()
.unwrap()
.insert(out_key.into(), values);
}
_ => {}
}
}
let sql_file_exts = configs["core"]["sql_file_exts"]
.as_array()
.unwrap()
.iter()
.map(|it| it.as_string().unwrap().to_owned())
.collect();
let mut this = Self {
raw: configs,
dialect: dialect
.expect("Dialect is disabled. Please enable the corresponding feature."),
extra_config_path,
_configs: HashMap::new(),
indentation: indentation.unwrap_or_default(),
sql_file_exts,
reflow: ReflowConfig::default(),
};
this.reflow = ReflowConfig::from_fluff_config(&this);
this
}
pub fn with_sql_file_exts(mut self, exts: Vec<String>) -> Self {
self.sql_file_exts = exts;
self
}
pub fn from_root(
extra_config_path: Option<String>,
ignore_local_config: bool,
overrides: Option<HashMap<String, String>>,
) -> Result<FluffConfig, SQLFluffUserError> {
let loader = ConfigLoader {};
let mut config = loader.try_load_config_up_to_path(
".",
extra_config_path.clone(),
ignore_local_config,
)?;
if let Some(overrides) = overrides
&& let Some(dialect) = overrides.get("dialect")
{
let core = config
.entry("core".into())
.or_insert_with(|| Value::Map(HashMap::new()));
core.as_map_mut()
.unwrap()
.insert("dialect".into(), Value::String(dialect.clone().into()));
}
Ok(FluffConfig::new(config, extra_config_path, None))
}
pub fn from_kwargs(
config: Option<FluffConfig>,
dialect: Option<Dialect>,
rules: Option<Vec<String>>,
) -> Self {
if (dialect.is_some() || rules.is_some()) && config.is_some() {
panic!(
"Cannot specify `config` with `dialect` or `rules`. Any config object specifies \
its own dialect and rules."
)
} else {
config.unwrap()
}
}
pub fn process_raw_file_for_config(&self, raw_str: &str) {
for raw_line in raw_str.lines() {
if raw_line.to_string().starts_with("-- sqlfluff") {
self.process_inline_config(raw_line)
}
}
}
pub fn process_inline_config(&self, _config_line: &str) {
panic!("Not implemented")
}
pub fn verify_dialect_specified(&self) -> Option<SQLFluffUserError> {
if self._configs.get("core")?.get("dialect").is_some() {
return None;
}
Some(SQLFluffUserError::new(format!(
"No dialect was specified. You must configure a dialect or
specify one on the command line using --dialect after the
command. Available dialects: {}",
dialect_readout().join(", ").as_str()
)))
}
pub fn get_dialect(&self) -> &Dialect {
&self.dialect
}
pub fn sql_file_exts(&self) -> &[String] {
self.sql_file_exts.as_ref()
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct FluffConfigIndentation {
pub template_blocks_indent: bool,
}
impl Default for FluffConfigIndentation {
fn default() -> Self {
Self {
template_blocks_indent: true,
}
}
}
pub struct ConfigLoader;
impl ConfigLoader {
#[allow(unused_variables)]
fn iter_config_locations_up_to_path(
path: &Path,
working_path: Option<&Path>,
ignore_local_config: bool,
) -> impl Iterator<Item = PathBuf> {
let mut given_path = std::path::absolute(path).unwrap();
let working_path = std::env::current_dir().unwrap();
if !given_path.is_dir() {
given_path = given_path.parent().unwrap().into();
}
let common_path = common_path::common_path(&given_path, working_path).unwrap();
let mut path_to_visit = common_path;
let head = Some(given_path.canonicalize().unwrap()).into_iter();
let tail = std::iter::from_fn(move || {
if path_to_visit != given_path {
let path = path_to_visit.canonicalize().unwrap();
let next_path_to_visit = {
let path_to_visit_as_path = path_to_visit.as_path();
let given_path_as_path = given_path.as_path();
match given_path_as_path.strip_prefix(path_to_visit_as_path) {
Ok(relative_path) => {
if let Some(first_part) = relative_path.components().next() {
path_to_visit.join(first_part.as_os_str())
} else {
path_to_visit.clone()
}
}
Err(_) => {
path_to_visit.clone()
}
}
};
if next_path_to_visit == path_to_visit {
return None;
}
path_to_visit = next_path_to_visit;
Some(path)
} else {
None
}
});
head.chain(tail)
}
pub fn load_config_up_to_path(
&self,
path: impl AsRef<Path>,
extra_config_path: Option<String>,
ignore_local_config: bool,
) -> HashMap<String, Value> {
self.try_load_config_up_to_path(path, extra_config_path, ignore_local_config)
.unwrap()
}
pub fn try_load_config_up_to_path(
&self,
path: impl AsRef<Path>,
extra_config_path: Option<String>,
ignore_local_config: bool,
) -> Result<HashMap<String, Value>, SQLFluffUserError> {
let path = path.as_ref();
let config_stack = if ignore_local_config {
if let Some(path) = extra_config_path {
vec![self.try_load_config_at_path(path)?]
} else {
Vec::new()
}
} else {
let configs = Self::iter_config_locations_up_to_path(path, None, ignore_local_config);
configs
.map(|path| self.try_load_config_at_path(path))
.collect::<Result<Vec<_>, _>>()?
};
Ok(nested_combine(config_stack))
}
pub fn load_config_at_path(&self, path: impl AsRef<Path>) -> HashMap<String, Value> {
self.try_load_config_at_path(path).unwrap()
}
pub fn try_load_config_at_path(
&self,
path: impl AsRef<Path>,
) -> Result<HashMap<String, Value>, SQLFluffUserError> {
let path = path.as_ref();
let filename_options = [
".sqlfluff",
".sqruff",
".sqruff.ini",
"pyproject.toml",
"sqruff.toml",
];
let mut configs = HashMap::new();
if path.is_dir() {
for fname in filename_options {
let path = path.join(fname);
if path.exists() {
ConfigLoader::try_load_config_file(path, &mut configs)?;
}
}
} else if path.is_file() {
ConfigLoader::try_load_config_file(path, &mut configs)?;
};
Ok(configs)
}
pub fn from_source(source: &str, path: Option<&Path>) -> HashMap<String, Value> {
Self::try_from_source(source, path).unwrap()
}
pub fn try_from_source(
source: &str,
path: Option<&Path>,
) -> Result<HashMap<String, Value>, SQLFluffUserError> {
let mut configs = HashMap::new();
let elems = ConfigLoader::try_get_config_elems_from_file(path, Some(source))?;
ConfigLoader::incorporate_vals(&mut configs, elems);
Ok(configs)
}
pub fn load_config_file(path: impl AsRef<Path>, configs: &mut HashMap<String, Value>) {
Self::try_load_config_file(path, configs).unwrap();
}
pub fn try_load_config_file(
path: impl AsRef<Path>,
configs: &mut HashMap<String, Value>,
) -> Result<(), SQLFluffUserError> {
let elems = ConfigLoader::try_get_config_elems_from_file(path.as_ref().into(), None)?;
ConfigLoader::incorporate_vals(configs, elems);
Ok(())
}
fn get_config_elems_from_file(
config_path: Option<&Path>,
config_string: Option<&str>,
) -> Vec<(Vec<String>, Value)> {
Self::try_get_config_elems_from_file(config_path, config_string).unwrap()
}
fn try_get_config_elems_from_file(
config_path: Option<&Path>,
config_string: Option<&str>,
) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
let content = match (config_path, config_string) {
(None, None) => {
unimplemented!("One of fpath or config_string is required.")
}
(_, Some(text)) => text.to_owned(),
(Some(path), None) => std::fs::read_to_string(path).map_err(|err| {
config_error(config_path, format!("Unable to read config file: {err}"))
})?,
};
if is_toml_config(config_path) {
return parse_toml_config_elems(&content, config_path);
}
parse_ini_config_elems(&content, config_path)
}
fn incorporate_vals(ctx: &mut HashMap<String, Value>, values: Vec<(Vec<String>, Value)>) {
for (path, value) in values {
let mut current_map = &mut *ctx;
for key in path.iter().take(path.len() - 1) {
match current_map
.entry(key.to_string())
.or_insert_with(|| Value::Map(HashMap::new()))
.as_map_mut()
{
Some(slot) => current_map = slot,
None => panic!("Overriding config value with section! [{path:?}]"),
}
}
let last_key = path.last().expect("Expected at least one element in path");
current_map.insert(last_key.to_string(), value);
}
}
}
fn is_toml_config(config_path: Option<&Path>) -> bool {
config_path.is_some_and(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name == "pyproject.toml" || name.ends_with(".toml"))
})
}
fn config_error(config_path: Option<&Path>, message: impl std::fmt::Display) -> SQLFluffUserError {
let location = config_path
.map(|path| path.display().to_string())
.unwrap_or_else(|| "config source".to_owned());
SQLFluffUserError::new(format!("Error loading config from {location}: {}", message))
}
fn parse_ini_config_elems(
content: &str,
config_path: Option<&Path>,
) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
let mut buff = Vec::new();
let mut config = Ini::new();
config
.read(content.to_owned())
.map_err(|err| config_error(config_path, err))?;
for section in config.sections() {
let key = if section == "sqlfluff" || section == "sqruff" {
vec!["core".to_owned()]
} else if let Some(key) = section
.strip_prefix("sqlfluff:")
.or_else(|| section.strip_prefix("sqruff:"))
{
key.split(':').map(ToOwned::to_owned).collect()
} else {
continue;
};
let config_map = config.get_map_ref();
if let Some(section) = config_map.get(§ion) {
for (name, value) in section {
let mut value: Value = value.as_deref().unwrap_or_default().parse().unwrap();
let name_lowercase = name.to_lowercase();
if name_lowercase == "load_macros_from_path" {
unimplemented!()
} else if name_lowercase.ends_with("_path") || name_lowercase.ends_with("_dir") {
value = resolve_relative_config_path(value, config_path);
}
let mut key = key.clone();
key.push(name.clone());
buff.push((key, value));
}
}
}
Ok(buff)
}
fn parse_toml_config_elems(
content: &str,
config_path: Option<&Path>,
) -> Result<Vec<(Vec<String>, Value)>, SQLFluffUserError> {
let root = content
.parse::<toml::Table>()
.map_err(|err| config_error(config_path, err))?;
let mut buff = Vec::new();
for config_root in ["sqlfluff", "sqruff"] {
if let Some(table) = root.get(config_root).and_then(toml::Value::as_table) {
collect_toml_config_elems(table, Vec::new(), config_path, &mut buff);
}
}
if let Some(tool) = root.get("tool").and_then(toml::Value::as_table) {
for config_root in ["sqlfluff", "sqruff"] {
if let Some(table) = tool.get(config_root).and_then(toml::Value::as_table) {
collect_toml_config_elems(table, Vec::new(), config_path, &mut buff);
}
}
}
Ok(buff)
}
fn collect_toml_config_elems(
table: &toml::Table,
section_path: Vec<String>,
config_path: Option<&Path>,
buff: &mut Vec<(Vec<String>, Value)>,
) {
for (name, value) in table {
match value {
toml::Value::Table(table) => {
let mut section_path = section_path.clone();
section_path.push(name.to_owned());
collect_toml_config_elems(table, section_path, config_path, buff);
}
value => {
if name == "load_macros_from_path" {
unimplemented!()
}
let mut value = toml_value_to_config_value(value);
if name.ends_with("_path") || name.ends_with("_dir") {
value = resolve_relative_config_path(value, config_path);
}
let key = toml_config_key_path(§ion_path, name);
buff.push((key, value));
}
}
}
}
fn toml_config_key_path(section_path: &[String], key: &str) -> Vec<String> {
if section_path.is_empty()
|| (section_path.len() == 1
&& section_path
.first()
.is_some_and(|section| section == "core"))
{
vec!["core".to_owned(), key.to_owned()]
} else if section_path
.first()
.is_some_and(|section| section == "rules")
&& section_path.len() > 1
{
let rest = §ion_path[1..];
vec!["rules".to_owned(), rest.join("."), key.to_owned()]
} else {
section_path
.iter()
.cloned()
.chain(std::iter::once(key.to_owned()))
.collect()
}
}
fn toml_value_to_config_value(value: &toml::Value) -> Value {
match value {
toml::Value::String(value) => Value::String(value.clone().into()),
toml::Value::Integer(value) => {
Value::Int((*value).try_into().expect("TOML integer out of i32 range"))
}
toml::Value::Float(value) => Value::Float(*value),
toml::Value::Boolean(value) => Value::Bool(*value),
toml::Value::Datetime(value) => Value::String(value.to_string().into()),
toml::Value::Array(values) => Value::Array(
values
.iter()
.map(toml_value_to_config_value)
.collect::<Vec<_>>(),
),
toml::Value::Table(values) => Value::Map(
values
.iter()
.map(|(key, value)| (key.clone(), toml_value_to_config_value(value)))
.collect(),
),
}
}
fn resolve_relative_config_path(mut value: Value, config_path: Option<&Path>) -> Value {
let path = PathBuf::from(value.as_string().unwrap());
if !path.is_absolute() {
let config_path = config_path.unwrap().parent().unwrap();
let current_dir = std::env::current_dir().unwrap();
let config_path = current_dir.join(config_path);
let config_path = std::path::absolute(config_path).unwrap();
let path = config_path.join(path);
let path: String = path.to_string_lossy().into();
value = Value::String(path.into());
}
value
}
fn nested_combine(config_stack: Vec<HashMap<String, Value>>) -> HashMap<String, Value> {
let capacity = config_stack.len();
let mut result = HashMap::with_capacity(capacity);
for dict in config_stack {
for (key, value) in dict {
result.insert(key, value);
}
}
result
}
impl<'a> From<&'a FluffConfig> for Parser<'a> {
fn from(config: &'a FluffConfig) -> Self {
let dialect = config.get_dialect();
let indentation_section = &config.raw["indentation"];
let indentation_config =
IndentationConfig::from_bool_lookup(|key| indentation_section[key].to_bool());
Self::new(dialect, indentation_config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sqruff_lib_core::dialects::init::DialectKind;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
fn temp_config_dir(name: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"sqruff-config-{name}-{}-{nonce}",
std::process::id()
));
fs::create_dir_all(&path).unwrap();
path
}
#[test]
fn test_dialect_config_section_parsing() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = snowflake
[sqruff:dialect:snowflake]
some_option = value
"#,
None,
);
let dialect_section = config.raw.get("dialect");
assert!(dialect_section.is_some());
let snowflake_config = dialect_section.unwrap().as_map().unwrap().get("snowflake");
assert!(snowflake_config.is_some());
let snowflake_map = snowflake_config.unwrap().as_map().unwrap();
assert_eq!(
snowflake_map.get("some_option").unwrap().as_string(),
Some("value")
);
}
#[test]
fn test_dialect_config_empty_section() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = bigquery
[sqruff:dialect:bigquery]
"#,
None,
);
assert_eq!(config.get_dialect().name, DialectKind::Bigquery);
}
#[test]
fn test_dialect_without_config_section() {
let config = FluffConfig::from_source(
r#"
[sqruff]
dialect = postgres
"#,
None,
);
assert_eq!(config.get_dialect().name, DialectKind::Postgres);
}
#[test]
fn test_templater_kind_defaults_to_raw() {
let config = FluffConfig::from_source("", None);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Raw);
}
#[test]
fn test_templater_kind_parses_placeholder() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = placeholder
"#,
None,
);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
}
#[test]
fn test_templater_section_uses_typed_kind() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = placeholder
[sqruff:templater:placeholder]
param_style = colon
"#,
None,
);
let section = config
.templater_section(TemplaterKind::Placeholder)
.unwrap();
assert_eq!(
section.get("param_style").unwrap().as_string(),
Some("colon")
);
}
#[test]
fn test_sqruff_toml_parses_sqlfluff_root_config() {
let config = FluffConfig::from_source(
r#"
[sqlfluff]
dialect = "postgres"
templater = "placeholder"
max_line_length = 60
[sqlfluff.indentation]
indented_joins = false
[sqlfluff.layout.type.comma]
line_position = "trailing"
"#,
Some(Path::new("sqruff.toml")),
);
assert_eq!(config.get_dialect().name, DialectKind::Postgres);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(60));
assert_eq!(
config.raw["indentation"]["indented_joins"].as_bool(),
Some(false)
);
assert_eq!(
config.raw["layout"]["type"]["comma"]["line_position"].as_string(),
Some("trailing")
);
}
#[test]
fn test_pyproject_toml_parses_tool_sqlfluff_config() {
let config = FluffConfig::from_source(
r#"
[project]
name = "example"
[tool.sqlfluff.core]
dialect = "postgres"
templater = "placeholder"
max_line_length = 42
exclude_rules = ["CP01", "LT05"]
[tool.sqlfluff.templater.placeholder]
param_style = "colon"
[tool.sqlfluff.rules.capitalisation.keywords]
capitalisation_policy = "upper"
"#,
Some(Path::new("pyproject.toml")),
);
assert_eq!(config.get_dialect().name, DialectKind::Postgres);
assert_eq!(config.templater_kind().unwrap(), TemplaterKind::Placeholder);
assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(42));
assert_eq!(
config.raw["core"]["rule_denylist"].as_array().unwrap(),
vec![Value::String("CP01".into()), Value::String("LT05".into())]
);
assert_eq!(
config
.templater_section(TemplaterKind::Placeholder)
.unwrap()
.get("param_style")
.unwrap()
.as_string(),
Some("colon")
);
assert_eq!(
config.raw["rules"]["capitalisation.keywords"]["capitalisation_policy"].as_string(),
Some("upper")
);
}
#[test]
fn test_load_config_at_path_discovers_toml_configs() {
let dir = temp_config_dir("toml-discovery");
fs::write(
dir.join("pyproject.toml"),
r#"
[tool.sqlfluff.core]
max_line_length = 41
"#,
)
.unwrap();
fs::write(
dir.join("sqruff.toml"),
r#"
[sqruff]
max_line_length = 39
"#,
)
.unwrap();
let config = FluffConfig::new(ConfigLoader {}.load_config_at_path(&dir), None, None);
fs::remove_dir_all(&dir).unwrap();
assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(39));
}
#[test]
fn test_load_config_at_path_discovers_sqruff_ini() {
let dir = temp_config_dir("sqruff-ini");
fs::write(
dir.join(".sqruff.ini"),
r#"
[sqruff]
max_line_length = 44
"#,
)
.unwrap();
let config = FluffConfig::new(ConfigLoader {}.load_config_at_path(&dir), None, None);
fs::remove_dir_all(&dir).unwrap();
assert_eq!(config.raw["core"]["max_line_length"].as_int(), Some(44));
}
#[test]
fn test_try_from_source_invalid_toml_returns_error() {
let err = FluffConfig::try_from_source(
"[sqlfluff]\ndialect = \"ansi",
Some(Path::new("sqruff.toml")),
)
.unwrap_err();
assert!(
err.to_string()
.contains("Error loading config from sqruff.toml")
);
}
#[cfg(feature = "python")]
#[test]
fn test_templater_context_uses_typed_kind() {
let config = FluffConfig::from_source(
r#"
[sqruff]
templater = python
[sqruff:templater:python:context]
blah = foo
"#,
None,
);
let context = config.templater_context(TemplaterKind::Python).unwrap();
assert_eq!(context.get("blah").unwrap().as_string(), Some("foo"));
}
}