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 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 // Header with level and lint rule
66 let level_str = match self.level {
67 Level::Error => "error".bold().red(),
68 Level::Deny => "denied".bold().red(),
69 Level::Warn => "warning".bold().yellow(),
70 Level::Suggest => "suggestion".bold().bright_blue(),
71 };
72
73 // Header line
74 write!(f, "{}[{}]", level_str, self.scoped_name.blue().bold())?;
75 // Optionally append summary to header line or add a newline.
76 if let Some(summary) = &self.summary {
77 writeln!(f, ": {}", summary.bright_white())?;
78 } else {
79 writeln!(f)?;
80 }
81
82 // Optional context
83 if let Some(arrow_line) = &self.arrow_line {
84 writeln!(f, " {} {}", "-->".bright_blue().bold(), arrow_line)?;
85 }
86
87 // Start the pipe section.
88 // A top and bottom pipe are added for better visual differentiation.
89 writeln!(f, " {}", "|".bright_blue().bold())?;
90 for line in self.message.lines() {
91 writeln!(f, " {} {}", "|".bright_blue().bold(), line)?;
92 }
93 writeln!(f, " {}", "|".bright_blue().bold())?;
94
95 let mut is_first_line = true;
96 let help_word = "help";
97 for line in self.help_text.lines() {
98 // Prefix the very first line with a `help: `.
99 if is_first_line {
100 writeln!(f, "{help_word}: {}", line.bright_white())?;
101 is_first_line = false;
102 continue;
103 }
104
105 // Don't indent empty lines
106 if line.is_empty() {
107 writeln!(f)?;
108 } else {
109 // The indentation is the length of the "help" word + 2 for the literal `: `.
110 let indentation = " ".repeat(help_word.len() + 2);
111 writeln!(f, "{indentation}{}", line.bright_white())?;
112 }
113 }
114
115 fn write_link(f: &mut fmt::Formatter<'_>, name: &str, url: &str) -> fmt::Result {
116 writeln!(f, " = {}: {}", name.cyan(), url.underline())
117 }
118
119 // Add custom links
120 for (name, url) in &self.custom_links {
121 write_link(f, name, url)?;
122 }
123
124 // Auto-generated documentation URL
125 let doc_url = &format!("{ALPM_LINT_WEBSITE}#{}", self.scoped_name);
126 write_link(f, "see", doc_url)?;
127
128 Ok(())
129 }
130}