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