Skip to content

Commit 1c3aab1

Browse files
committed
Allow matching errors and warnings by error code.
1 parent 3d262a2 commit 1c3aab1

File tree

14 files changed

+315
-86
lines changed

14 files changed

+315
-86
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ A smaller version of compiletest-rs
1111
## Supported magic comment annotations
1212

1313
If your test tests for failure, you need to add a `//~` annotation where the error is happening
14-
to make sure that the test will always keep failing with a specific message at the annotated line.
14+
to ensure that the test will always keep failing at the annotated line. These comments can take two forms:
1515

16-
`//~ ERROR: XXX` make sure the stderr output contains `XXX` for an error in the line where this comment is written
17-
18-
* Also supports `HELP`, `WARN` or `NOTE` for different kind of message
19-
* if one of those levels is specified explicitly, *all* diagnostics of this level or higher need an annotation. If you want to avoid this, just leave out the all caps level note entirely.
20-
* If the all caps note is left out, a message of any level is matched. Leaving it out is not allowed for `ERROR` levels.
21-
* This checks the output *before* normalization, so you can check things that get normalized away, but need to
22-
be careful not to accidentally have a pattern that differs between platforms.
23-
* if `XXX` is of the form `/XXX/` it is treated as a regex instead of a substring and will succeed if the regex matches.
16+
* `//~ LEVEL: XXX` matches by error level and message text
17+
* `LEVEL` can be one of the following (descending order): `ERROR`, `HELP`, `WARN` or `NOTE`
18+
* If a level is specified explicitly, *all* diagnostics of that level or higher need an annotation. To avoid this see `//@require-annotations-for-level`
19+
* This checks the output *before* normalization, so you can check things that get normalized away, but need to
20+
be careful not to accidentally have a pattern that differs between platforms.
21+
* if `XXX` is of the form `/XXX/` it is treated as a regex instead of a substring and will succeed if the regex matches.
22+
* `//~ CODE` matches by diagnostic code.
23+
* `CODE` can take multiple forms such as: `E####`, `lint_name`, `tool::lint_name`.
2424

2525
In order to change how a single test is tested, you can add various `//@` comments to the test.
2626
Any other comments will be ignored, and all `//@` comments must be formatted precisely as

src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
parser::{Pattern, Spanned},
2+
parser::{ErrorMatchKind, Spanned},
33
rustc_stderr::{Message, Span},
44
Mode,
55
};
@@ -19,7 +19,7 @@ pub enum Error {
1919
expected: i32,
2020
},
2121
/// A pattern was declared but had no matching error.
22-
PatternNotFound(Spanned<Pattern>),
22+
PatternNotFound(ErrorMatchKind),
2323
/// A ui test checking for failure does not have any failure patterns
2424
NoPatternsFound,
2525
/// A ui test checking for success has failure patterns

src/lib.rs

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use color_eyre::eyre::{eyre, Result};
1414
use crossbeam_channel::{unbounded, Receiver, Sender};
1515
use dependencies::{Build, BuildManager};
1616
use lazy_static::lazy_static;
17-
use parser::{ErrorMatch, MaybeSpanned, OptWithLine, Revisioned, Spanned};
17+
use parser::{ErrorMatch, ErrorMatchKind, MaybeSpanned, OptWithLine, Revisioned, Spanned};
1818
use regex::bytes::{Captures, Regex};
1919
use rustc_stderr::{Level, Message, Span};
2020
use status_emitter::{StatusEmitter, TestStatus};
@@ -1079,41 +1079,61 @@ fn check_annotations(
10791079
{
10801080
messages_from_unknown_file_or_line.remove(i);
10811081
} else {
1082-
errors.push(Error::PatternNotFound(error_pattern.clone()));
1082+
errors.push(Error::PatternNotFound(ErrorMatchKind::Pattern {
1083+
pattern: error_pattern.clone(),
1084+
level: Level::Error,
1085+
}));
10831086
}
10841087
}
10851088

