Skip to content

Commit c6ec6d9

Browse files
authored
Merge pull request #165 from Jarcho/no_msg
Allow patternless error and warning matchers
2 parents c239bb8 + b69da28 commit c6ec6d9

16 files changed

+505
-79
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
* Started maintaining a changelog
1313
* `Config::comment_defaults` allows setting `//@` comments for all tests
14+
* `//~` comments can now specify just an error code or lint name, without any message. ERROR level is implied
15+
* `Revisioned::diagnostic_code_prefix` allows stripping a prefix of diagnostic codes to avoid having to repeat `clippy::` in all messages
1416

1517
### Fixed
1618

README.md

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ A stable 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`.
24+
* This will only match a diagnostic at the `ERROR` level.
2425

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

src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ pub enum Error {
2525
/// Can be `None` when it is expected outside the current file
2626
expected_line: Option<NonZeroUsize>,
2727
},
28+
/// A diagnostic code matcher was declared but had no matching error.
29+
CodeNotFound {
30+
/// The code that was not found, and the span of where that code was declared.
31+
code: Spanned<String>,
32+
/// Can be `None` when it is expected outside the current file
33+
expected_line: Option<NonZeroUsize>,
34+
},
2835
/// A ui test checking for failure does not have any failure patterns
2936
NoPatternsFound,
3037
/// A ui test checking for success has failure patterns

src/lib.rs

Lines changed: 57 additions & 21 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, OptWithLine, Revisioned, Spanned};
17+
use parser::{ErrorMatch, ErrorMatchKind, OptWithLine, Revisioned, Spanned};
1818
use regex::bytes::{Captures, Regex};
1919
use rustc_stderr::{Level, Message};
2020
use spanned::Span;
@@ -915,6 +915,7 @@ fn run_rustfix(
915915
edition,
916916
mode: OptWithLine::new(Mode::Pass, Span::default()),
917917
no_rustfix: OptWithLine::new((), Span::default()),
918+
diagnostic_code_prefix: OptWithLine::new(String::new(), Span::default()),
918919
needs_asm_support: false,
919920
},
920921
))
@@ -1071,40 +1072,75 @@ fn check_annotations(
10711072
});
10721073
}
10731074
}
1075+
let diagnostic_code_prefix = comments
1076+
.find_one_for_revision(revision, "diagnostic_code_prefix", |r| {
1077+
r.diagnostic_code_prefix.clone()
1078+
})?
1079+
.into_inner()
1080+
.map(|s| s.content)
1081+
.unwrap_or_default();
10741082

10751083
// The order on `Level` is such that `Error` is the highest level.
10761084
// We will ensure that *all* diagnostics of level at least `lowest_annotation_level`
10771085
// are matched.
10781086
let mut lowest_annotation_level = Level::Error;
1079-
for &ErrorMatch {
1080-
ref pattern,
1081-
level,
1082-
line,
1083-
} in comments
1087+
'err: for &ErrorMatch { ref kind, line } in comments
10841088
.for_revision(revision)
10851089
.flat_map(|r| r.error_matches.iter())
10861090
{
1087-
seen_error_match = Some(pattern.span());
1088-
// If we found a diagnostic with a level annotation, make sure that all
1089-
// diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
1090-
// for this pattern.
1091-
if lowest_annotation_level > level {
1092-
lowest_annotation_level = level;
1091+
match kind {
1092+
ErrorMatchKind::Code(code) => {
1093+
seen_error_match = Some(code.span());
1094+
}
1095+
&ErrorMatchKind::Pattern { ref pattern, level } => {
1096+
seen_error_match = Some(pattern.span());
1097+
// If we found a diagnostic with a level annotation, make sure that all
1098+
// diagnostics of that level have annotations, even if we don't end up finding a matching diagnostic
1099+
// for this pattern.
1100+
if lowest_annotation_level > level {
1101+
lowest_annotation_level = level;
1102+
}
1103+
}
10931104
}
10941105

10951106
if let Some(msgs) = messages.get_mut(line.get()) {
1096-
let found = msgs
1097-
.iter()
1098-
.position(|msg| pattern.matches(&msg.message) && msg.level == level);
1099-
if let Some(found) = found {
1100-
msgs.remove(found);
1101-
continue;
1107+
match kind {
1108+
&ErrorMatchKind::Pattern { ref pattern, level } => {
1109+
let found = msgs
1110+
.iter()
1111+
.position(|msg| pattern.matches(&msg.message) && msg.level == level);
1112+
if let Some(found) = found {
1113+
msgs.remove(found);
1114+
continue;
1115+
}
1116+
}
1117+
ErrorMatchKind::Code(code) => {
1118+
for (i, msg) in msgs.iter().enumerate() {
1119+
if msg.level != Level::Error {
1120+
continue;
1121+
}
1122+
let Some(msg_code) = &msg.code else { continue };
1123+
let Some(msg) = msg_code.strip_prefix(&diagnostic_code_prefix) else {
1124+
continue;
1125+
};
1126+
if msg == **code {
1127+
msgs.remove(i);
1128+
continue 'err;
1129+
}
1130+
}
1131+
}
11021132
}
11031133
}
11041134

1105-
errors.push(Error::PatternNotFound {
1106-
pattern: pattern.clone(),
1107-
expected_line: Some(line),
1135+
errors.push(match kind {
1136+
ErrorMatchKind::Pattern { pattern, .. } => Error::PatternNotFound {
1137+
pattern: pattern.clone(),
1138+
expected_line: Some(line),
1139+
},
1140+
ErrorMatchKind::Code(code) => Error::CodeNotFound {
1141+
code: Spanned::new(format!("{}{}", diagnostic_code_prefix, **code), code.span()),
1142+
expected_line: Some(line),
1143+
},
11081144
});
11091145
}
11101146

src/parser.rs

Lines changed: 64 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ pub struct Revisioned {
164164
pub(crate) needs_asm_support: bool,
165165
/// Don't run [`rustfix`] for this test
166166
pub no_rustfix: OptWithLine<()>,
167+
/// Prefix added to all diagnostic code matchers. Note this will make it impossible
168+
/// match codes which do not contain this prefix.
169+
pub diagnostic_code_prefix: OptWithLine<String>,
167170
}
168171

169172
#[derive(Debug)]
@@ -212,10 +215,20 @@ pub enum Pattern {
212215
Regex(Regex),
213216
}
214217

218+
#[derive(Debug, Clone)]
219+
pub(crate) enum ErrorMatchKind {
220+
/// A level and pattern pair parsed from a `//~ LEVEL: Message` comment.
221+
Pattern {
222+
pattern: Spanned<Pattern>,
223+
level: Level,
224+
},
225+
/// An error code parsed from a `//~ error_code` comment.
226+
Code(Spanned<String>),
227+
}
228+
215229
#[derive(Debug, Clone)]
216230
pub(crate) struct ErrorMatch {
217-
pub pattern: Spanned<Pattern>,
218-
pub level: Level,
231+
pub(crate) kind: ErrorMatchKind,
219232
/// The line this pattern is expecting to find a message in.
220233
pub line: NonZeroUsize,
221234
}
@@ -302,6 +315,7 @@ impl Comments {
302315
}
303316
}
304317
}
318+
305319
let Revisioned {
306320
span,
307321
ignore,
@@ -319,6 +333,7 @@ impl Comments {
319333
mode,
320334
needs_asm_support,
321335
no_rustfix,
336+
diagnostic_code_prefix,
322337
} = parser.comments.base();
323338
if span.is_dummy() {
324339
*span = defaults.span;
@@ -345,6 +360,9 @@ impl Comments {
345360
if no_rustfix.is_none() {
346361
*no_rustfix = defaults.no_rustfix;
347362
}
363+
if diagnostic_code_prefix.is_none() {
364+
*diagnostic_code_prefix = defaults.diagnostic_code_prefix;
365+
}
348366
*needs_asm_support |= defaults.needs_asm_support;
349367

350368
if parser.errors.is_empty() {
@@ -788,7 +806,10 @@ impl<CommentsType> CommentParser<CommentsType> {
788806
}
789807

790808
impl CommentParser<&mut Revisioned> {
791-
// parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*)
809+
// parse something like:
810+
// (\[[a-z]+(,[a-z]+)*\])?
811+
// (?P<offset>\||[\^]+)? *
812+
// ((?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*))|(?P<code>[a-z0-9_:]+)
792813
fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) {
793814
let (match_line, pattern) = match pattern.chars().next() {
794815
Some('|') => (
@@ -831,43 +852,52 @@ impl CommentParser<&mut Revisioned> {
831852
};
832853

833854
let pattern = pattern.trim_start();
834-
let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) {
835-
Some(offset) => offset,
836-
None => {
837-
self.error(pattern.span(), "pattern without level");
838-
return;
839-
}
840-
};
855+
let offset = pattern
856+
.bytes()
857+
.position(|c| !(c.is_ascii_alphanumeric() || c == b'_' || c == b':'))
858+
.unwrap_or(pattern.len());
859+
860+
let (level_or_code, pattern) = pattern.split_at(offset);
861+
if let Some(level) = level_or_code.strip_suffix(":") {
862+
let level = match (*level).parse() {
863+
Ok(level) => level,
864+
Err(msg) => {
865+
self.error(level.span(), msg);
866+
return;
867+
}
868+
};
841869

842-
let (level, pattern) = pattern.split_at(offset);
843-
let level = match (*level).parse() {
844-
Ok(level) => level,
845-
Err(msg) => {
846-
self.error(level.span(), msg);
847-
return;
848-
}
849-
};
850-
let pattern = match pattern.strip_prefix(":") {
851-
Some(offset) => offset,
852-
None => {
853-
self.error(pattern.span(), "no `:` after level found");
854-
return;
855-
}
856-
};
870+
let pattern = pattern.trim();
857871

858-
let pattern = pattern.trim();
872+
self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
859873

860-
self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
874+
let pattern = self.parse_error_pattern(pattern);
861875

862-
let pattern = self.parse_error_pattern(pattern);
876+
self.error_matches.push(ErrorMatch {
877+
kind: ErrorMatchKind::Pattern { pattern, level },
878+
line: match_line,
879+
});
880+
} else if (*level_or_code).parse::<Level>().is_ok() {
881+
// Shouldn't conflict with any real diagnostic code
882+
self.error(level_or_code.span(), "no `:` after level found");
883+
return;
884+
} else if !pattern.trim_start().is_empty() {
885+
self.error(
886+
pattern.span(),
887+
format!("text found after error code `{}`", *level_or_code),
888+
);
889+
return;
890+
} else {
891+
self.error_matches.push(ErrorMatch {
892+
kind: ErrorMatchKind::Code(Spanned::new(
893+
level_or_code.to_string(),
894+
level_or_code.span(),
895+
)),
896+
line: match_line,
897+
});
898+
};
863899

864900
*fallthrough_to = Some(match_line);
865-
866-
self.error_matches.push(ErrorMatch {
867-
pattern,
868-
level,
869-
line: match_line,
870-
});
871901
}
872902
}
873903

src/parser/tests.rs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::path::Path;
22

33
use crate::{
4-
parser::{Condition, Pattern},
4+
parser::{Condition, ErrorMatchKind, Pattern},
55
Error,
66
};
77

@@ -20,8 +20,11 @@ fn main() {
2020
println!("parsed comments: {:#?}", comments);
2121
assert_eq!(comments.revisioned.len(), 1);
2222
let revisioned = &comments.revisioned[&vec![]];
23-
assert_eq!(revisioned.error_matches[0].pattern.line().get(), 5);
24-
match &*revisioned.error_matches[0].pattern {
23+
let ErrorMatchKind::Pattern { pattern, .. } = &revisioned.error_matches[0].kind else {
24+
panic!("expected pattern matcher");
25+
};
26+
assert_eq!(pattern.line().get(), 5);
27+
match &**pattern {
2528
Pattern::SubString(s) => {
2629
assert_eq!(
2730
s,
@@ -32,6 +35,24 @@ fn main() {
3235
}
3336
}
3437

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

0 commit comments

Comments
 (0)