1use crate::ast::Span;
21use serde::Deserialize;
22use std::path::PathBuf;
23
24pub static DEFAULT_LINTS: &str = include_str!("../lints.toml");
26
27pub const MAX_NESTING_DEPTH: usize = 4;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
33#[serde(rename_all = "lowercase")]
34pub enum Severity {
35 Error,
36 Warning,
37 Hint,
38}
39
40#[derive(Debug, Clone, Deserialize)]
42pub struct LintRule {
43 pub id: String,
45 pub pattern: String,
47 #[serde(default)]
49 pub replacement: String,
50 pub message: String,
52 #[serde(default = "default_severity")]
54 pub severity: Severity,
55}
56
57fn default_severity() -> Severity {
58 Severity::Warning
59}
60
61#[derive(Debug, Clone, Deserialize)]
63pub struct LintConfig {
64 #[serde(rename = "lint")]
65 pub rules: Vec<LintRule>,
66}
67
68impl LintConfig {
69 pub fn from_toml(toml_str: &str) -> Result<Self, String> {
71 toml::from_str(toml_str).map_err(|e| format!("Failed to parse lint config: {}", e))
72 }
73
74 pub fn default_config() -> Result<Self, String> {
76 Self::from_toml(DEFAULT_LINTS)
77 }
78
79 pub fn merge(&mut self, other: LintConfig) {
81 for rule in other.rules {
83 if let Some(existing) = self.rules.iter_mut().find(|r| r.id == rule.id) {
84 *existing = rule;
85 } else {
86 self.rules.push(rule);
87 }
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
99pub struct KnownLint {
100 pub id: String,
101 pub message: String,
102 pub severity: Severity,
103}
104
105pub fn known_lint_ids() -> Vec<KnownLint> {
118 let mut out = Vec::new();
119
120 if let Ok(config) = LintConfig::default_config() {
121 for rule in config.rules {
122 out.push(KnownLint {
123 id: rule.id,
124 message: rule.message,
125 severity: rule.severity,
126 });
127 }
128 }
129
130 out.push(KnownLint {
131 id: "deep-nesting".to_string(),
132 message: format!(
133 "deeply nested if/else ({}+ levels) - consider `cond` or extracting to helper words",
134 MAX_NESTING_DEPTH
135 ),
136 severity: Severity::Hint,
137 });
138
139 out.push(KnownLint {
140 id: "unchecked-error-flag".to_string(),
141 message: "operation returns a Bool success flag that is dropped without being checked"
142 .to_string(),
143 severity: Severity::Warning,
144 });
145
146 out
147}
148
149#[derive(Debug, Clone)]
151pub struct CompiledPattern {
152 pub rule: LintRule,
154 pub elements: Vec<PatternElement>,
156}
157
158#[derive(Debug, Clone, PartialEq)]
160pub enum PatternElement {
161 Word(String),
163 SingleWildcard(String),
165 MultiWildcard,
167}
168
169impl CompiledPattern {
170 pub fn compile(rule: LintRule) -> Result<Self, String> {
172 let mut elements = Vec::new();
173 let mut multi_wildcard_count = 0;
174
175 for token in rule.pattern.split_whitespace() {
176 if token == "$..." {
177 multi_wildcard_count += 1;
178 elements.push(PatternElement::MultiWildcard);
179 } else if token.starts_with('$') {
180 elements.push(PatternElement::SingleWildcard(token.to_string()));
181 } else {
182 elements.push(PatternElement::Word(token.to_string()));
183 }
184 }
185
186 if elements.is_empty() {
187 return Err(format!("Empty pattern in lint rule '{}'", rule.id));
188 }
189
190 if multi_wildcard_count > 1 {
193 return Err(format!(
194 "Pattern in lint rule '{}' has {} multi-wildcards ($...), but at most 1 is allowed",
195 rule.id, multi_wildcard_count
196 ));
197 }
198
199 Ok(CompiledPattern { rule, elements })
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct LintDiagnostic {
206 pub id: String,
208 pub message: String,
210 pub severity: Severity,
212 pub replacement: String,
214 pub file: PathBuf,
216 pub line: usize,
218 pub end_line: Option<usize>,
220 pub start_column: Option<usize>,
222 pub end_column: Option<usize>,
224 pub word_name: String,
226 pub start_index: usize,
228 pub end_index: usize,
230}
231
232#[derive(Debug, Clone)]
234pub(super) struct WordInfo<'a> {
235 pub(super) name: &'a str,
236 pub(super) span: Option<&'a Span>,
237}
238
239pub fn format_diagnostics(diagnostics: &[LintDiagnostic]) -> String {
240 let mut output = String::new();
241 for d in diagnostics {
242 let severity_str = match d.severity {
243 Severity::Error => "error",
244 Severity::Warning => "warning",
245 Severity::Hint => "hint",
246 };
247 let location = match d.start_column {
249 Some(col) => format!("{}:{}:{}", d.file.display(), d.line + 1, col + 1),
250 None => format!("{}:{}", d.file.display(), d.line + 1),
251 };
252 output.push_str(&format!(
253 "{}: {} [{}]: {}\n",
254 location, severity_str, d.id, d.message
255 ));
256 if !d.replacement.is_empty() {
257 output.push_str(&format!(" suggestion: replace with `{}`\n", d.replacement));
258 } else if d.replacement.is_empty() && d.message.contains("no effect") {
259 output.push_str(" suggestion: remove this code\n");
260 }
261 }
262 output
263}