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}