alpm_srcinfo/error.rs
1//! All error types that are exposed by this crate.
2use std::{fmt::Display, path::PathBuf, string::FromUtf8Error};
3
4use colored::Colorize;
5use thiserror::Error;
6
7#[cfg(doc)]
8use crate::{SourceInfoV1, source_info::parser::SourceInfoContent};
9
10/// The high-level error that can occur when using this crate.
11///
12/// Notably, it contains two important enums in the context of parsing:
13/// - `ParseError` is a already formatted error generated by the `winnow` parser. This effectively
14/// means that some invalid data has been encountered.
15/// - `SourceInfoErrors` is a list of all logical or lint errors that're encountered in the final
16/// step. This error also contains the original file on which the errors occurred.
17#[derive(Debug, Error)]
18#[non_exhaustive]
19pub enum Error {
20 /// ALPM type error
21 #[error("ALPM type parse error: {0}")]
22 AlpmType(#[from] alpm_types::Error),
23
24 /// IO error
25 #[error("I/O error while {0}:\n{1}")]
26 Io(&'static str, std::io::Error),
27
28 /// IO error with additional path info for more context.
29 #[error("I/O error at path {0:?} while {1}:\n{2}")]
30 IoPath(PathBuf, &'static str, std::io::Error),
31
32 /// UTF-8 parse error when reading the input file.
33 #[error(transparent)]
34 InvalidUTF8(#[from] FromUtf8Error),
35
36 /// A section or keyword is missing for a SRCINFO schema version.
37 #[error(
38 "The SRCINFO data misses one or more required sections ({sections}) or keywords ({keywords}) for schema version {schema_version}"
39 )]
40 MissingSchemaSectionsOrKeywords {
41 sections: &'static str,
42 keywords: &'static str,
43 schema_version: &'static str,
44 },
45
46 /// No input file given.
47 ///
48 /// This error only occurs when running the [`crate::commands`] functions.
49 #[error("No input file given.")]
50 NoInputFile,
51
52 /// A parsing error that occurred during winnow file parsing.
53 #[error("File parsing error:\n{0}")]
54 ParseError(String),
55
56 /// A list of errors that occurred during the final SRCINFO data parsing step.
57 ///
58 /// These may contain any combination of [`SourceInfoError`].
59 #[error("Errors while parsing SRCINFO data:\n\n{0}")]
60 SourceInfoErrors(SourceInfoErrors),
61
62 /// JSON error while creating JSON formatted output.
63 ///
64 /// This error only occurs when running the [`crate::commands`] functions.
65 #[error("JSON error: {0}")]
66 Json(#[from] serde_json::Error),
67
68 /// Unsupported schema version
69 #[error("Unsupported schema version: {0}")]
70 UnsupportedSchemaVersion(String),
71}
72
73/// A helper struct to provide proper line based error/linting messages.
74///
75/// Provides a list of [`SourceInfoError`]s and the SRCINFO data in which the errors occurred.
76#[derive(Debug, Clone)]
77pub struct SourceInfoErrors {
78 inner: Vec<SourceInfoError>,
79 file_content: String,
80}
81
82impl Display for SourceInfoErrors {
83 /// Display all errors in one big well-formatted error message.
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 // We go through all errors and print them out one after another.
86 let mut error_iter = self.inner.iter().enumerate().peekable();
87 while let Some((index, error)) = error_iter.next() {
88 // Line and message are generic and the same for every error message.
89 let line_nr = error.line;
90 let message = &error.message;
91
92 // Build the the headline based on the error type.
93 let specific_line =
94 line_nr.map_or("".to_string(), |line| format!(" on line {}", line + 1));
95 let headline = match error.error_type {
96 SourceInfoErrorType::LintWarning => {
97 format!("{}{specific_line}:", "Linter Warning".yellow())
98 }
99 SourceInfoErrorType::DeprecationWarning => {
100 format!("{}{specific_line}:", "Deprecation Warning".yellow())
101 }
102 SourceInfoErrorType::Unrecoverable => {
103 format!("{}{specific_line}:", "Logical Error".red())
104 }
105 };
106
107 // Write the headline
108 let error_index = format!("[{index}]").bold().red();
109 // Print the error details slightly indented based on the length of the error index
110 // prefix.
111 let indentation = " ".repeat(error_index.len() + 1);
112 write!(f, "{error_index} {headline}")?;
113 // Add the line, if it exists.
114 // Prefix it with a bold line number for better visibility.
115 if let Some(line_nr) = line_nr {
116 let content_line = self
117 .file_content
118 .lines()
119 .nth(line_nr)
120 .expect("Error: Couldn't seek to line. Please report bug upstream.");
121 // Lines aren't 0 indexed.
122 let human_line_nr = line_nr + 1;
123 write!(
124 f,
125 "\n{indentation}{} {content_line }\n",
126 format!("{human_line_nr}: |").to_string().bold()
127 )?;
128 }
129
130 // Write the message with some spacing
131 write!(f, "\n{indentation}{message}")?;
132
133 // Write two newlines with a red separator between this and the next error
134 if error_iter.peek().is_some() {
135 write!(f, "\n\n{}", "──────────────────────────────\n".bold())?;
136 }
137 }
138
139 Ok(())
140 }
141}
142
143impl SourceInfoErrors {
144 /// Creates a new [`SourceInfoErrors`].
145 pub fn new(errors: Vec<SourceInfoError>, file_content: String) -> Self {
146 Self {
147 inner: errors,
148 file_content,
149 }
150 }
151
152 /// Filters the inner errors based on a given closure.
153 pub fn filter<F>(&mut self, filter: F)
154 where
155 F: Fn(&SourceInfoError) -> bool,
156 {
157 self.inner.retain(filter);
158 }
159
160 /// Returns a reference to the list of errors.
161 pub fn errors(&self) -> &Vec<SourceInfoError> {
162 &self.inner
163 }
164
165 /// Filters for and errors on unrecoverable errors.
166 ///
167 /// Consumes `self` and simply returns if `self` contains no [`SourceInfoError`] of type
168 /// [`SourceInfoErrorType::Unrecoverable`].
169 ///
170 /// # Errors
171 ///
172 /// Returns an error if `self` contains any [`SourceInfoError`] of type
173 /// [`SourceInfoErrorType::Unrecoverable`].
174 pub fn check_unrecoverable_errors(mut self) -> Result<(), Error> {
175 // Filter only for errors that're unrecoverable, i.e. critical.
176 self.filter(|err| matches!(err.error_type, SourceInfoErrorType::Unrecoverable));
177
178 if !self.inner.is_empty() {
179 self.sort_errors();
180 return Err(Error::SourceInfoErrors(self));
181 }
182
183 Ok(())
184 }
185
186 /// Sorts the errors.
187 ///
188 /// The following order is applied:
189 ///
190 /// - Hard errors without line numbers
191 /// - Hard errors with line numbers, by ascending line number
192 /// - Deprecation warnings without line numbers
193 /// - Deprecation warnings with line numbers, by ascending line number
194 /// - Linter warnings without line numbers
195 /// - Linter warnings with line numbers, by ascending line number
196 fn sort_errors(&mut self) {
197 self.inner.sort_by(|a, b| {
198 use std::cmp::Ordering;
199
200 let prio = |error: &SourceInfoError| match error.error_type {
201 SourceInfoErrorType::Unrecoverable => 0,
202 SourceInfoErrorType::DeprecationWarning => 1,
203 SourceInfoErrorType::LintWarning => 2,
204 };
205
206 // Extract ordering criteria based on error type.
207 let a_prio = prio(a);
208 let b_prio = prio(b);
209
210 // Compare by error severity first.
211 match a_prio.cmp(&b_prio) {
212 // If it's the same error, do a comparison on a line basis.
213 // Unspecific errors should come first!
214 Ordering::Equal => match (a.line, b.line) {
215 (Some(a), Some(b)) => a.cmp(&b),
216 (Some(_), None) => Ordering::Less,
217 (None, Some(_)) => Ordering::Greater,
218 (None, None) => Ordering::Equal,
219 },
220 // If it's not the same error, the ordering is clear.
221 other => other,
222 }
223 });
224 }
225}
226
227/// Errors that may occur when converting [`SourceInfoContent`] into a [`SourceInfoV1`].
228///
229/// The severity of an error is defined by its [`SourceInfoErrorType`], which may range from linting
230/// errors, deprecation warnings to hard unrecoverable errors.
231#[derive(Debug, Clone)]
232pub struct SourceInfoError {
233 pub error_type: SourceInfoErrorType,
234 pub line: Option<usize>,
235 pub message: String,
236}
237
238/// A [`SourceInfoError`] type.
239///
240/// Provides context for the severity of a [`SourceInfoError`].
241/// The type of "error" that has occurred.
242#[derive(Debug, Clone)]
243pub enum SourceInfoErrorType {
244 /// A simple linter error type. Can be ignored but should be fixed.
245 LintWarning,
246 /// Something changed in the SRCINFO format and this should be removed for future
247 /// compatibility.
248 DeprecationWarning,
249 /// A hard unrecoverable logic error has been detected.
250 /// The returned [SourceInfoV1] representation is faulty and should not be used.
251 Unrecoverable,
252}
253
254/// Creates a [`SourceInfoError`] for unrecoverable issues.
255///
256/// Takes an optional `line` on which the issue occurred and a `message`.
257pub fn unrecoverable(line: Option<usize>, message: impl ToString) -> SourceInfoError {
258 SourceInfoError {
259 error_type: SourceInfoErrorType::Unrecoverable,
260 line,
261 message: message.to_string(),
262 }
263}
264
265/// Creates a [`SourceInfoError`] for linting issues.
266///
267/// Takes an optional `line` on which the issue occurred and a `message`.
268pub fn lint(line: Option<usize>, message: impl ToString) -> SourceInfoError {
269 SourceInfoError {
270 error_type: SourceInfoErrorType::LintWarning,
271 line,
272 message: message.to_string(),
273 }
274}
275
276/// Creates a [`SourceInfoError`] for deprecation warnings.
277///
278/// Takes an optional `line` on which the issue occurred and a `message`.
279pub fn deprecation(line: Option<usize>, message: impl ToString) -> SourceInfoError {
280 SourceInfoError {
281 error_type: SourceInfoErrorType::DeprecationWarning,
282 line,
283 message: message.to_string(),
284 }
285}