Skip to main content

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