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 }
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 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 #[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#[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 if !current_dir.pop() {
158 break;
159 }
160 }
161
162 None
163}