alpm_srcinfo/
error.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
//! All error types that are exposed by this crate.
use std::{fmt::Display, path::PathBuf, string::FromUtf8Error};

use colored::Colorize;
use thiserror::Error;

#[cfg(doc)]
use crate::{parser::SourceInfoContent, source_info::SourceInfo};

/// The high-level error that can occur when using this crate.
///
/// Notably, it contains two important enums in the context of parsing:
/// - `ParseError` is a already formatted error generated by the `winnow` parser. This effectively
///   means that some invalid data has been encountered.
/// - `SourceInfoErrors` is a list of all logical or lint errors that're encountered in the final
///   step. This error also contains the original file on which the errors occurred.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
    /// ALPM type error
    #[error("ALPM type parse error: {0}")]
    AlpmType(#[from] alpm_types::Error),

    /// IO error
    #[error("I/O error while {0}:\n{1}")]
    Io(&'static str, std::io::Error),

    /// IO error with additional path info for more context.
    #[error("I/O error at path {0:?} while {1}:\n{2}")]
    IoPath(PathBuf, &'static str, std::io::Error),

    /// UTF-8 parse error when reading the input file.
    #[error(transparent)]
    InvalidUTF8(#[from] FromUtf8Error),

    /// No input file given.
    ///
    /// This error only occurs when running the [`crate::commands`] functions.
    #[error("No input file given.")]
    NoInputFile,

    /// A parsing error that occurred during winnow file parsing.
    #[error("File parsing error:\n{0}")]
    ParseError(String),

    /// A list of errors that occurred during the final SRCINFO data parsing step.
    ///
    /// These may contain any combination of [`SourceInfoError`].
    #[error("Errors while parsing SRCINFO data:\n\n{0}")]
    SourceInfoErrors(SourceInfoErrors),

    /// JSON error while creating JSON formatted output.
    ///
    /// This error only occurs when running the [`crate::commands`] functions.
    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),
}

/// A helper struct to provide proper line based error/linting messages.
///
/// Provides a list of [`SourceInfoError`]s and the SRCINFO data in which the errors occurred.
#[derive(Debug, Clone)]
pub struct SourceInfoErrors {
    inner: Vec<SourceInfoError>,
    file_content: String,
}

impl Display for SourceInfoErrors {
    /// Display all errors in one big well-formatted error message.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        // We go through all errors and print them out one after another.
        let mut error_iter = self.inner.iter().enumerate().peekable();
        while let Some((index, error)) = error_iter.next() {
            // Line and message are generic and the same for every error message.
            let line_nr = error.line;
            let message = &error.message;

            // Build the the headline based on the error type.
            let specific_line =
                line_nr.map_or("".to_string(), |line| format!(" on line {}", line + 1));
            let headline = match error.error_type {
                SourceInfoErrorType::LintWarning => {
                    format!("{}{specific_line}:", "Linter Warning".yellow())
                }
                SourceInfoErrorType::DeprecationWarning => {
                    format!("{}{specific_line}:", "Deprecation Warning".yellow())
                }
                SourceInfoErrorType::Unrecoverable => {
                    format!("{}{specific_line}:", "Logical Error".red())
                }
            };

            // Write the headline
            let error_index = format!("[{index}]").bold().red();
            // Print the error details slightly indented based on the length of the error index
            // prefix.
            let indentation = " ".repeat(error_index.len() + 1);
            write!(f, "{error_index} {headline}")?;
            // Add the line, if it exists.
            // Prefix it with a bold line number for better visibility.
            if let Some(line_nr) = line_nr {
                let content_line = self
                    .file_content
                    .lines()
                    .nth(line_nr)
                    .expect("Error: Couldn't seek to line. Please report bug upstream.");
                // Lines aren't 0 indexed.
                let human_line_nr = line_nr + 1;
                write!(
                    f,
                    "\n{indentation}{} {content_line }\n",
                    format!("{human_line_nr}: |").to_string().bold()
                )?;
            }

            // Write the message with some spacing
            write!(f, "\n{indentation}{message}")?;

            // Write two newlines with a red separator between this and the next error
            if error_iter.peek().is_some() {
                write!(f, "\n\n{}", "──────────────────────────────\n".bold())?;
            }
        }

        Ok(())
    }
}

impl SourceInfoErrors {
    /// Creates a new [`SourceInfoErrors`].
    pub fn new(errors: Vec<SourceInfoError>, file_content: String) -> Self {
        Self {
            inner: errors,
            file_content,
        }
    }

    /// Filters the inner errors based on a given closure.
    pub fn filter<F>(&mut self, filter: F)
    where
        F: Fn(&SourceInfoError) -> bool,
    {
        self.inner.retain(filter);
    }

    /// Returns a reference to the list of errors.
    pub fn errors(&self) -> &Vec<SourceInfoError> {
        &self.inner
    }

    /// Filters for and errors on unrecoverable errors.
    ///
    /// Consumes `self` and simply returns if `self` contains no [`SourceInfoError`] of type
    /// [`SourceInfoErrorType::Unrecoverable`].
    ///
    /// # Errors
    ///
    /// Returns an error if `self` contains any [`SourceInfoError`] of type
    /// [`SourceInfoErrorType::Unrecoverable`].
    pub fn check_unrecoverable_errors(mut self) -> Result<(), Error> {
        // Filter only for errors that're unrecoverable, i.e. critical.
        self.filter(|err| matches!(err.error_type, SourceInfoErrorType::Unrecoverable));

        if !self.inner.is_empty() {
            self.sort_errors();
            return Err(Error::SourceInfoErrors(self));
        }

        Ok(())
    }

    /// Sorts the errors.
    ///
    /// The following order is applied:
    ///
    /// - Hard errors without line numbers
    /// - Hard errors with line numbers, by ascending line number
    /// - Deprecation warnings without line numbers
    /// - Deprecation warnings with line numbers, by ascending line number
    /// - Linter warnings without line numbers
    /// - Linter warnings with line numbers, by ascending line number
    fn sort_errors(&mut self) {
        self.inner.sort_by(|a, b| {
            use std::cmp::Ordering;

            let prio = |error: &SourceInfoError| match error.error_type {
                SourceInfoErrorType::Unrecoverable => 0,
                SourceInfoErrorType::DeprecationWarning => 1,
                SourceInfoErrorType::LintWarning => 2,
            };

            // Extract ordering criteria based on error type.
            let a_prio = prio(a);
            let b_prio = prio(b);

            // Compare by error severity first.
            match a_prio.cmp(&b_prio) {
                // If it's the same error, do a comparison on a line basis.
                // Unspecific errors should come first!
                Ordering::Equal => match (a.line, b.line) {
                    (Some(a), Some(b)) => a.cmp(&b),
                    (Some(_), None) => Ordering::Less,
                    (None, Some(_)) => Ordering::Greater,
                    (None, None) => Ordering::Equal,
                },
                // If it's not the same error, the ordering is clear.
                other => other,
            }
        });
    }
}

/// Errors that may occur when converting [`SourceInfoContent`] into a [`SourceInfo`].
///
/// The severity of an error is defined by its [`SourceInfoErrorType`], which may range from linting
/// errors, deprecation warnings to hard unrecoverable errors.
#[derive(Debug, Clone)]
pub struct SourceInfoError {
    pub error_type: SourceInfoErrorType,
    pub line: Option<usize>,
    pub message: String,
}

/// A [`SourceInfoError`] type.
///
/// Provides context for the severity of a [`SourceInfoError`].
/// The type of "error" that has occurred.
#[derive(Debug, Clone)]
pub enum SourceInfoErrorType {
    /// A simple linter error type. Can be ignored but should be fixed.
    LintWarning,
    /// Something changed in the SRCINFO format and this should be removed for future
    /// compatibility.
    DeprecationWarning,
    /// A hard unrecoverable logic error has been detected.
    /// The returned [SourceInfo] representation is faulty and should not be used.
    Unrecoverable,
}

/// Creates a [`SourceInfoError`] for unrecoverable issues.
///
/// Takes an optional `line` on which the issue occurred and a `message`.
pub fn unrecoverable(line: Option<usize>, message: impl ToString) -> SourceInfoError {
    SourceInfoError {
        error_type: SourceInfoErrorType::Unrecoverable,
        line,
        message: message.to_string(),
    }
}

/// Creates a [`SourceInfoError`] for linting issues.
///
/// Takes an optional `line` on which the issue occurred and a `message`.
pub fn lint(line: Option<usize>, message: impl ToString) -> SourceInfoError {
    SourceInfoError {
        error_type: SourceInfoErrorType::LintWarning,
        line,
        message: message.to_string(),
    }
}

/// Creates a [`SourceInfoError`] for deprecation warnings.
///
/// Takes an optional `line` on which the issue occurred and a `message`.
pub fn deprecation(line: Option<usize>, message: impl ToString) -> SourceInfoError {
    SourceInfoError {
        error_type: SourceInfoErrorType::DeprecationWarning,
        line,
        message: message.to_string(),
    }
}