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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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#[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#[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}