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}