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