1use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5use thiserror::Error;
6
7pub type LintResult<T> = Result<T, LintError>;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Fix {
12 pub start_byte: usize,
14 pub end_byte: usize,
16 pub replacement: String,
18 pub description: String,
20 pub safe: bool,
22}
23
24impl Fix {
25 pub fn replace(
27 start: usize,
28 end: usize,
29 replacement: impl Into<String>,
30 description: impl Into<String>,
31 safe: bool,
32 ) -> Self {
33 Self {
34 start_byte: start,
35 end_byte: end,
36 replacement: replacement.into(),
37 description: description.into(),
38 safe,
39 }
40 }
41
42 pub fn insert(
44 position: usize,
45 text: impl Into<String>,
46 description: impl Into<String>,
47 safe: bool,
48 ) -> Self {
49 Self {
50 start_byte: position,
51 end_byte: position,
52 replacement: text.into(),
53 description: description.into(),
54 safe,
55 }
56 }
57
58 pub fn delete(start: usize, end: usize, description: impl Into<String>, safe: bool) -> Self {
60 Self {
61 start_byte: start,
62 end_byte: end,
63 replacement: String::new(),
64 description: description.into(),
65 safe,
66 }
67 }
68
69 pub fn is_insertion(&self) -> bool {
71 self.start_byte == self.end_byte && !self.replacement.is_empty()
72 }
73
74 pub fn is_deletion(&self) -> bool {
76 self.replacement.is_empty() && self.start_byte < self.end_byte
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Diagnostic {
83 pub level: DiagnosticLevel,
84 pub message: String,
85 pub file: PathBuf,
86 pub line: usize,
87 pub column: usize,
88 pub rule: String,
89 pub suggestion: Option<String>,
90 #[serde(default)]
92 pub fixes: Vec<Fix>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub assumption: Option<String>,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
104pub enum DiagnosticLevel {
105 Error,
106 Warning,
107 Info,
108}
109
110impl Diagnostic {
111 pub fn error(
112 file: PathBuf,
113 line: usize,
114 column: usize,
115 rule: &str,
116 message: impl Into<String>,
117 ) -> Self {
118 Self {
119 level: DiagnosticLevel::Error,
120 message: message.into(),
121 file,
122 line,
123 column,
124 rule: rule.to_string(),
125 suggestion: None,
126 fixes: Vec::new(),
127 assumption: None,
128 }
129 }
130
131 pub fn warning(
132 file: PathBuf,
133 line: usize,
134 column: usize,
135 rule: &str,
136 message: impl Into<String>,
137 ) -> Self {
138 Self {
139 level: DiagnosticLevel::Warning,
140 message: message.into(),
141 file,
142 line,
143 column,
144 rule: rule.to_string(),
145 suggestion: None,
146 fixes: Vec::new(),
147 assumption: None,
148 }
149 }
150
151 pub fn info(
152 file: PathBuf,
153 line: usize,
154 column: usize,
155 rule: &str,
156 message: impl Into<String>,
157 ) -> Self {
158 Self {
159 level: DiagnosticLevel::Info,
160 message: message.into(),
161 file,
162 line,
163 column,
164 rule: rule.to_string(),
165 suggestion: None,
166 fixes: Vec::new(),
167 assumption: None,
168 }
169 }
170
171 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
172 self.suggestion = Some(suggestion.into());
173 self
174 }
175
176 pub fn with_assumption(mut self, assumption: impl Into<String>) -> Self {
181 self.assumption = Some(assumption.into());
182 self
183 }
184
185 pub fn with_fix(mut self, fix: Fix) -> Self {
187 self.fixes.push(fix);
188 self
189 }
190
191 pub fn with_fixes(mut self, fixes: impl IntoIterator<Item = Fix>) -> Self {
193 self.fixes.extend(fixes);
194 self
195 }
196
197 pub fn has_fixes(&self) -> bool {
199 !self.fixes.is_empty()
200 }
201
202 pub fn has_safe_fixes(&self) -> bool {
204 self.fixes.iter().any(|f| f.safe)
205 }
206}
207
208#[derive(Error, Debug)]
210pub enum LintError {
211 #[error("Failed to read file: {path}")]
212 FileRead {
213 path: PathBuf,
214 #[source]
215 source: std::io::Error,
216 },
217
218 #[error("Failed to write file: {path}")]
219 FileWrite {
220 path: PathBuf,
221 #[source]
222 source: std::io::Error,
223 },
224
225 #[error("Refusing to read symlink: {path}")]
226 FileSymlink { path: PathBuf },
227
228 #[error("File too large: {path} ({size} bytes, limit {limit} bytes)")]
229 FileTooBig {
230 path: PathBuf,
231 size: u64,
232 limit: u64,
233 },
234
235 #[error("Not a regular file: {path}")]
236 FileNotRegular { path: PathBuf },
237
238 #[error("Invalid exclude pattern: {pattern} ({message})")]
239 InvalidExcludePattern { pattern: String, message: String },
240
241 #[error("Too many files to validate: {count} files found, limit is {limit}")]
242 TooManyFiles { count: usize, limit: usize },
243
244 #[error(transparent)]
245 Other(anyhow::Error),
246}