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)]
94pub struct CompiledPattern {
95 pub rule: LintRule,
97 pub elements: Vec<PatternElement>,
99}
100
101#[derive(Debug, Clone, PartialEq)]
103pub enum PatternElement {
104 Word(String),
106 SingleWildcard(String),
108 MultiWildcard,
110}
111
112impl CompiledPattern {
113 pub fn compile(rule: LintRule) -> Result<Self, String> {
115 let mut elements = Vec::new();
116 let mut multi_wildcard_count = 0;
117
118 for token in rule.pattern.split_whitespace() {
119 if token == "$..." {
120 multi_wildcard_count += 1;
121 elements.push(PatternElement::MultiWildcard);
122 } else if token.starts_with('$') {
123 elements.push(PatternElement::SingleWildcard(token.to_string()));
124 } else {
125 elements.push(PatternElement::Word(token.to_string()));
126 }
127 }
128
129 if elements.is_empty() {
130 return Err(format!("Empty pattern in lint rule '{}'", rule.id));
131 }
132
133 if multi_wildcard_count > 1 {
136 return Err(format!(
137 "Pattern in lint rule '{}' has {} multi-wildcards ($...), but at most 1 is allowed",
138 rule.id, multi_wildcard_count
139 ));
140 }
141
142 Ok(CompiledPattern { rule, elements })
143 }
144}
145
146#[derive(Debug, Clone)]
148pub struct LintDiagnostic {
149 pub id: String,
151 pub message: String,
153 pub severity: Severity,
155 pub replacement: String,
157 pub file: PathBuf,
159 pub line: usize,
161 pub end_line: Option<usize>,
163 pub start_column: Option<usize>,
165 pub end_column: Option<usize>,
167 pub word_name: String,
169 pub start_index: usize,
171 pub end_index: usize,
173}
174
175#[derive(Debug, Clone)]
177pub(super) struct WordInfo<'a> {
178 pub(super) name: &'a str,
179 pub(super) span: Option<&'a Span>,
180}
181
182pub fn format_diagnostics(diagnostics: &[LintDiagnostic]) -> String {
183 let mut output = String::new();
184 for d in diagnostics {
185 let severity_str = match d.severity {
186 Severity::Error => "error",
187 Severity::Warning => "warning",
188 Severity::Hint => "hint",
189 };
190 let location = match d.start_column {
192 Some(col) => format!("{}:{}:{}", d.file.display(), d.line + 1, col + 1),
193 None => format!("{}:{}", d.file.display(), d.line + 1),
194 };
195 output.push_str(&format!(
196 "{}: {} [{}]: {}\n",
197 location, severity_str, d.id, d.message
198 ));
199 if !d.replacement.is_empty() {
200 output.push_str(&format!(" suggestion: replace with `{}`\n", d.replacement));
201 } else if d.replacement.is_empty() && d.message.contains("no effect") {
202 output.push_str(" suggestion: remove this code\n");
203 }
204 }
205 output
206}