nu_lint/
config.rs

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