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}