10861089
// The order on `Level` is such that `Error` is the highest level.
10871090
// We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
10881091
// are matched.
10891092
let mut lowest_annotation_level = Level::Error;
1090-
for &ErrorMatch {
1091-
ref pattern,
1092-
level,
1093-
line,
1094-
} in comments
1093+
for &ErrorMatch { ref kind, line } in comments
10951094
.for_revision(revision)
10961095
.flat_map(|r| r.error_matches.iter())
10971096
{
1098-
seen_error_match = Some(pattern.span());
1099-
// If we found a diagnostic with a level annotation, make sure that all
1100-
// diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
1101-
// for this pattern.
1102-
if lowest_annotation_level > level {
1103-
lowest_annotation_level = level;
1097+
match kind {
1098+
ErrorMatchKind::Code(code) => {
1099+
seen_error_match = Some(code.span());
1100+
}
1101+
&ErrorMatchKind::Pattern { ref pattern, level } => {
1102+
seen_error_match = Some(pattern.span());
1103+
// If we found a diagnostic with a level annotation, make sure that all
1104+
// diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
1105+
// for this pattern.
1106+
if lowest_annotation_level > level {
1107+
lowest_annotation_level = level;
1108+
}
1109+
}
11041110
}
11051111

11061112
if let Some(msgs) = messages.get_mut(line.get()) {
1107-
let found = msgs
1108-
.iter()
1109-
.position(|msg| pattern.matches(&msg.message) && msg.level == level);
1110-
if let Some(found) = found {
1111-
msgs.remove(found);
1112-
continue;
1113+
match kind {
1114+
&ErrorMatchKind::Pattern { ref pattern, level } => {
1115+
let found = msgs
1116+
.iter()
1117+
.position(|msg| pattern.matches(&msg.message) && msg.level == level);
1118+
if let Some(found) = found {
1119+
msgs.remove(found);
1120+
continue;
1121+
}
1122+
}
1123+
ErrorMatchKind::Code(code) => {
1124+
let found = msgs.iter().position(|msg| {
1125+
msg.level >= Level::Warn
1126+
&& msg.code.as_ref().is_some_and(|msg| *msg == **code)
1127+
});
1128+
if let Some(found) = found {
1129+
msgs.remove(found);
1130+
continue;
1131+
}
1132+
}
11131133
}
11141134
}
11151135

1116-
errors.push(Error::PatternNotFound(pattern.clone()));
1136+
errors.push(Error::PatternNotFound(kind.clone()));
11171137
}
11181138

11191139
let required_annotation_level = comments.find_one_for_revision(

src/parser.rs

Lines changed: 65 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -178,10 +178,28 @@ pub enum Pattern {
178178
Regex(Regex),
179179
}
180180

181+
#[derive(Debug, Clone)]
182+
pub enum ErrorMatchKind {
183+
/// A level and pattern pair parsed from a `//~ LEVEL: Message` comment.
184+
Pattern {
185+
pattern: Spanned<Pattern>,
186+
level: Level,
187+
},
188+
/// An error code parsed from a `//~ error_code` comment.
189+
Code(Spanned<String>),
190+
}
191+
impl ErrorMatchKind {
192+
pub(crate) fn span(&self) -> Span {
193+
match self {
194+
Self::Pattern { pattern, .. } => pattern.span(),
195+
Self::Code(code) => code.span(),
196+
}
197+
}
198+
}
199+
181200
#[derive(Debug)]
182201
pub(crate) struct ErrorMatch {
183-
pub pattern: Spanned<Pattern>,
184-
pub level: Level,
202+
pub(crate) kind: ErrorMatchKind,
185203
/// The line this pattern is expecting to find a message in.
186204
pub line: NonZeroUsize,
187205
}
@@ -260,6 +278,7 @@ impl Comments {
260278
}
261279
}
262280
}
281+
263282
if parser.errors.is_empty() {
264283
Ok(parser.comments)
265284
} else {
@@ -707,7 +726,10 @@ impl<CommentsType> CommentParser<CommentsType> {
707726
}
708727

709728
impl CommentParser<&mut Revisioned> {
710-
// parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*)
729+
// parse something like:
730+
// (\[[a-z]+(,[a-z]+)*\])?
731+
// (?P<offset>\||[\^]+)? *
732+
// ((?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*))|(?P<code>[a-z0-9_:]+)
711733
fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) {
712734
let (match_line, pattern) = match pattern.chars().next() {
713735
Some('|') => (
@@ -750,43 +772,52 @@ impl CommentParser<&mut Revisioned> {
750772
};
751773

752774
let pattern = pattern.trim_start();
753-
let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) {
754-
Some(offset) => offset,
755-
None => {
756-
self.error(pattern.span(), "pattern without level");
757-
return;
758-
}
759-
};
775+
let offset = pattern
776+
.bytes()
777+
.position(|c| !(c.is_ascii_alphanumeric() || c == b'_' || c == b':'))
778+
.unwrap_or(pattern.len());
779+
780+
let (level_or_code, pattern) = pattern.split_at(offset);
781+
if let Some(level) = level_or_code.strip_suffix(":") {
782+
let level = match (*level).parse() {
783+
Ok(level) => level,
784+
Err(msg) => {
785+
self.error(level.span(), msg);
786+
return;
787+
}
788+
};
760789

761-
let (level, pattern) = pattern.split_at(offset);
762-
let level = match (*level).parse() {
763-
Ok(level) => level,
764-
Err(msg) => {
765-
self.error(level.span(), msg);
766-
return;
767-
}
768-
};
769-
let pattern = match pattern.strip_prefix(":") {
770-
Some(offset) => offset,
771-
None => {
772-
self.error(pattern.span(), "no `:` after level found");
773-
return;
774-
}
775-
};
790+
let pattern = pattern.trim();
776791

777-
let pattern = pattern.trim();
792+
self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
778793

779-
self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
794+
let pattern = self.parse_error_pattern(pattern);
780795

781-
let pattern = self.parse_error_pattern(pattern);
796+
self.error_matches.push(ErrorMatch {
797+
kind: ErrorMatchKind::Pattern { pattern, level },
798+
line: match_line,
799+
});
800+
} else if (*level_or_code).parse::<Level>().is_ok() {
801+
// Shouldn't conflict with any real diagnostic code
802+
self.error(pattern.span(), "no `:` after level found");
803+
return;
804+
} else if !pattern.trim_start().is_empty() {
805+
self.error(
806+
pattern.span(),
807+
format!("text found after error code `{}`", *level_or_code),
808+
);
809+
return;
810+
} else {
811+
self.error_matches.push(ErrorMatch {
812+
kind: ErrorMatchKind::Code(Spanned::new(
813+
level_or_code.to_string(),
814+
level_or_code.span(),
815+
)),
816+
line: match_line,
817+
});
818+
};
782819

783820
*fallthrough_to = Some(match_line);
784-
785-
self.error_matches.push(ErrorMatch {
786-
pattern,
787-
level,
788-
line: match_line,
789-
});
790821
}
791822
}
792823

src/parser/tests.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
parser::{Condition, Pattern},
2+
parser::{Condition, ErrorMatchKind, Pattern},
33
Error,
44
};
55

@@ -18,8 +18,11 @@ fn main() {
1818
println!("parsed comments: {:#?}", comments);
1919
assert_eq!(comments.revisioned.len(), 1);
2020
let revisioned = &comments.revisioned[&vec![]];
21-
assert_eq!(revisioned.error_matches[0].pattern.line().get(), 5);
22-
match &*revisioned.error_matches[0].pattern {
21+
let ErrorMatchKind::Pattern { pattern, .. } = &revisioned.error_matches[0].kind else {
22+
panic!("expected pattern matcher");
23+
};
24+
assert_eq!(pattern.line().get(), 5);
25+
match &**pattern {
2326
Pattern::SubString(s) => {
2427
assert_eq!(
2528
s,
@@ -30,6 +33,24 @@ fn main() {
3033
}
3134
}
3235

36+
#[test]
37+
fn parse_error_code_comment() {
38+
let s = r"
39+
fn main() {
40+
let _x: i32 = 0u32; //~ E0308
41+
}
42+
";
43+
let comments = Comments::parse(s).unwrap();
44+
println!("parsed comments: {:#?}", comments);
45+
assert_eq!(comments.revisioned.len(), 1);
46+
let revisioned = &comments.revisioned[&vec![]];
47+
let ErrorMatchKind::Code(code) = &revisioned.error_matches[0].kind else {
48+
panic!("expected error code matcher");
49+
};
50+
assert_eq!(code.line().get(), 3);
51+
assert_eq!(**code, "E0308");
52+
}
53+
3354
#[test]
3455
fn parse_missing_level() {
3556
let s = r"
@@ -44,7 +65,7 @@ fn main() {
4465
assert_eq!(errors.len(), 1);
4566
match &errors[0] {
4667
Error::InvalidComment { msg, span } if span.line_start.get() == 5 => {
47-
assert_eq!(msg, "unknown level `encountered`")
68+
assert_eq!(msg, "text found after error code `encountered`")
4869
}
4970
_ => unreachable!(),
5071
}

src/rustc_stderr.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@ use std::{
55
path::{Path, PathBuf},
66
};
77

8+
#[derive(serde::Deserialize, Debug)]
9+
struct RustcDiagnosticCode {
10+
code: String,
11+
}
12+
813
#[derive(serde::Deserialize, Debug)]
914
struct RustcMessage {
1015
rendered: Option<String>,
1116
spans: Vec<RustcSpan>,
1217
level: String,
1318
message: String,
1419
children: Vec<RustcMessage>,
20+
code: Option<RustcDiagnosticCode>,
1521
}
1622

1723
#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
18-
pub(crate) enum Level {
24+
pub enum Level {
1925
Ice = 5,
2026
Error = 4,
2127
Warn = 3,
@@ -31,6 +37,7 @@ pub struct Message {
3137
pub(crate) level: Level,
3238
pub(crate) message: String,
3339
pub(crate) line_col: Option<Span>,
40+
pub(crate) code: Option<String>,
3441
}
3542

3643
/// Information about macro expansion.
@@ -126,6 +133,7 @@ impl RustcMessage {
126133
level: self.level.parse().unwrap(),
127134
message: self.message,
128135
line_col: line,
136+
code: self.code.map(|x| x.code),
129137
};
130138
if let Some(line) = line {
131139
if messages.len() <= line.line_start.get() {

0 commit comments

Comments
 (0)