alpm_srcinfo/
error.rs

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