wdl_core/concern/lint/
warning.rs

1//! Lint warnings.
2
3use crate::concern::lint::Level;
4use crate::concern::lint::TagSet;
5use crate::concern::Code;
6use crate::display;
7use crate::file::Location;
8
9mod builder;
10
11pub use builder::Builder;
12use nonempty::NonEmpty;
13use serde::Deserialize;
14use serde::Serialize;
15
16/// A lint warning.
17#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
18pub struct Warning {
19    /// The code.
20    code: Code,
21
22    /// The lint level.
23    level: Level,
24
25    /// The lint tags.
26    tags: TagSet,
27
28    /// The locations.
29    locations: NonEmpty<Location>,
30
31    /// The subject.
32    subject: String,
33
34    /// The body.
35    body: String,
36
37    /// The (optional) text to describe how to fix the issue.
38    fix: Option<String>,
39}
40
41impl Warning {
42    /// Gets the code for this [`Warning`].
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use std::path::PathBuf;
48    ///
49    /// use wdl_core::concern::code::Kind;
50    /// use wdl_core::concern::lint;
51    /// use wdl_core::concern::lint::warning::Builder;
52    /// use wdl_core::concern::lint::Level;
53    /// use wdl_core::concern::lint::TagSet;
54    /// use wdl_core::concern::Code;
55    /// use wdl_core::file::Location;
56    /// use wdl_core::Version;
57    ///
58    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
59    /// let warning = Builder::default()
60    ///     .code(code)
61    ///     .level(Level::High)
62    ///     .tags(TagSet::new(&[lint::Tag::Style]))
63    ///     .push_location(Location::Unplaced)
64    ///     .subject("Hello, world!")
65    ///     .body("A body.")
66    ///     .fix("How to fix the issue.")
67    ///     .try_build()?;
68    ///
69    /// assert_eq!(warning.code().grammar(), &Version::V1);
70    /// assert_eq!(warning.code().index().get(), 1);
71    ///
72    /// # Ok::<(), Box<dyn std::error::Error>>(())
73    /// ```
74    pub fn code(&self) -> &Code {
75        &self.code
76    }
77
78    /// Gets the lint level for this [`Warning`].
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use std::path::PathBuf;
84    ///
85    /// use wdl_core::concern::code::Kind;
86    /// use wdl_core::concern::lint;
87    /// use wdl_core::concern::lint::warning::Builder;
88    /// use wdl_core::concern::lint::Level;
89    /// use wdl_core::concern::lint::TagSet;
90    /// use wdl_core::concern::Code;
91    /// use wdl_core::file::Location;
92    /// use wdl_core::Version;
93    ///
94    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
95    /// let warning = Builder::default()
96    ///     .code(code)
97    ///     .level(Level::High)
98    ///     .tags(TagSet::new(&[lint::Tag::Style]))
99    ///     .push_location(Location::Unplaced)
100    ///     .subject("Hello, world!")
101    ///     .body("A body.")
102    ///     .fix("How to fix the issue.")
103    ///     .try_build()?;
104    ///
105    /// assert_eq!(warning.level(), &Level::High);
106    ///
107    /// # Ok::<(), Box<dyn std::error::Error>>(())
108    /// ```
109    pub fn level(&self) -> &Level {
110        &self.level
111    }
112
113    /// Gets the lint tags for this [`Warning`].
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use std::path::PathBuf;
119    ///
120    /// use wdl_core::concern::code::Kind;
121    /// use wdl_core::concern::lint;
122    /// use wdl_core::concern::lint::warning::Builder;
123    /// use wdl_core::concern::lint::Level;
124    /// use wdl_core::concern::lint::TagSet;
125    /// use wdl_core::concern::Code;
126    /// use wdl_core::file::Location;
127    /// use wdl_core::Version;
128    ///
129    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
130    /// let warning = Builder::default()
131    ///     .code(code)
132    ///     .level(Level::High)
133    ///     .tags(TagSet::new(&[lint::Tag::Style]))
134    ///     .push_location(Location::Unplaced)
135    ///     .subject("Hello, world!")
136    ///     .body("A body.")
137    ///     .fix("How to fix the issue.")
138    ///     .try_build()?;
139    ///
140    /// assert_eq!(warning.tags(), &TagSet::new(&[lint::Tag::Style]));
141    ///
142    /// # Ok::<(), Box<dyn std::error::Error>>(())
143    /// ```
144    pub fn tags(&self) -> &TagSet {
145        &self.tags
146    }
147
148    /// Gets the locations for this [`Warning`].
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// use std::path::PathBuf;
154    ///
155    /// use wdl_core::concern::code::Kind;
156    /// use wdl_core::concern::lint;
157    /// use wdl_core::concern::lint::warning::Builder;
158    /// use wdl_core::concern::lint::Level;
159    /// use wdl_core::concern::lint::TagSet;
160    /// use wdl_core::concern::Code;
161    /// use wdl_core::file::Location;
162    /// use wdl_core::Version;
163    ///
164    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
165    /// let warning = Builder::default()
166    ///     .code(code)
167    ///     .level(Level::High)
168    ///     .tags(TagSet::new(&[lint::Tag::Style]))
169    ///     .subject("Hello, world!")
170    ///     .push_location(Location::Unplaced)
171    ///     .body("A body.")
172    ///     .fix("How to fix the issue.")
173    ///     .try_build()?;
174    ///
175    /// assert_eq!(warning.locations().first(), &Location::Unplaced);
176    ///
177    /// # Ok::<(), Box<dyn std::error::Error>>(())
178    /// ```
179    pub fn locations(&self) -> &NonEmpty<Location> {
180        &self.locations
181    }
182
183    /// Gets the subject for this [`Warning`].
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use std::path::PathBuf;
189    ///
190    /// use wdl_core::concern::code::Kind;
191    /// use wdl_core::concern::lint;
192    /// use wdl_core::concern::lint::warning::Builder;
193    /// use wdl_core::concern::lint::Level;
194    /// use wdl_core::concern::lint::TagSet;
195    /// use wdl_core::concern::Code;
196    /// use wdl_core::file::Location;
197    /// use wdl_core::Version;
198    ///
199    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
200    /// let warning = Builder::default()
201    ///     .code(code)
202    ///     .level(Level::High)
203    ///     .tags(TagSet::new(&[lint::Tag::Style]))
204    ///     .subject("Hello, world!")
205    ///     .push_location(Location::Unplaced)
206    ///     .body("A body.")
207    ///     .fix("How to fix the issue.")
208    ///     .try_build()?;
209    ///
210    /// assert_eq!(warning.subject(), "Hello, world!");
211    ///
212    /// # Ok::<(), Box<dyn std::error::Error>>(())
213    /// ```
214    pub fn subject(&self) -> &str {
215        self.subject.as_ref()
216    }
217
218    /// Gets the body for this [`Warning`].
219    ///
220    /// # Examples
221    ///
222    /// ```
223    /// use std::path::PathBuf;
224    ///
225    /// use wdl_core::concern::code::Kind;
226    /// use wdl_core::concern::lint;
227    /// use wdl_core::concern::lint::warning::Builder;
228    /// use wdl_core::concern::lint::Level;
229    /// use wdl_core::concern::lint::TagSet;
230    /// use wdl_core::concern::Code;
231    /// use wdl_core::file::Location;
232    /// use wdl_core::Version;
233    ///
234    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
235    /// let warning = Builder::default()
236    ///     .code(code)
237    ///     .level(Level::High)
238    ///     .tags(TagSet::new(&[lint::Tag::Style]))
239    ///     .push_location(Location::Unplaced)
240    ///     .subject("Hello, world!")
241    ///     .body("A body.")
242    ///     .fix("How to fix the issue.")
243    ///     .try_build()?;
244    ///
245    /// assert_eq!(warning.body(), "A body.");
246    ///
247    /// # Ok::<(), Box<dyn std::error::Error>>(())
248    /// ```
249    pub fn body(&self) -> &str {
250        self.body.as_str()
251    }
252
253    /// Gets the fix text for this [`Warning`].
254    ///
255    /// # Examples
256    ///
257    /// ```
258    /// use std::path::PathBuf;
259    ///
260    /// use wdl_core::concern::code::Kind;
261    /// use wdl_core::concern::lint;
262    /// use wdl_core::concern::lint::warning::Builder;
263    /// use wdl_core::concern::lint::Level;
264    /// use wdl_core::concern::lint::TagSet;
265    /// use wdl_core::concern::Code;
266    /// use wdl_core::file::Location;
267    /// use wdl_core::Version;
268    ///
269    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
270    /// let warning = Builder::default()
271    ///     .code(code)
272    ///     .level(Level::High)
273    ///     .tags(TagSet::new(&[lint::Tag::Style]))
274    ///     .push_location(Location::Unplaced)
275    ///     .subject("Hello, world!")
276    ///     .body("A body.")
277    ///     .fix("How to fix the issue.")
278    ///     .try_build()?;
279    ///
280    /// assert_eq!(warning.fix(), Some("How to fix the issue."));
281    ///
282    /// # Ok::<(), Box<dyn std::error::Error>>(())
283    /// ```
284    pub fn fix(&self) -> Option<&str> {
285        self.fix.as_deref()
286    }
287
288    /// Displays a [`Warning`] according to the `mode` specified.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// use std::fmt::Write as _;
294    /// use std::path::PathBuf;
295    ///
296    /// use wdl_core::concern::lint::warning::Builder;
297    /// use wdl_core::concern::lint::TagSet;
298    /// use wdl_core::concern::lint;
299    /// use wdl_core::concern::lint::Level;
300    /// use wdl_core::concern::Code;
301    /// use wdl_core::concern::code::Kind;
302    /// use wdl_core::file::Location;
303    /// use wdl_core::Version;
304    /// use wdl_core::display;
305    ///
306    /// let code = Code::try_new(Kind::Warning, Version::V1, 1)?;
307    /// let warning = Builder::default()
308    ///     .code(code)
309    ///     .level(Level::High)
310    ///     .tags(TagSet::new(&[lint::Tag::Style]))
311    ///     .push_location(Location::Unplaced)
312    ///     .subject("Hello, world!")
313    ///     .body("A body.")
314    ///     .fix("Apply ample foobar.")
315    ///     .try_build()?;
316    ///
317    /// let mut result = String::new();
318    /// warning.display(&mut result, display::Mode::OneLine)?;
319    /// assert_eq!(result, String::from("[v1::W001::[Style]::High] Hello, world!"));
320    ///
321    /// result.clear();
322    /// warning.display(&mut result, display::Mode::Full)?;
323    /// assert_eq!(result, String::from("[v1::W001::[Style]::High] Hello, world!\n\nA body.\n\nTo fix this warning, apply ample foobar."));
324    ///
325    /// # Ok::<(), Box<dyn std::error::Error>>(())
326    pub fn display(&self, f: &mut impl std::fmt::Write, mode: display::Mode) -> std::fmt::Result {
327        match mode {
328            display::Mode::OneLine => display_one_line(self, f),
329            display::Mode::Full => display_full(self, f),
330        }
331    }
332}
333
334/// Displays the warning as a single line.
335fn display_one_line(warning: &Warning, f: &mut impl std::fmt::Write) -> std::fmt::Result {
336    write!(
337        f,
338        "[{}::{}::{:?}] {}",
339        warning.code, warning.tags, warning.level, warning.subject
340    )?;
341
342    let locations = warning
343        .locations
344        .iter()
345        .flat_map(|location| location.to_string())
346        .collect::<Vec<_>>();
347
348    if !locations.is_empty() {
349        write!(f, " ({})", locations.join(", "))?;
350    }
351
352    Ok(())
353}
354
355/// Displays all information about the warning.
356fn display_full(warning: &Warning, f: &mut impl std::fmt::Write) -> std::fmt::Result {
357    display_one_line(warning, f)?;
358    write!(f, "\n\n{}", warning.body)?;
359
360    if let Some(fix) = warning.fix() {
361        write!(f, "\n\nTo fix this warning, {}", fix.to_ascii_lowercase())?;
362    }
363
364    Ok(())
365}
366
367impl std::fmt::Display for Warning {
368    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369        self.display(f, display::Mode::OneLine)
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::concern::lint::Tag;
377
378    #[test]
379    fn display() -> Result<(), Box<dyn std::error::Error>> {
380        let code = Code::try_new(crate::concern::code::Kind::Warning, crate::Version::V1, 1)?;
381        let warning = Builder::default()
382            .code(code)
383            .level(Level::Medium)
384            .tags(TagSet::new(&[Tag::Style]))
385            .push_location(Location::Unplaced)
386            .subject("Hello, world!")
387            .body("A body.")
388            .fix("How to fix the issue.")
389            .try_build()?;
390
391        assert_eq!(
392            warning.to_string(),
393            "[v1::W001::[Style]::Medium] Hello, world!"
394        );
395
396        Ok(())
397    }
398}