Skip to main content

whitaker_common/
diagnostics.rs

1//! Ergonomic builders for lint diagnostics and suggestions.
2#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
3
4use crate::span::SourceSpan;
5
6/// Applicability of a suggestion, mirroring rustc semantics.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub enum Applicability {
9    /// The suggestion can be applied mechanically.
10    MachineApplicable,
11    /// The suggestion is likely correct but not guaranteed.
12    MaybeIncorrect,
13    /// The suggestion contains placeholders requiring manual edits.
14    HasPlaceholders,
15    /// Applicability is not specified.
16    Unspecified,
17}
18
19/// Represents a fix-it suggestion.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct Suggestion {
22    message: String,
23    replacement: String,
24    applicability: Applicability,
25}
26
27impl Suggestion {
28    /// Creates a new suggestion.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use whitaker_common::diagnostics::{Applicability, Suggestion};
34    ///
35    /// let suggestion = Suggestion::new("Use expect", "expect(...)", Applicability::MaybeIncorrect);
36    /// assert_eq!(suggestion.message(), "Use expect");
37    /// ```
38    #[must_use]
39    pub fn new(
40        message: impl Into<String>,
41        replacement: impl Into<String>,
42        applicability: Applicability,
43    ) -> Self {
44        Self {
45            message: message.into(),
46            replacement: replacement.into(),
47            applicability,
48        }
49    }
50
51    /// Returns the human-readable message.
52    #[must_use]
53    pub fn message(&self) -> &str {
54        &self.message
55    }
56
57    /// Returns the replacement snippet.
58    #[must_use]
59    pub fn replacement(&self) -> &str {
60        &self.replacement
61    }
62
63    /// Returns the applicability classification.
64    #[must_use]
65    pub const fn applicability(&self) -> Applicability {
66        self.applicability
67    }
68}
69
70/// Represents a lint diagnostic with optional notes and suggestions.
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct Diagnostic {
73    code: String,
74    message: String,
75    span: SourceSpan,
76    notes: Vec<String>,
77    helps: Vec<String>,
78    suggestions: Vec<Suggestion>,
79}
80
81impl Diagnostic {
82    /// Returns the lint code.
83    #[must_use]
84    pub fn code(&self) -> &str {
85        &self.code
86    }
87
88    /// Returns the primary message.
89    #[must_use]
90    pub fn message(&self) -> &str {
91        &self.message
92    }
93
94    /// Returns the primary span.
95    #[must_use]
96    pub const fn span(&self) -> SourceSpan {
97        self.span
98    }
99
100    /// Returns additional diagnostic notes.
101    #[must_use]
102    pub fn notes(&self) -> &[String] {
103        &self.notes
104    }
105
106    /// Returns help messages.
107    #[must_use]
108    pub fn helps(&self) -> &[String] {
109        &self.helps
110    }
111
112    /// Returns collected suggestions.
113    #[must_use]
114    pub fn suggestions(&self) -> &[Suggestion] {
115        &self.suggestions
116    }
117}
118
119/// Builder for [`Diagnostic`] instances.
120pub struct DiagnosticBuilder {
121    diagnostic: Diagnostic,
122}
123
124impl DiagnosticBuilder {
125    fn new(code: impl Into<String>, message: impl Into<String>, span: SourceSpan) -> Self {
126        Self {
127            diagnostic: Diagnostic {
128                code: code.into(),
129                message: message.into(),
130                span,
131                notes: Vec::new(),
132                helps: Vec::new(),
133                suggestions: Vec::new(),
134            },
135        }
136    }
137
138    /// Adds a note to the diagnostic.
139    #[must_use]
140    pub fn note(mut self, note: impl Into<String>) -> Self {
141        self.diagnostic.notes.push(note.into());
142        self
143    }
144
145    /// Adds a help message to the diagnostic.
146    #[must_use]
147    pub fn help(mut self, help: impl Into<String>) -> Self {
148        self.diagnostic.helps.push(help.into());
149        self
150    }
151
152    /// Adds a suggestion to the diagnostic.
153    #[must_use]
154    pub fn suggestion(mut self, suggestion: Suggestion) -> Self {
155        self.diagnostic.suggestions.push(suggestion);
156        self
157    }
158
159    /// Completes the builder and returns the diagnostic.
160    #[must_use]
161    pub fn build(self) -> Diagnostic {
162        self.diagnostic
163    }
164}
165
166/// Starts building a lint diagnostic for a given span.
167///
168/// # Examples
169///
170/// ```
171/// use whitaker_common::diagnostics::{span_lint, Applicability, Suggestion};
172/// use whitaker_common::span::{SourceLocation, SourceSpan};
173///
174/// let span = SourceSpan::new(SourceLocation::new(1, 0), SourceLocation::new(1, 4)).expect("valid span for example");
175/// let diagnostic = span_lint("demo", "Example", span)
176///     .help("Consider refactoring")
177///     .suggestion(Suggestion::new("Use helper", "helper()", Applicability::MaybeIncorrect))
178///     .build();
179/// assert_eq!(diagnostic.code(), "demo");
180/// ```
181#[must_use]
182pub fn span_lint(
183    code: impl Into<String>,
184    message: impl Into<String>,
185    span: SourceSpan,
186) -> DiagnosticBuilder {
187    DiagnosticBuilder::new(code, message, span)
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use crate::span::{SourceLocation, SourceSpan};
194    use rstest::rstest;
195
196    #[rstest]
197    fn builds_diagnostic() {
198        let span = SourceSpan::new(SourceLocation::new(2, 1), SourceLocation::new(2, 5))
199            .expect("valid span for diagnostic test");
200        let diagnostic = span_lint("lint", "Message", span)
201            .note("Note")
202            .help("Help")
203            .suggestion(Suggestion::new(
204                "Fix",
205                "fix()",
206                Applicability::MachineApplicable,
207            ))
208            .build();
209
210        assert_eq!(diagnostic.code(), "lint");
211        assert_eq!(diagnostic.notes(), &[String::from("Note")]);
212        assert_eq!(diagnostic.helps(), &[String::from("Help")]);
213        assert_eq!(diagnostic.suggestions().len(), 1);
214    }
215}