alpm_lint/issue/
mod.rs

1//! Generic representation of a lint issue.
2
3use std::{collections::BTreeMap, fmt};
4
5use alpm_types::SystemArchitecture;
6use colored::{ColoredString, Colorize};
7use serde::{Deserialize, Serialize};
8
9use crate::{Level, LintRule, LintScope};
10
11pub mod display;
12
13use display::LintIssueDisplay;
14
15/// An issue a [`LintRule`] may encounter.
16#[derive(Clone, Debug, Deserialize, Serialize)]
17pub struct LintIssue {
18    /// The name of the lint rule that triggers this error.
19    pub lint_rule: String,
20    /// The severity level of this issue.
21    pub level: Level,
22    /// The help text that is displayed when the issue is encountered.
23    pub help_text: String,
24    /// The scope in which the lint is discovered.
25    pub scope: LintScope,
26    /// The type of issue that is encountered.
27    pub issue_type: LintIssueType,
28    /// Links that can be appended to an issue.
29    /// Stored as a map of `name -> URL`.
30    pub links: BTreeMap<String, String>,
31}
32
33impl LintIssue {
34    /// Creates a new [`LintIssue`] from a [`LintRule`] and [`LintIssueType`].
35    pub fn from_rule<T: LintRule>(rule: &T, issue_type: LintIssueType) -> Self {
36        LintIssue {
37            lint_rule: rule.scoped_name(),
38            level: rule.level(),
39            help_text: rule.help_text(),
40            scope: rule.scope(),
41            issue_type,
42            links: rule.extra_links().unwrap_or_default(),
43        }
44    }
45}
46
47impl fmt::Display for LintIssue {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        write!(f, "{}", Into::<LintIssueDisplay>::into(self.clone()))
50    }
51}
52
53impl From<LintIssue> for LintIssueDisplay {
54    /// Convert this [`LintIssue`] into a [`LintIssueDisplay`] for formatted output.
55    fn from(other: LintIssue) -> LintIssueDisplay {
56        let mut summary = None;
57        let mut arrow_line = None;
58        let message = match other.issue_type {
59            LintIssueType::SourceInfo(issue) => match issue {
60                SourceInfoIssue::Generic {
61                    summary: inner_summary,
62
63                    arrow_line: inner_arrow_line,
64                    message,
65                } => {
66                    arrow_line = inner_arrow_line;
67                    summary = Some(inner_summary);
68                    message
69                }
70                SourceInfoIssue::BaseField {
71                    field_name,
72                    value,
73                    context,
74                    architecture,
75                } => {
76                    arrow_line = Some(format!(
77                        "in field '{}'",
78                        SourceInfoIssue::field_fmt(&field_name, architecture)
79                    ));
80                    format!("{context}: {value}")
81                }
82                SourceInfoIssue::PackageField {
83                    field_name,
84                    value,
85                    context,
86                    architecture,
87                    package_name,
88                } => {
89                    arrow_line = Some(format!(
90                        "in field '{}' for package '{}'",
91                        SourceInfoIssue::field_fmt(&field_name, architecture),
92                        package_name.bold()
93                    ));
94                    format!("{context}: {value}")
95                }
96                SourceInfoIssue::MissingField { field_name } => {
97                    format!("Field '{}' is required but missing", field_name.bold())
98                }
99            },
100        };
101
102        LintIssueDisplay {
103            level: other.level,
104            scoped_name: other.lint_rule,
105            summary,
106            arrow_line,
107            message,
108            help_text: other.help_text,
109            custom_links: other.links,
110        }
111    }
112}
113
114/// The type of issue that may be encountered during linting.
115///
116/// This is used to categorize lint issues and to provide detailed data
117/// for good error messages for each type of issue.
118#[derive(Clone, Debug, Deserialize, Serialize)]
119pub enum LintIssueType {
120    /// All issues that can be encountered when linting a [SRCINFO] file.
121    ///
122    /// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
123    SourceInfo(SourceInfoIssue),
124}
125
126/// A specific type of [SRCINFO] related lint issues that may be encountered during linting.
127///
128/// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
129#[derive(Clone, Debug, Deserialize, Serialize)]
130pub enum SourceInfoIssue {
131    /// A generic issue that only consists of some text without any additional fields.
132    ///
133    /// Use this for one-off issues that don't fit any other "issue category".
134    /// The lint rule must take care of the formatting itself.
135    ///
136    /// # Note
137    ///
138    /// If you find yourself using this variant multiple times in a similar manner, consider
139    /// creating a dedicated variant for that use case.
140    Generic {
141        /// A brief, one-line summary of the issue for display above the main error line.
142        ///
143        /// This is used to populate [`LintIssueDisplay::summary`].
144        summary: String,
145
146        /// Additional context that can be displayed between summary and message.
147        ///
148        /// This is used to populate [`LintIssueDisplay::arrow_line`].
149        arrow_line: Option<String>,
150
151        /// The detailed message describing this issue, shown in the context section.
152        ///
153        /// This can contain more specific information about what was found and where.
154        ///
155        /// This is used to populate [`LintIssueDisplay::message`].
156        message: String,
157    },
158
159    /// A lint issue on a `PackageBase` field.
160    BaseField {
161        /// The field name which causes the issue.
162        ///
163        /// Used as [`LintIssueDisplay::arrow_line`] in the form of:
164        /// `in field {field_name}`
165        field_name: String,
166
167        /// The value that causes the issue.
168        ///
169        /// Used as [`LintIssueDisplay::message`] in the form of:
170        /// `"{context}: {value}"`
171        value: String,
172
173        /// Additional context that describes what kind of issue is found.
174        ///
175        /// Used as [`LintIssueDisplay::message`] in the form of:
176        /// `"{context}: {value}"`
177        context: String,
178
179        /// The architecture in case the field is architecture specific.
180        ///
181        /// If this is set, it'll be used as [`LintIssueDisplay::message`] in the form of:
182        /// `"{context}: {value} for architecture {arch}"`
183        architecture: Option<SystemArchitecture>,
184    },
185
186    /// A lint issue on a field that belongs to a specific package.
187    PackageField {
188        /// The field name which causes the issue.
189        ///
190        /// Used as [`LintIssueDisplay::arrow_line`] in the form of:
191        /// `format!("in field {field_name} for package {package_name}")`
192        field_name: String,
193
194        /// The name of the package for which the issue is detected.
195        ///
196        /// Used as [`LintIssueDisplay::arrow_line`] in the form of:
197        /// `"in field {field_name} for package {package_name}"`
198        package_name: String,
199
200        /// The value that causes the issue.
201        ///
202        /// Used as [`LintIssueDisplay::message`] in the form of:
203        /// `"{context}: {value}"`
204        value: String,
205
206        /// Additional context that describes what kind of issue is found.
207        ///
208        /// Used as [`LintIssueDisplay::message`] in the form of:
209        /// `"{context}: {value}"`
210        context: String,
211
212        /// The architecture in case the field is architecture specific.
213        ///
214        /// If this is set, it'll be used as [`LintIssueDisplay::message`] in the form of:
215        /// `"{context}: {value} for architecture {arch}"`
216        architecture: Option<SystemArchitecture>,
217    },
218
219    /// A required field is missing from the package base.
220    MissingField {
221        /// The name of the field that is missing.
222        field_name: String,
223    },
224}
225
226impl SourceInfoIssue {
227    /// Takes a field name with an optional architecture and returns the correct
228    /// [SRCINFO] formatting as bold text.
229    ///
230    /// [SRCINFO]: https://alpm.archlinux.page/specifications/SRCINFO.5.html
231    pub fn field_fmt(field_name: &str, architecture: Option<SystemArchitecture>) -> ColoredString {
232        match architecture {
233            Some(arch) => format!("{field_name}_{arch}").bold(),
234            None => field_name.bold(),
235        }
236    }
237}
238
239impl From<SourceInfoIssue> for LintIssueType {
240    fn from(issue: SourceInfoIssue) -> Self {
241        LintIssueType::SourceInfo(issue)
242    }
243}