use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
sync::LazyLock,
};
use miette::Severity;
use serde::{Deserialize, Serialize};
use crate::{
LintError,
rule::Rule,
rules::{USED_RULES, groups::ALL_GROUPS},
};
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum LintLevel {
Off,
Hint,
#[default]
Warning,
Error,
}
impl TryFrom<LintLevel> for Severity {
type Error = ();
fn try_from(value: LintLevel) -> Result<Self, ()> {
match value {
LintLevel::Off => Err(()),
LintLevel::Hint => Ok(Self::Advice),
LintLevel::Warning => Ok(Self::Warning),
LintLevel::Error => Ok(Self::Error),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum PipelinePlacement {
#[default]
Start,
End,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct Config {
pub groups: HashMap<String, LintLevel>,
pub rules: HashMap<String, LintLevel>,
pub sequential: bool,
pub pipeline_placement: PipelinePlacement,
pub max_pipeline_length: usize,
pub skip_external_parse_errors: bool,
pub explicit_optional_access: bool,
pub max_function_body_statements: usize,
pub max_inlinable_pipeline_elements: usize,
}
impl Default for Config {
fn default() -> Self {
Self {
groups: HashMap::new(),
rules: HashMap::new(),
sequential: false,
pipeline_placement: PipelinePlacement::default(),
max_pipeline_length: 80,
skip_external_parse_errors: true,
explicit_optional_access: false,
max_function_body_statements: 40,
max_inlinable_pipeline_elements: 2,
}
}
}
impl Config {
#[must_use]
pub fn default_static() -> &'static Self {
static DEFAULT: LazyLock<Config> = LazyLock::new(Config::default);
&DEFAULT
}
pub(crate) fn load_from_str(toml_str: &str) -> Result<Self, LintError> {
toml::from_str(toml_str).map_err(|source| LintError::Config { source })
}
pub(crate) fn load_from_file(path: &Path) -> Result<Self, LintError> {
log::debug!("Loading configuration file at {}", path.display());
let content = fs::read_to_string(path).map_err(|source| LintError::Io {
path: path.to_path_buf(),
source,
})?;
Self::load_from_str(&content)
}
pub fn validate(&self) -> Result<(), LintError> {
log::debug!("Validating loaded configuration.");
for rule_id_in_config_file in self.rules.keys() {
if USED_RULES
.iter()
.find(|rule| rule.id() == rule_id_in_config_file)
.is_none()
{
return Err(LintError::RuleDoesNotExist {
non_existing_id: rule_id_in_config_file.clone(),
});
}
}
for rule in USED_RULES {
if self.get_lint_level(*rule) == LintLevel::Off {
continue;
}
for conflicting_rule in rule.conflicts_with() {
if self.get_lint_level(*conflicting_rule) > LintLevel::Off {
return Err(LintError::RuleConflict {
rule_a: rule.id(),
rule_b: conflicting_rule.id(),
});
}
}
}
Ok(())
}
#[must_use]
pub fn get_lint_level(&self, rule: &dyn Rule) -> LintLevel {
let rule_id = rule.id();
if let Some(level) = self.rules.get(rule_id) {
log::trace!(
"Rule '{rule_id}' has individual level '{level:?}' in config, overriding set \
levels"
);
return *level;
}
for (set_name, level) in &self.groups {
let Some(lint_set) = ALL_GROUPS.iter().find(|set| set.name == set_name.as_str()) else {
continue;
};
if !lint_set.rules.iter().any(|r| r.id() == rule_id) {
continue;
}
log::trace!("Rule '{rule_id}' found in set '{set_name}' with level {level:?}");
return *level;
}
rule.level()
}
}
#[must_use]
pub fn user_config_path() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("nu-lint.toml"))
}
#[must_use]
pub fn load_user_config() -> Config {
user_config_path()
.filter(|p| p.is_file())
.and_then(|p| Config::load_from_file(&p).ok())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load_config_simple_str() {
let toml_str = r#"
[rules]
snake_case_variables = "error"
other_rule = "off"
"#;
let config = Config::load_from_str(toml_str).unwrap();
assert_eq!(config.rules["snake_case_variables"], LintLevel::Error);
assert_eq!(config.rules["other_rule"], LintLevel::Off);
}
#[test]
fn test_validate_passes_with_default_config() {
let result = Config::default().validate();
assert!(result.is_ok());
}
}