Skip to main content

litcheck_filecheck/
errors.rs

1use crate::common::*;
2use crate::test::TestInputType;
3
4#[derive(Diagnostic, Debug, thiserror::Error)]
5#[error("{test_from} failed")]
6#[diagnostic(help("see below for details"))]
7pub struct TestFailed {
8    pub test_from: TestInputType,
9    #[related]
10    pub errors: Vec<CheckFailedError>,
11}
12impl TestFailed {
13    pub fn new<'input, 'context: 'input>(
14        errors: Vec<CheckFailedError>,
15        context: &MatchContext<'input, 'context>,
16    ) -> Self {
17        Self {
18            test_from: TestInputType(context.match_file().uri().clone()),
19            errors,
20        }
21    }
22
23    pub fn errors(&self) -> &[CheckFailedError] {
24        self.errors.as_slice()
25    }
26}
27
28#[derive(Diagnostic, Debug, thiserror::Error)]
29pub enum InvalidCheckFileError {
30    #[error("check file did not contain any rules")]
31    #[diagnostic()]
32    Empty,
33    #[error("invalid CHECK-LABEL pattern")]
34    #[diagnostic()]
35    CheckLabelVariable {
36        #[label("in this pattern")]
37        line: SourceSpan,
38        #[label("variables/substitutions are not allowed on CHECK-LABEL lines")]
39        var: SourceSpan,
40    },
41    #[error("{kind} directives are not permitted to be the first directive in a file")]
42    #[diagnostic()]
43    InvalidFirstCheck {
44        #[label]
45        line: SourceSpan,
46        kind: Check,
47    },
48    #[error("invalid CHECK pattern")]
49    #[diagnostic()]
50    EmptyPattern(#[label("expected a non-empty pattern here")] SourceSpan),
51}
52
53#[derive(Debug, Diagnostic, thiserror::Error)]
54#[diagnostic()]
55#[error("invalid cast to numeric value: {kind:?}")]
56pub struct InvalidNumericCastError {
57    #[label("occurs due to cast implied by this pattern")]
58    pub span: Option<SourceSpan>,
59    pub kind: std::num::IntErrorKind,
60    #[label("specifically, the value captured by this pattern is not of the correct format")]
61    pub specific_span: Option<SourceSpan>,
62    #[source_code]
63    pub match_file: Arc<SourceFile>,
64}
65
66#[derive(Debug, Diagnostic, thiserror::Error)]
67#[error("reference to undefined variable '{name}'")]
68pub struct UndefinedVariableError {
69    #[label("occurs here")]
70    pub span: SourceSpan,
71    #[source_code]
72    pub match_file: Arc<SourceFile>,
73    pub name: Symbol,
74}
75
76#[derive(Diagnostic, Debug, thiserror::Error)]
77pub enum CheckFailedError {
78    #[error("the input file was rejected because it is empty, and --allow-empty was not set")]
79    #[diagnostic(
80        help = "if your input was the piped output of a command, it may have succeeded with no output when you expected it to fail"
81    )]
82    EmptyInput,
83    /// Indicates an error while processing a potential match
84    #[error("an error occurred while processing a potential match")]
85    #[diagnostic()]
86    MatchError {
87        #[label(primary, "when matching against this input")]
88        span: SourceSpan,
89        #[source_code]
90        input_file: Arc<SourceFile>,
91        #[related]
92        labels: Vec<RelatedLabel>,
93        #[help]
94        help: Option<String>,
95    },
96    /// Indicates a match for an excluded pattern.
97    #[error("match found, but was excluded")]
98    #[diagnostic()]
99    MatchFoundButExcluded {
100        #[label(primary, "match found here")]
101        span: SourceSpan,
102        #[source_code]
103        input_file: Arc<SourceFile>,
104        #[related]
105        labels: Vec<RelatedLabel>,
106    },
107    /// Indicates a match for an expected pattern, but the match is on the
108    /// wrong line.
109    #[error("match found for expected pattern, but on the wrong line")]
110    #[diagnostic()]
111    MatchFoundButWrongLine {
112        #[label(primary, "match found here")]
113        span: SourceSpan,
114        #[source_code]
115        input_file: Arc<SourceFile>,
116        #[related]
117        pattern: Option<RelatedCheckError>,
118    },
119    /// Indicates a discarded match for an expected pattern.
120    #[error("match found, but was discarded")]
121    #[diagnostic()]
122    MatchFoundButDiscarded {
123        #[label(primary, "match found here")]
124        span: SourceSpan,
125        #[source_code]
126        input_file: Arc<SourceFile>,
127        #[related]
128        labels: Vec<RelatedLabel>,
129        #[help]
130        note: Option<String>,
131    },
132    /// Indicates an error while processing a match after the match was found
133    /// for an expected or excluded pattern.
134    #[error("match found, but there was an error processing it")]
135    #[diagnostic()]
136    MatchFoundErrorNote {
137        #[label(primary, "match found here")]
138        span: SourceSpan,
139        #[source_code]
140        input_file: Arc<SourceFile>,
141        #[related]
142        pattern: Option<RelatedCheckError>,
143        #[help]
144        help: Option<String>,
145    },
146    /// Indicates an error while processing a match after the match was found
147    /// for an expected or excluded pattern.
148    #[error("match found, but there was an error when evaluating a constraint")]
149    #[diagnostic()]
150    MatchFoundConstraintFailed {
151        #[label(primary, "match found here")]
152        span: SourceSpan,
153        #[source_code]
154        input_file: Arc<SourceFile>,
155        #[related]
156        pattern: Option<RelatedCheckError>,
157        #[related]
158        error: Option<RelatedError>,
159        #[help]
160        help: Option<String>,
161    },
162    /// Indicates no match for an expected pattern, but this might follow good
163    /// matches when multiple matches are expected for the pattern, or it might
164    /// follow discarded matches for the pattern.
165    #[error("no matches were found for expected pattern")]
166    #[diagnostic()]
167    MatchNoneButExpected {
168        #[label(primary, "pattern at this location was not matched")]
169        span: SourceSpan,
170        #[source_code]
171        match_file: Arc<SourceFile>,
172        #[help]
173        note: Option<String>,
174    },
175    /// Indicates no match due to an expected or excluded pattern that has
176    /// proven to be invalid at match time.  The exact problems are usually
177    /// reported in subsequent diagnostics of the same match type but with
178    /// `Note` set.
179    #[error("unable to match invalid pattern")]
180    #[diagnostic()]
181    MatchNoneForInvalidPattern {
182        #[label(primary, "pattern at this location was invalid")]
183        span: SourceSpan,
184        #[source_code]
185        match_file: Arc<SourceFile>,
186        #[related]
187        error: Option<RelatedError>,
188    },
189    /// Indicates a match attempt failed for unknown reasons
190    #[error("error occurred while matching pattern")]
191    #[diagnostic()]
192    MatchNoneErrorNote {
193        #[label(primary, "when matching this pattern")]
194        span: SourceSpan,
195        #[source_code]
196        match_file: Arc<SourceFile>,
197        #[related]
198        error: Option<RelatedError>,
199    },
200    /// Indicates a fuzzy match that serves as a suggestion for the next
201    /// intended match for an expected pattern with too few or no good matches.
202    #[error("an exact match was not found, but some similar matches were found, see notes")]
203    #[diagnostic()]
204    MatchFuzzy {
205        #[label(primary, "pattern at this location was invalid")]
206        span: SourceSpan,
207        #[source_code]
208        match_file: Arc<SourceFile>,
209        #[help]
210        notes: Option<String>,
211    },
212    /// Indicates that matching all patterns in a set of patterns failed due
213    /// to at least one pattern not being matched.
214    ///
215    /// This occurs with CHECK-DAG/CHECK-NOT which are evaluated in groups
216    #[error("one or more matches were not found for a set of expected patterns")]
217    #[diagnostic(help("see diagnostics for details about each failed pattern"))]
218    MatchAllFailed {
219        #[related]
220        failed: Vec<CheckFailedError>,
221    },
222    #[error("unable to match all instances of repeat pattern (matched {n} of {count} times)")]
223    #[diagnostic(help("see related errors below for additional details"))]
224    MatchRepeatedError {
225        #[label(primary, "when matching this pattern for the {}th time", n + 1)]
226        span: SourceSpan,
227        #[source_code]
228        match_file: Arc<SourceFile>,
229        n: usize,
230        count: usize,
231        #[label(collection)]
232        related: Vec<LabeledSpan>,
233    },
234    #[error("one or more matches were not found for a set of expected patterns")]
235    #[diagnostic(help("see related error for more information"))]
236    MatchGroupFailed {
237        #[label(primary, "this check failed")]
238        span: SourceSpan,
239        #[source_code]
240        match_file: Arc<SourceFile>,
241        #[related]
242        cause: Vec<CheckFailedError>,
243        #[label("these checks were skipped because they were dependent on the check that failed")]
244        skipped: Option<SourceSpan>,
245    },
246}
247impl CheckFailedError {
248    /// Returns true if this error was produced in the context of a possibly-valid match
249    pub fn match_was_found(&self) -> bool {
250        matches!(
251            self,
252            Self::MatchFoundButExcluded { .. }
253                | Self::MatchFoundButWrongLine { .. }
254                | Self::MatchFoundButDiscarded { .. }
255                | Self::MatchFoundErrorNote { .. }
256                | Self::MatchFoundConstraintFailed { .. }
257        )
258    }
259
260    pub fn related_labels_for(&self, related_span: SourceSpan) -> Vec<LabeledSpan> {
261        use CheckFailedError::*;
262        let mut related = vec![];
263        let related_source_id = related_span.source_id();
264        match self {
265            EmptyInput => (),
266            err @ (MatchError { span, labels, .. }
267            | MatchFoundButExcluded { span, labels, .. }
268            | MatchFoundButDiscarded { span, labels, .. }) => {
269                if span.source_id() == related_source_id {
270                    related.push(LabeledSpan::new_with_span(Some(err.to_string()), *span));
271                }
272                for label in labels {
273                    if label.file.id() == related_source_id {
274                        for label in label.labels.iter() {
275                            related.push(LabeledSpan::new_with_span(
276                                label.label().map(|s| s.to_string()),
277                                label.span(),
278                            ))
279                        }
280                    }
281                }
282            }
283            err @ (MatchFoundButWrongLine { span, pattern, .. }
284            | MatchFoundErrorNote { span, pattern, .. }) => {
285                if span.source_id() == related_source_id {
286                    related.push(LabeledSpan::new_with_span(Some(err.to_string()), *span));
287                }
288                if let Some(pattern) = pattern.as_ref()
289                    && pattern.span.source_id() == related_source_id
290                {
291                    related.push(LabeledSpan::new_with_span(
292                        Some("due to pattern at this location".to_string()),
293                        pattern.span,
294                    ));
295                }
296            }
297            err @ MatchFoundConstraintFailed {
298                span,
299                pattern,
300                error,
301                ..
302            } => {
303                if span.source_id() == related_source_id {
304                    related.push(LabeledSpan::new_with_span(Some(err.to_string()), *span));
305                    if let Some(error) = error.as_ref() {
306                        related.push(LabeledSpan::new_with_span(Some(error.to_string()), *span));
307                    }
308                }
309                if let Some(pattern) = pattern.as_ref()
310                    && pattern.span.source_id() == related_source_id
311                {
312                    related.push(LabeledSpan::new_with_span(
313                        Some("due to pattern at this location".to_string()),
314                        pattern.span,
315                    ));
316                }
317            }
318            err @ MatchNoneButExpected { span, .. } => {
319                let message = err.to_string();
320                related.push(LabeledSpan::new_with_span(Some(message), *span));
321            }
322            err @ MatchNoneForInvalidPattern { span, error, .. } => {
323                if span.source_id() == related_source_id {
324                    let message = err.to_string();
325                    related.push(LabeledSpan::new_with_span(Some(message), *span));
326                    if let Some(error) = error.as_ref() {
327                        related.push(LabeledSpan::new_with_span(Some(error.to_string()), *span));
328                    }
329                }
330            }
331            MatchNoneErrorNote { span, error, .. } => {
332                if span.source_id() == related_source_id
333                    && let Some(error) = error.as_ref()
334                {
335                    related.push(LabeledSpan::new_with_span(Some(error.to_string()), *span));
336                }
337            }
338            err @ MatchFuzzy { span, notes, .. } => {
339                if span.source_id() == related_source_id {
340                    let message = err.to_string();
341                    related.push(LabeledSpan::new_with_span(Some(message), *span));
342                    if let Some(notes) = notes.clone() {
343                        related.push(LabeledSpan::new_with_span(Some(notes), *span));
344                    }
345                }
346            }
347            MatchAllFailed { .. } | MatchRepeatedError { .. } | MatchGroupFailed { .. } => (),
348        }
349
350        related
351    }
352}
353
354/// This is used to associated source spans from the match file
355/// with those from the input file.
356#[derive(Diagnostic, Debug, thiserror::Error)]
357#[error("check failed")]
358#[diagnostic()]
359pub struct RelatedCheckError {
360    #[label("due to pattern at this location")]
361    pub span: SourceSpan,
362    #[source_code]
363    pub match_file: Arc<SourceFile>,
364}
365
366#[derive(Debug, thiserror::Error)]
367#[error("see also")]
368pub struct RelatedLabel {
369    pub severity: litcheck::diagnostics::Severity,
370    pub labels: SmallVec<[Label; 1]>,
371    pub file: Arc<SourceFile>,
372}
373impl RelatedLabel {
374    pub fn error(label: Label, file: Arc<SourceFile>) -> Self {
375        Self {
376            severity: litcheck::diagnostics::Severity::Error,
377            labels: smallvec![label],
378            file,
379        }
380    }
381
382    pub fn warn(label: Label, file: Arc<SourceFile>) -> Self {
383        Self {
384            severity: litcheck::diagnostics::Severity::Warning,
385            labels: smallvec![label],
386            file,
387        }
388    }
389
390    pub fn note(label: Label, file: Arc<SourceFile>) -> Self {
391        Self {
392            severity: litcheck::diagnostics::Severity::Advice,
393            labels: smallvec![label],
394            file,
395        }
396    }
397
398    pub fn notes(label: impl IntoIterator<Item = Label>, file: Arc<SourceFile>) -> Self {
399        Self {
400            severity: litcheck::diagnostics::Severity::Advice,
401            labels: label.into_iter().collect(),
402            file,
403        }
404    }
405}
406impl Diagnostic for RelatedLabel {
407    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
408        None
409    }
410    fn severity(&self) -> Option<litcheck::diagnostics::Severity> {
411        Some(self.severity)
412    }
413    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
414        None
415    }
416    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
417        None
418    }
419    fn source_code(&self) -> Option<&dyn litcheck::diagnostics::SourceCode> {
420        Some(&self.file)
421    }
422    fn labels(&self) -> Option<Box<dyn Iterator<Item = litcheck::diagnostics::LabeledSpan> + '_>> {
423        if self.labels.is_empty() {
424            None
425        } else {
426            Some(Box::new(self.labels.iter().cloned().map(|l| l.into())))
427        }
428    }
429    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
430        None
431    }
432    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
433        None
434    }
435}
436
437/// This type wraps related diagnostics for use with [CheckFailedError]
438#[derive(Debug)]
439pub struct RelatedError(Report);
440impl RelatedError {
441    pub fn into_report(self) -> Report {
442        self.0
443    }
444
445    #[inline(always)]
446    pub fn as_diagnostic(&self) -> &dyn Diagnostic {
447        self.0.as_ref()
448    }
449}
450impl Diagnostic for RelatedError {
451    fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
452        self.as_diagnostic().code()
453    }
454    fn severity(&self) -> Option<litcheck::diagnostics::Severity> {
455        self.as_diagnostic().severity()
456    }
457    fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
458        self.as_diagnostic().help()
459    }
460    fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
461        self.as_diagnostic().url()
462    }
463    fn source_code(&self) -> Option<&dyn litcheck::diagnostics::SourceCode> {
464        self.as_diagnostic().source_code()
465    }
466    fn labels(&self) -> Option<Box<dyn Iterator<Item = litcheck::diagnostics::LabeledSpan> + '_>> {
467        self.as_diagnostic().labels()
468    }
469    fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
470        self.as_diagnostic().related()
471    }
472    fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
473        self.as_diagnostic().diagnostic_source()
474    }
475}
476impl fmt::Display for RelatedError {
477    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
478        fmt::Display::fmt(&self.0, f)
479    }
480}
481impl std::error::Error for RelatedError {
482    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
483        AsRef::<dyn std::error::Error>::as_ref(&self.0).source()
484    }
485}
486impl From<Report> for RelatedError {
487    fn from(report: Report) -> Self {
488        Self(report)
489    }
490}
491impl RelatedError {
492    pub const fn new(report: Report) -> Self {
493        Self(report)
494    }
495
496    pub fn wrap<E>(error: E) -> Self
497    where
498        E: Diagnostic + Send + Sync + 'static,
499    {
500        Self(Report::new_boxed(Box::new(error)))
501    }
502}