Skip to main content

litcheck_filecheck/
errors.rs

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