alpm_lint/issue/display.rs
1//! Generic representation of human readable lint issue messages.
2//!
3//! Provides the [`LintIssueDisplay`] type, which defines a uniform format for displaying issue
4//! messages.
5
6use std::{collections::BTreeMap, fmt};
7
8use colored::Colorize;
9
10use crate::Level;
11
12const ALPM_LINT_WEBSITE: &str = "https://alpm.archlinux.page/lints/index.html";
13
14/// A generic structure that represents all possible components of a lint issue display.
15///
16/// The actual layouting is done in the [`fmt::Display`] implementation of [`LintIssueDisplay`].
17///
18/// # Visual Layout
19///
20/// ```text
21/// level[scoped_name]: summary <- header with optional summary
22/// --> arrow_line <- arrow line with context (optional)
23/// |
24/// | message <- main issue description
25/// |
26/// help: help_text line 1 <- help section
27/// help_text line 2...
28/// = custom_link: url <- custom links (optional)
29/// = see: documentation_url <- auto-generated doc link
30/// ```
31///
32/// # Examples
33///
34/// ```text
35/// warning[source_info::duplicate_architecture]
36/// --> in field 'arch' for package 'example'
37/// |
38/// | found duplicate value: x86_64
39/// |
40/// help: Architecture lists should be unique.
41/// = see: https://alpm.archlinux.page/lints/...
42/// ```
43#[derive(Clone, Debug)]
44// Allow missing docs, as the individual fields are better explained via the graphic on the struct.
45#[allow(missing_docs)]
46pub struct LintIssueDisplay {
47 /// The lint level of the lint rule.
48 pub level: Level,
49 /// The full name of the lint rule.
50 pub scoped_name: String,
51 /// The optional summary of the lint rule.
52 pub summary: Option<String>,
53 /// The optional information on where the issue occurs.
54 pub arrow_line: Option<String>,
55 /// A message outlining what the specific issue is.
56 pub message: String,
57 /// A help text outlining what can be done to fix the issue.
58 pub help_text: String,
59 /// A map of additional URL names and URLs.
60 pub custom_links: BTreeMap<String, String>,
61}
62
63impl fmt::Display for LintIssueDisplay {
64 // TODO(cleanup): Investigate the indexing_slicing
65 #[expect(clippy::arithmetic_side_effects)]
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 // Header with level and lint rule
68 let level_str = match self.level {
69 Level::Error => "error".bold().red(),
70 Level::Deny => "denied".bold().red(),
71 Level::Warn => "warning".bold().yellow(),
72 Level::Suggest => "suggestion".bold().bright_blue(),
73 };
74
75 // Header line
76 write!(f, "{}[{}]", level_str, self.scoped_name.blue().bold())?;
77 // Optionally append summary to header line or add a newline.
78 if let Some(summary) = &self.summary {
79 writeln!(f, ": {}", summary.bright_white())?;
80 } else {
81 writeln!(f)?;
82 }
83
84 // Optional context
85 if let Some(arrow_line) = &self.arrow_line {
86 writeln!(f, " {} {}", "-->".bright_blue().bold(), arrow_line)?;
87 }
88
89 // Start the pipe section.
90 // A top and bottom pipe are added for better visual differentiation.
91 writeln!(f, " {}", "|".bright_blue().bold())?;
92 for line in self.message.lines() {
93 writeln!(f, " {} {}", "|".bright_blue().bold(), line)?;
94 }
95 writeln!(f, " {}", "|".bright_blue().bold())?;
96
97 let mut is_first_line = true;
98 let help_word = "help";
99 for line in self.help_text.lines() {
100 // Prefix the very first line with a `help: `.
101 if is_first_line {
102 writeln!(f, "{help_word}: {}", line.bright_white())?;
103 is_first_line = false;
104 continue;
105 }
106
107 // Don't indent empty lines
108 if line.is_empty() {
109 writeln!(f)?;
110 } else {
111 // The indentation is the length of the "help" word + 2 for the literal `: `.
112 let indentation = " ".repeat(help_word.len() + 2);
113 writeln!(f, "{indentation}{}", line.bright_white())?;
114 }
115 }
116
117 fn write_link(f: &mut fmt::Formatter<'_>, name: &str, url: &str) -> fmt::Result {
118 writeln!(f, " = {}: {}", name.cyan(), url.underline())
119 }
120
121 // Add custom links
122 for (name, url) in &self.custom_links {
123 write_link(f, name, url)?;
124 }
125
126 // Auto-generated documentation URL
127 let doc_url = &format!("{ALPM_LINT_WEBSITE}#{}", self.scoped_name);
128 write_link(f, "see", doc_url)?;
129
130 Ok(())
131 }
132}