rslint_errors/
diagnostic.rs

1use crate::suggestion::SuggestionChange;
2use crate::{
3    file::{FileId, FileSpan, Span},
4    Applicability, CodeSuggestion, DiagnosticTag, Severity, SuggestionStyle,
5};
6use rslint_text_edit::*;
7
8/// A diagnostic message that can give information
9/// like errors or warnings.
10#[derive(Debug, Clone, PartialEq, Hash)]
11pub struct Diagnostic {
12    pub file_id: FileId,
13
14    pub severity: Severity,
15    pub code: Option<String>,
16    pub title: String,
17    pub tag: Option<DiagnosticTag>,
18
19    pub primary: Option<SubDiagnostic>,
20    pub children: Vec<SubDiagnostic>,
21    pub suggestions: Vec<CodeSuggestion>,
22    pub footers: Vec<Footer>,
23}
24
25impl Diagnostic {
26    /// Creates a new [`Diagnostic`] with the `Error` severity.
27    pub fn error(file_id: FileId, code: impl Into<String>, title: impl Into<String>) -> Self {
28        Self::new_with_code(file_id, Severity::Error, title, Some(code.into()))
29    }
30
31    /// Creates a new [`Diagnostic`] with the `Warning` severity.
32    pub fn warning(file_id: FileId, code: impl Into<String>, title: impl Into<String>) -> Self {
33        Self::new_with_code(file_id, Severity::Warning, title, Some(code.into()))
34    }
35
36    /// Creates a new [`Diagnostic`] with the `Help` severity.
37    pub fn help(file_id: FileId, code: impl Into<String>, title: impl Into<String>) -> Self {
38        Self::new_with_code(file_id, Severity::Help, title, Some(code.into()))
39    }
40
41    /// Creates a new [`Diagnostic`] with the `Note` severity.
42    pub fn note(file_id: FileId, code: impl Into<String>, title: impl Into<String>) -> Self {
43        Self::new_with_code(file_id, Severity::Note, title, Some(code.into()))
44    }
45
46    /// Creates a new [`Diagnostic`] that will be used in a builder-like way
47    /// to modify labels, and suggestions.
48    pub fn new(file_id: FileId, severity: Severity, title: impl Into<String>) -> Self {
49        Self::new_with_code(file_id, severity, title, None)
50    }
51
52    /// Creates a new [`Diagnostic`] with an error code that will be used in a builder-like way
53    /// to modify labels, and suggestions.
54    pub fn new_with_code(
55        file_id: FileId,
56        severity: Severity,
57        title: impl Into<String>,
58        code: Option<String>,
59    ) -> Self {
60        Self {
61            file_id,
62            code,
63            severity,
64            title: title.into(),
65            primary: None,
66            tag: None,
67            children: vec![],
68            suggestions: vec![],
69            footers: vec![],
70        }
71    }
72
73    /// Overwrites the severity of this diagnostic.
74    pub fn severity(mut self, severity: Severity) -> Self {
75        self.severity = severity;
76        self
77    }
78
79    /// Marks this diagnostic as deprecated code, which will
80    /// be displayed in the language server.
81    ///
82    /// This does not have any influence on the diagnostic rendering.
83    pub fn deprecated(mut self) -> Self {
84        self.tag = if matches!(self.tag, Some(DiagnosticTag::Unnecessary)) {
85            Some(DiagnosticTag::Both)
86        } else {
87            Some(DiagnosticTag::Deprecated)
88        };
89        self
90    }
91
92    /// Marks this diagnostic as unnecessary code, which will
93    /// be displayed in the language server.
94    ///
95    /// This does not have any influence on the diagnostic rendering.
96    pub fn unnecessary(mut self) -> Self {
97        self.tag = if matches!(self.tag, Some(DiagnosticTag::Deprecated)) {
98            Some(DiagnosticTag::Both)
99        } else {
100            Some(DiagnosticTag::Unnecessary)
101        };
102        self
103    }
104
105    /// Attaches a label to this [`Diagnostic`], that will point to another file
106    /// that is provided.
107    pub fn label_in_file(mut self, severity: Severity, span: FileSpan, msg: String) -> Self {
108        self.children.push(SubDiagnostic {
109            severity,
110            msg,
111            span,
112        });
113        self
114    }
115
116    /// Attaches a label to this [`Diagnostic`].
117    ///
118    /// The given span has to be in the file that was provided while creating this [`Diagnostic`].
119    pub fn label(mut self, severity: Severity, span: impl Span, msg: impl Into<String>) -> Self {
120        self.children.push(SubDiagnostic {
121            severity,
122            msg: msg.into(),
123            span: FileSpan::new(self.file_id, span),
124        });
125        self
126    }
127
128    /// Attaches a primary label to this [`Diagnostic`].
129    pub fn primary(mut self, span: impl Span, msg: impl Into<String>) -> Self {
130        self.primary = Some(SubDiagnostic {
131            severity: self.severity,
132            msg: msg.into(),
133            span: FileSpan::new(self.file_id, span),
134        });
135        self
136    }
137
138    /// Attaches a secondary label to this [`Diagnostic`].
139    pub fn secondary(self, span: impl Span, msg: impl Into<String>) -> Self {
140        self.label(Severity::Note, span, msg)
141    }
142
143    /// Prints out a message that suggests a possible solution, that is in another
144    /// file as this `Diagnostic`, to the error.
145    ///
146    /// If the message plus the suggestion is longer than 25 chars,
147    /// the suggestion is displayed as a new children of this `Diagnostic`,
148    /// otherwise it will be inlined with the other labels.
149    ///
150    /// A suggestion is displayed like:
151    /// ```no_rust
152    /// try adding a `;`: console.log();
153    /// ```
154    /// or in a separate multiline suggestion
155    ///
156    /// The message should not contain the `:` because it's added automatically.
157    /// The suggestion will automatically be wrapped inside two backticks.
158    pub fn suggestion_in_file(
159        self,
160        span: impl Span,
161        msg: &str,
162        suggestion: impl Into<String>,
163        applicability: Applicability,
164        file: FileId,
165    ) -> Self {
166        self.suggestion_inner(span, msg, suggestion, applicability, None, file)
167    }
168
169    fn auto_suggestion_style(span: &impl Span, msg: &str) -> SuggestionStyle {
170        if span.as_range().len() + msg.len() > 25 {
171            SuggestionStyle::Full
172        } else {
173            SuggestionStyle::Inline
174        }
175    }
176
177    /// Prints out a message that suggests a possible solution to the error.
178    ///
179    /// If the message plus the suggestion is longer than 25 chars,
180    /// the suggestion is displayed as a new children of this `Diagnostic`,
181    /// otherwise it will be inlined with the other labels.
182    ///
183    /// A suggestion is displayed like:
184    /// ```no_rust
185    /// try adding a `;`: console.log();
186    /// ```
187    /// or in a separate multiline suggestion
188    ///
189    /// The message should not contain the `:` because it's added automatically.
190    /// The suggestion will automatically be wrapped inside two backticks.
191    pub fn suggestion(
192        self,
193        span: impl Span,
194        msg: &str,
195        suggestion: impl Into<String>,
196        applicability: Applicability,
197    ) -> Self {
198        let file = self.file_id;
199        self.suggestion_inner(span, msg, suggestion, applicability, None, file)
200    }
201
202    /// Add a suggestion which is always shown in the [Full](SuggestionStyle::Full) style.
203    pub fn suggestion_full(
204        self,
205        span: impl Span,
206        msg: &str,
207        suggestion: impl Into<String>,
208        applicability: Applicability,
209    ) -> Self {
210        let file = self.file_id;
211        self.suggestion_inner(
212            span,
213            msg,
214            suggestion,
215            applicability,
216            SuggestionStyle::Full,
217            file,
218        )
219    }
220
221    /// Add a suggestion which is always shown in the [Inline](SuggestionStyle::Inline) style.
222    pub fn suggestion_inline(
223        self,
224        span: impl Span,
225        msg: &str,
226        suggestion: impl Into<String>,
227        applicability: Applicability,
228    ) -> Self {
229        let file = self.file_id;
230        self.suggestion_inner(
231            span,
232            msg,
233            suggestion,
234            applicability,
235            SuggestionStyle::Inline,
236            file,
237        )
238    }
239
240    /// Add a suggestion which does not have a suggestion code.
241    pub fn suggestion_no_code(
242        self,
243        span: impl Span,
244        msg: &str,
245        applicability: Applicability,
246    ) -> Self {
247        let file = self.file_id;
248        self.suggestion_inner(
249            span,
250            msg,
251            "",
252            applicability,
253            SuggestionStyle::HideCode,
254            file,
255        )
256    }
257
258    pub fn indel_suggestion(
259        mut self,
260        indels: impl IntoIterator<Item = Indel>,
261        span: impl Span,
262        msg: &str,
263        applicability: Applicability,
264    ) -> Self {
265        let span = FileSpan {
266            file: self.file_id,
267            range: span.as_range(),
268        };
269        let indels = indels.into_iter().collect::<Vec<_>>();
270        let labels = indels
271            .iter()
272            .filter(|x| !x.insert.is_empty())
273            .map(|x| x.delete.as_range().start..x.delete.as_range().start + x.insert.len())
274            .collect();
275
276        let suggestion = CodeSuggestion {
277            substitution: SuggestionChange::Indels(indels),
278            applicability,
279            msg: msg.to_string(),
280            labels,
281            span,
282            style: SuggestionStyle::Full,
283        };
284        self.suggestions.push(suggestion);
285        self
286    }
287
288    /// Add a suggestion with info labels which point to places in the suggestion.
289    ///
290    /// **The label ranges are relative to the start of the span, not relative to the original code**
291    pub fn suggestion_with_labels(
292        mut self,
293        span: impl Span,
294        msg: &str,
295        suggestion: impl Into<String>,
296        applicability: Applicability,
297        labels: impl IntoIterator<Item = impl Span>,
298    ) -> Self {
299        let span = FileSpan {
300            file: self.file_id,
301            range: span.as_range(),
302        };
303
304        let labels = labels
305            .into_iter()
306            .map(|x| {
307                let range = x.as_range();
308                span.range.start + range.start..span.range.start + range.end
309            })
310            .collect::<Vec<_>>();
311        let suggestion = CodeSuggestion {
312            substitution: SuggestionChange::String(suggestion.into()),
313            applicability,
314            msg: msg.to_string(),
315            labels,
316            span,
317            style: SuggestionStyle::Full,
318        };
319        self.suggestions.push(suggestion);
320        self
321    }
322
323    /// Add a suggestion with info labels which point to places in the suggestion.
324    ///
325    /// **The label ranges are relative to the source code, not relative to the original code**
326    pub fn suggestion_with_src_labels(
327        mut self,
328        span: impl Span,
329        msg: &str,
330        suggestion: impl Into<String>,
331        applicability: Applicability,
332        labels: impl IntoIterator<Item = impl Span>,
333    ) -> Self {
334        let span = FileSpan {
335            file: self.file_id,
336            range: span.as_range(),
337        };
338
339        let labels = labels.into_iter().map(|x| x.as_range()).collect::<Vec<_>>();
340        let suggestion = CodeSuggestion {
341            substitution: SuggestionChange::String(suggestion.into()),
342            applicability,
343            msg: msg.to_string(),
344            labels,
345            span,
346            style: SuggestionStyle::Full,
347        };
348        self.suggestions.push(suggestion);
349        self
350    }
351
352    fn suggestion_inner(
353        mut self,
354        span: impl Span,
355        msg: &str,
356        suggestion: impl Into<String>,
357        applicability: Applicability,
358        style: impl Into<Option<SuggestionStyle>>,
359        file: FileId,
360    ) -> Self {
361        let style = style
362            .into()
363            .unwrap_or_else(|| Self::auto_suggestion_style(&span, msg));
364        let span = FileSpan {
365            file,
366            range: span.as_range(),
367        };
368        let suggestion = CodeSuggestion {
369            substitution: SuggestionChange::String(suggestion.into()),
370            applicability,
371            msg: msg.to_string(),
372            labels: vec![],
373            span,
374            style,
375        };
376        self.suggestions.push(suggestion);
377        self
378    }
379
380    /// Adds a footer to this `Diagnostic`, which will be displayed under the actual error.
381    pub fn footer(mut self, severity: Severity, msg: impl Into<String>) -> Self {
382        self.footers.push(Footer {
383            msg: msg.into(),
384            severity,
385        });
386        self
387    }
388
389    /// Adds a footer to this `Diagnostic`, with the `Help` severity.
390    pub fn footer_help(self, msg: impl Into<String>) -> Self {
391        self.footer(Severity::Help, msg)
392    }
393
394    /// Adds a footer to this `Diagnostic`, with the `Note` severity.
395    pub fn footer_note(self, msg: impl Into<String>) -> Self {
396        self.footer(Severity::Note, msg)
397    }
398}
399
400/// Everything that can be added to a diagnostic, like
401/// a suggestion that will be displayed under the actual error.
402#[derive(Debug, Clone, PartialEq, Hash)]
403pub struct SubDiagnostic {
404    pub severity: Severity,
405    pub msg: String,
406    pub span: FileSpan,
407}
408
409/// A note or help that is displayed under the diagnostic.
410#[derive(Debug, Clone, PartialEq, Hash)]
411pub struct Footer {
412    pub msg: String,
413    pub severity: Severity,
414}