Skip to main content

use_diagnostic_label/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5
6use use_diagnostic_message::DiagnosticMessage;
7use use_diagnostic_span::DiagnosticSpan;
8
9/// The role of a diagnostic label.
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum DiagnosticLabelKind {
12    /// The primary location or reason for a diagnostic.
13    Primary,
14    /// Additional related context.
15    Secondary,
16    /// Help-oriented context.
17    Help,
18    /// Note-oriented context.
19    Note,
20}
21
22impl DiagnosticLabelKind {
23    /// Returns the canonical lowercase label kind.
24    #[must_use]
25    pub const fn as_str(self) -> &'static str {
26        match self {
27            Self::Primary => "primary",
28            Self::Secondary => "secondary",
29            Self::Help => "help",
30            Self::Note => "note",
31        }
32    }
33}
34
35impl fmt::Display for DiagnosticLabelKind {
36    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
37        formatter.write_str(self.as_str())
38    }
39}
40
41/// Renderer-neutral context attached to a diagnostic.
42#[derive(Clone, Debug, Eq, Hash, PartialEq)]
43pub struct DiagnosticLabel {
44    kind: DiagnosticLabelKind,
45    message: DiagnosticMessage,
46    span: Option<DiagnosticSpan>,
47}
48
49impl DiagnosticLabel {
50    /// Creates a diagnostic label.
51    #[must_use]
52    pub const fn new(
53        kind: DiagnosticLabelKind,
54        message: DiagnosticMessage,
55        span: Option<DiagnosticSpan>,
56    ) -> Self {
57        Self {
58            kind,
59            message,
60            span,
61        }
62    }
63
64    /// Creates a primary label with a span.
65    #[must_use]
66    pub const fn primary(message: DiagnosticMessage, span: DiagnosticSpan) -> Self {
67        Self::new(DiagnosticLabelKind::Primary, message, Some(span))
68    }
69
70    /// Creates a secondary label with a span.
71    #[must_use]
72    pub const fn secondary(message: DiagnosticMessage, span: DiagnosticSpan) -> Self {
73        Self::new(DiagnosticLabelKind::Secondary, message, Some(span))
74    }
75
76    /// Creates a help label without a span.
77    #[must_use]
78    pub const fn help(message: DiagnosticMessage) -> Self {
79        Self::new(DiagnosticLabelKind::Help, message, None)
80    }
81
82    /// Creates a note label without a span.
83    #[must_use]
84    pub const fn note(message: DiagnosticMessage) -> Self {
85        Self::new(DiagnosticLabelKind::Note, message, None)
86    }
87
88    /// Returns the label kind.
89    #[must_use]
90    pub const fn kind(&self) -> DiagnosticLabelKind {
91        self.kind
92    }
93
94    /// Returns the label message.
95    #[must_use]
96    pub const fn message(&self) -> &DiagnosticMessage {
97        &self.message
98    }
99
100    /// Returns the optional label span.
101    #[must_use]
102    pub const fn span(&self) -> Option<&DiagnosticSpan> {
103        self.span.as_ref()
104    }
105}
106
107impl fmt::Display for DiagnosticLabel {
108    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
109        self.message.fmt(formatter)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::{DiagnosticLabel, DiagnosticLabelKind};
116    use use_diagnostic_message::DiagnosticMessage;
117    use use_diagnostic_span::{DiagnosticPosition, DiagnosticSpan};
118
119    fn test_span() -> DiagnosticSpan {
120        let start = DiagnosticPosition::new(2, 5).expect("position should be valid");
121        let end = DiagnosticPosition::new(2, 9).expect("position should be valid");
122        DiagnosticSpan::without_source(start, end).expect("span should be valid")
123    }
124
125    #[test]
126    fn creates_primary_label_with_span() {
127        let label = DiagnosticLabel::primary(
128            DiagnosticMessage::new("invalid value").expect("message should be valid"),
129            test_span(),
130        );
131
132        assert_eq!(label.kind(), DiagnosticLabelKind::Primary);
133        assert!(label.span().is_some());
134    }
135
136    #[test]
137    fn creates_secondary_label_with_span() {
138        let label = DiagnosticLabel::secondary(
139            DiagnosticMessage::new("defined here").expect("message should be valid"),
140            test_span(),
141        );
142
143        assert_eq!(label.kind(), DiagnosticLabelKind::Secondary);
144        assert!(label.span().is_some());
145    }
146
147    #[test]
148    fn creates_help_label_without_span() {
149        let label = DiagnosticLabel::help(
150            DiagnosticMessage::new("provide the missing field").expect("message should be valid"),
151        );
152
153        assert_eq!(label.kind(), DiagnosticLabelKind::Help);
154        assert!(label.span().is_none());
155    }
156
157    #[test]
158    fn display_and_debug_do_not_assume_renderer_output() {
159        let label = DiagnosticLabel::note(
160            DiagnosticMessage::new("plain context").expect("message should be valid"),
161        );
162
163        assert_eq!(label.to_string(), "plain context");
164        assert!(format!("{label:?}").contains("Note"));
165    }
166}