nu_lint/
config.rs

1use std::{
2    collections::HashMap,
3    env::current_dir,
4    fs,
5    path::{Path, PathBuf},
6    process,
7};
8
9use serde::{Deserialize, Serialize};
10
11use crate::violation::Severity;
12
13#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
14pub struct Config {
15    #[serde(default)]
16    pub general: GeneralConfig,
17
18    #[serde(default)]
19    pub rules: HashMap<String, RuleSeverity>,
20
21    #[serde(default)]
22    pub style: StyleConfig,
23
24    #[serde(default)]
25    pub exclude: ExcludeConfig,
26
27    #[serde(default)]
28    pub fix: FixConfig,
29}
30
31#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
32pub struct GeneralConfig {
33    #[serde(default = "GeneralConfig::default_min_severity")]
34    pub min_severity: RuleSeverity,
35}
36
37impl Default for GeneralConfig {
38    fn default() -> Self {
39        Self {
40            min_severity: Self::default_min_severity(),
41        }
42    }
43}
44
45impl GeneralConfig {
46    const fn default_min_severity() -> RuleSeverity {
47        RuleSeverity::Warning // Show warnings and errors by default
48    }
49}
50
51#[derive(Debug, Clone, Copy, Deserialize, Serialize, Default, PartialEq, PartialOrd, Ord, Eq)]
52#[serde(rename_all = "lowercase")]
53pub enum RuleSeverity {
54    Off,
55    Info,
56    Warning,
57    #[default]
58    Error,
59}
60
61impl From<RuleSeverity> for Option<Severity> {
62    fn from(rule_sev: RuleSeverity) -> Self {
63        match rule_sev {
64            RuleSeverity::Error => Some(Severity::Error),
65            RuleSeverity::Warning => Some(Severity::Warning),
66            RuleSeverity::Info => Some(Severity::Info),
67            RuleSeverity::Off => None,
68        }
69    }
70}
71
72impl From<Severity> for RuleSeverity {
73    fn from(severity: Severity) -> Self {
74        match severity {
75            Severity::Error => Self::Error,
76            Severity::Warning => Self::Warning,
77            Severity::Info => Self::Info,
78        }
79    }
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
83pub struct StyleConfig {
84    #[serde(default = "StyleConfig::default_line_length")]
85    pub line_length: usize,
86
87    #[serde(default = "StyleConfig::default_indent_spaces")]
88    pub indent_spaces: usize,
89}
90
91impl StyleConfig {
92    const fn default_line_length() -> usize {
93        100
94    }
95
96    const fn default_indent_spaces() -> usize {
97        4
98    }
99}
100
101#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
102pub struct ExcludeConfig {
103    #[serde(default)]
104    pub patterns: Vec<String>,
105}
106
107#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq, Eq)]
108pub struct FixConfig {
109    pub enabled: bool,
110
111    pub safe_only: bool,
112}
113
114impl Config {
115    /// Load configuration from a TOML file.
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if the file cannot be read or if the TOML content is
120    /// invalid.
121    pub(crate) fn load_from_file(path: &Path) -> Result<Self, crate::LintError> {
122        let content = fs::read_to_string(path)?;
123        Ok(toml::from_str(&content)?)
124    }
125
126    /// Load configuration from file or use defaults
127    #[must_use]
128    pub fn load(config_path: Option<&PathBuf>) -> Self {
129        config_path
130            .cloned()
131            .or_else(find_config_file)
132            .map_or_else(Self::default, |path| {
133                Self::load_from_file(&path).unwrap_or_else(|e| {
134                    eprintln!("Error loading config from {}: {e}", path.display());
135                    process::exit(2);
136                })
137            })
138    }
139
140    pub(crate) fn rule_severity(&self, rule_id: &str) -> Option<Severity> {
141        self.rules.get(rule_id).copied().and_then(Into::into)
142    }
143}
144
145/// Search for .nu-lint.toml in current directory and parent directories
146#[must_use]
147pub fn find_config_file() -> Option<PathBuf> {
148    let mut current_dir = current_dir().ok()?;
149
150    loop {
151        let config_path = current_dir.join(".nu-lint.toml");
152        if config_path.exists() && config_path.is_file() {
153            return Some(config_path);
154        }
155
156        // Try to go to parent directory
157        if !current_dir.pop() {
158            break;
159        }
160    }
161
162    None
163}