8 use bstr
::{ByteSlice, Utf8Error}
;
9 use regex
::bytes
::Regex
;
12 rustc_stderr
::{Level, Span}
,
16 use color_eyre
::eyre
::{Context, Result}
;
18 pub(crate) use spanned
::*;
24 /// This crate supports various magic comments that get parsed as file-specific
25 /// configuration values. This struct parses them all in one go and then they
26 /// get processed by their respective use sites.
27 #[derive(Default, Debug)]
28 pub(crate) struct Comments
{
29 /// List of revision names to execute. Can only be specified once
30 pub revisions
: Option
<Vec
<String
>>,
31 /// Comments that are only available under specific revisions.
32 /// The defaults are in key `vec![]`
33 pub revisioned
: HashMap
<Vec
<String
>, Revisioned
>,
37 /// Check that a comment isn't specified twice across multiple differently revisioned statements.
38 /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up
39 /// specifying two error patterns that are available in revision `foo`.
40 pub fn find_one_for_revision
<'a
, T
: 'a
>(
44 f
: impl Fn(&'a Revisioned
) -> OptWithLine
<T
>,
45 ) -> Result
<OptWithLine
<T
>, Errored
> {
46 let mut result
= None
;
47 let mut errors
= vec
![];
48 for rev
in self.for_revision(revision
) {
49 if let Some(found
) = f(rev
).into_inner() {
51 errors
.push(found
.line());
53 result
= found
.into();
57 if errors
.is_empty() {
61 command
: Command
::new(format
!("<finding flags for revision `{revision}`>")),
62 errors
: vec
![Error
::MultipleRevisionsWithResults
{
63 kind
: kind
.to_string(),
72 /// Returns an iterator over all revisioned comments that match the revision.
73 pub fn for_revision
<'a
>(&'a
self, revision
: &'a
str) -> impl Iterator
<Item
= &'a Revisioned
> {
74 self.revisioned
.iter().filter_map(move |(k
, v
)| {
75 if k
.is_empty() || k
.iter().any(|rev
| rev
== revision
) {
83 pub(crate) fn edition(
86 config
: &crate::Config
,
87 ) -> Result
<Option
<MaybeSpanned
<String
>>, Errored
> {
89 self.find_one_for_revision(revision
, "`edition` annotations", |r
| r
.edition
.clone())?
;
92 .map(MaybeSpanned
::from
)
93 .or(config
.edition
.clone().map(MaybeSpanned
::new_config
));
99 /// Comments that can be filtered for specific revisions.
100 pub(crate) struct Revisioned
{
101 /// The character range in which this revisioned item was first added.
102 /// Used for reporting errors on unknown revisions.
104 /// Don't run this test if any of these filters apply
105 pub ignore
: Vec
<Condition
>,
106 /// Only run this test if all of these filters apply
107 pub only
: Vec
<Condition
>,
108 /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar
109 pub stderr_per_bitwidth
: bool
,
110 /// Additional flags to pass to the executable
111 pub compile_flags
: Vec
<String
>,
112 /// Additional env vars to set for the executable
113 pub env_vars
: Vec
<(String
, String
)>,
114 /// Normalizations to apply to the stderr output before emitting it to disk
115 pub normalize_stderr
: Vec
<(Regex
, Vec
<u8>)>,
116 /// Normalizations to apply to the stdout output before emitting it to disk
117 pub normalize_stdout
: Vec
<(Regex
, Vec
<u8>)>,
118 /// Arbitrary patterns to look for in the stderr.
119 /// The error must be from another file, as errors from the current file must be
120 /// checked via `error_matches`.
121 pub error_in_other_files
: Vec
<Spanned
<Pattern
>>,
122 pub error_matches
: Vec
<ErrorMatch
>,
123 /// Ignore diagnostics below this level.
124 /// `None` means pick the lowest level from the `error_pattern`s.
125 pub require_annotations_for_level
: OptWithLine
<Level
>,
126 pub aux_builds
: Vec
<Spanned
<PathBuf
>>,
127 pub edition
: OptWithLine
<String
>,
128 /// Overwrites the mode from `Config`.
129 pub mode
: OptWithLine
<Mode
>,
130 pub needs_asm_support
: bool
,
131 /// Don't run [`rustfix`] for this test
132 pub no_rustfix
: OptWithLine
<()>,
136 struct CommentParser
<T
> {
137 /// The comments being built.
139 /// Any errors that ocurred during comment parsing.
141 /// The available commands and their parsing logic
142 commands
: HashMap
<&'
static str, CommandParserFunc
>,
145 type CommandParserFunc
= fn(&mut CommentParser
<&mut Revisioned
>, args
: Spanned
<&str>, span
: Span
);
147 impl<T
> std
::ops
::Deref
for CommentParser
<T
> {
150 fn deref(&self) -> &Self::Target
{
155 impl<T
> std
::ops
::DerefMut
for CommentParser
<T
> {
156 fn deref_mut(&mut self) -> &mut Self::Target
{
161 /// The conditions used for "ignore" and "only" filters.
163 pub(crate) enum Condition
{
164 /// The given string must appear in the host triple.
166 /// The given string must appear in the target triple.
168 /// Tests that the bitwidth is the given one.
170 /// Tests that the target is the host.
174 #[derive(Debug, Clone)]
175 /// An error pattern parsed from a `//~` comment.
182 pub(crate) struct ErrorMatch
{
183 pub pattern
: Spanned
<Pattern
>,
185 /// The line this pattern is expecting to find a message in.
186 pub line
: NonZeroUsize
,
190 fn parse(c
: &str) -> std
::result
::Result
<Self, String
> {
192 Ok(Condition
::OnHost
)
193 } else if let Some(bits
) = c
.strip_suffix("bit") {
194 let bits
: u8 = bits
.parse().map_err(|_err
| {
195 format
!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith")
197 Ok(Condition
::Bitwidth(bits
))
198 } else if let Some(triple_substr
) = c
.strip_prefix("target-") {
199 Ok(Condition
::Target(triple_substr
.to_owned()))
200 } else if let Some(triple_substr
) = c
.strip_prefix("host-") {
201 Ok(Condition
::Host(triple_substr
.to_owned()))
204 "`{c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/"
211 pub(crate) fn parse_file(path
: &Path
) -> Result
<std
::result
::Result
<Self, Vec
<Error
>>> {
213 std
::fs
::read(path
).wrap_err_with(|| format
!("failed to read {}", path
.display()))?
;
214 Ok(Self::parse(&content
))
217 /// Parse comments in `content`.
218 /// `path` is only used to emit diagnostics if parsing fails.
220 content
: &(impl AsRef
<[u8]> + ?Sized
),
221 ) -> std
::result
::Result
<Self, Vec
<Error
>> {
222 let mut parser
= CommentParser
{
223 comments
: Comments
::default(),
225 commands
: CommentParser
::<_
>::commands(),
228 let mut fallthrough_to
= None
; // The line that a `|` will refer to.
229 for (l
, line
) in content
.as_ref().lines().enumerate() {
230 let l
= NonZeroUsize
::new(l
+ 1).unwrap(); // enumerate starts at 0, but line numbers start at 1
234 column_start
: NonZeroUsize
::new(1).unwrap(),
235 column_end
: NonZeroUsize
::new(line
.chars().count() + 1).unwrap(),
237 match parser
.parse_checked_line(&mut fallthrough_to
, Spanned
::new(line
, span
)) {
239 Err(e
) => parser
.error(span
, format
!("Comment is not utf8: {e:?}")),
242 if let Some(revisions
) = &parser
.comments
.revisions
{
243 for (key
, revisioned
) in &parser
.comments
.revisioned
{
245 if !revisions
.contains(rev
) {
246 parser
.errors
.push(Error
::InvalidComment
{
247 msg
: format
!("the revision `{rev}` is not known"),
248 span
: revisioned
.span
,
254 for (key
, revisioned
) in &parser
.comments
.revisioned
{
256 parser
.errors
.push(Error
::InvalidComment
{
257 msg
: "there are no revisions in this test".into(),
258 span
: revisioned
.span
,
263 if parser
.errors
.is_empty() {
271 impl CommentParser
<Comments
> {
272 fn parse_checked_line(
274 fallthrough_to
: &mut Option
<NonZeroUsize
>,
275 line
: Spanned
<&[u8]>,
276 ) -> std
::result
::Result
<(), Utf8Error
> {
277 if let Some(command
) = line
.strip_prefix(b
"//@") {
278 self.parse_command(command
.to_str()?
.trim())
279 } else if let Some((_
, pattern
)) = line
.split_once_str("//~") {
280 let (revisions
, pattern
) = self.parse_revisions(pattern
.to_str()?
);
281 self.revisioned(revisions
, |this
| {
282 this
.parse_pattern(pattern
, fallthrough_to
)
285 *fallthrough_to
= None
;
286 for pos
in line
.find_iter("//") {
287 let (_
, rest
) = line
.to_str()?
.split_at(pos
+ 2);
288 for rest
in std
::iter
::once(rest
).chain(rest
.strip_prefix(" ")) {
289 if let Some('@'
| '
~'
| '
['
| '
]'
| '
^' | '
|'
) = rest
.chars().next() {
293 "comment looks suspiciously like a test suite command: `{}`\n\
294 All `//@` test suite commands must be at the start of the line.\n\
295 The `//` must be directly followed by `@` or `~`.",
300 let mut parser
= Self {
302 comments
: Comments
::default(),
303 commands
: std
::mem
::take(&mut self.commands
),
305 parser
.parse_command(rest
);
306 if parser
.errors
.is_empty() {
309 "a compiletest-rs style comment was detected.\n\
310 Please use text that could not also be interpreted as a command,\n\
311 and prefix all actual commands with `//@`",
314 self.commands
= parser
.commands
;
323 impl<CommentsType
> CommentParser
<CommentsType
> {
324 fn error(&mut self, span
: Span
, s
: impl Into
<String
>) {
325 self.errors
.push(Error
::InvalidComment
{
331 fn check(&mut self, span
: Span
, cond
: bool
, s
: impl Into
<String
>) {
337 fn check_some
<T
>(&mut self, span
: Span
, opt
: Option
<T
>, s
: impl Into
<String
>) -> Option
<T
> {
338 self.check(span
, opt
.is_some(), s
);
343 impl CommentParser
<Comments
> {
344 fn parse_command(&mut self, command
: Spanned
<&str>) {
345 let (revisions
, command
) = self.parse_revisions(command
);
347 // Commands are letters or dashes, grab everything until the first character that is neither of those.
348 let (command
, args
) = match command
350 .find_map(|(i
, c
)| (!c
.is_alphanumeric() && c
!= '
-'
&& c
!= '_'
).then_some(i
))
352 None
=> (command
, Spanned
::new("", command
.span().shrink_to_end())),
354 let (command
, args
) = command
.split_at(i
);
355 // Commands are separated from their arguments by ':' or ' '
359 .expect("the `position` above guarantees that there is at least one char");
361 args
.span().shrink_to_start(),
363 "test command must be followed by `:` (or end the line)",
365 (command
, args
.split_at(next
.len_utf8()).1.trim())
369 if *command
== "revisions" {
372 revisions
.is_empty(),
373 "revisions cannot be declared under a revision",
377 self.revisions
.is_none(),
378 "cannot specify `revisions` twice",
380 self.revisions
= Some(args
.split_whitespace().map(|s
| s
.to_string()).collect());
383 self.revisioned(revisions
, |this
| this
.parse_command(command
, args
));
388 revisions
: Spanned
<Vec
<String
>>,
389 f
: impl FnOnce(&mut CommentParser
<&mut Revisioned
>),
391 let span
= revisions
.span();
392 let revisions
= revisions
.into_inner();
393 let mut this
= CommentParser
{
394 errors
: std
::mem
::take(&mut self.errors
),
395 commands
: std
::mem
::take(&mut self.commands
),
399 .or_insert_with(|| Revisioned
{
401 ignore
: Default
::default(),
402 only
: Default
::default(),
403 stderr_per_bitwidth
: Default
::default(),
404 compile_flags
: Default
::default(),
405 env_vars
: Default
::default(),
406 normalize_stderr
: Default
::default(),
407 normalize_stdout
: Default
::default(),
408 error_in_other_files
: Default
::default(),
409 error_matches
: Default
::default(),
410 require_annotations_for_level
: Default
::default(),
411 aux_builds
: Default
::default(),
412 edition
: Default
::default(),
413 mode
: Default
::default(),
414 needs_asm_support
: Default
::default(),
415 no_rustfix
: Default
::default(),
422 self.commands
= commands
;
423 self.errors
= errors
;
427 impl CommentParser
<&mut Revisioned
> {
428 fn parse_normalize_test(
432 ) -> Option
<(Regex
, Vec
<u8>)> {
433 let (from
, rest
) = self.parse_str(args
);
435 let to
= match rest
.strip_prefix("->") {
441 "normalize-{mode}-test needs a pattern and replacement separated by `->`"
448 let (to
, rest
) = self.parse_str(to
);
453 "trailing text after pattern replacement",
456 let regex
= self.parse_regex(from
)?
;
457 Some((regex
, to
.as_bytes().to_owned()))
460 fn commands() -> HashMap
<&'
static str, CommandParserFunc
> {
461 let mut commands
= HashMap
::<_
, CommandParserFunc
>::new();
462 macro_rules
! commands
{
463 ($
($name
:expr
=> ($this
:ident
, $args
:ident
, $span
:ident
)$block
:block
)*) => {
464 $
(commands
.insert($name
, |$this
, $args
, $span
| {
470 "compile-flags" => (this
, args
, _span
){
471 if let Some(parsed
) = comma
::parse_command(*args
) {
472 this
.compile_flags
.extend(parsed
);
474 this
.error(args
.span(), format
!("`{}` contains an unclosed quotation mark", *args
));
477 "rustc-env" => (this
, args
, _span
){
478 for env
in args
.split_whitespace() {
479 if let Some((k
, v
)) = this
.check_some(
482 "environment variables must be key/value pairs separated by a `=`",
484 this
.env_vars
.push((k
.to_string(), v
.to_string()));
488 "normalize-stderr-test" => (this
, args
, _span
){
489 if let Some(res
) = this
.parse_normalize_test(args
, "stderr") {
490 this
.normalize_stderr
.push(res
)
493 "normalize-stdout-test" => (this
, args
, _span
){
494 if let Some(res
) = this
.parse_normalize_test(args
, "stdout") {
495 this
.normalize_stdout
.push(res
)
498 "error-pattern" => (this
, _args
, span
){
499 this
.error(span
, "`error-pattern` has been renamed to `error-in-other-file`");
501 "error-in-other-file" => (this
, args
, _span
){
502 let args
= args
.trim();
503 let pat
= this
.parse_error_pattern(args
);
504 this
.error_in_other_files
.push(pat
);
506 "stderr-per-bitwidth" => (this
, _args
, span
){
507 // args are ignored (can be used as comment)
510 !this
.stderr_per_bitwidth
,
511 "cannot specify `stderr-per-bitwidth` twice",
513 this
.stderr_per_bitwidth
= true;
515 "run-rustfix" => (this
, _args
, span
){
516 this
.error(span
, "rustfix is now ran by default when applicable suggestions are found");
518 "no-rustfix" => (this
, _args
, span
){
519 // args are ignored (can be used as comment)
520 let prev
= this
.no_rustfix
.set((), span
);
524 "cannot specify `no-rustfix` twice",
527 "needs-asm-support" => (this
, _args
, span
){
528 // args are ignored (can be used as comment)
531 !this
.needs_asm_support
,
532 "cannot specify `needs-asm-support` twice",
534 this
.needs_asm_support
= true;
536 "aux-build" => (this
, args
, _span
){
537 let name
= match args
.split_once(":") {
538 Some((name
, rest
)) => {
539 this
.error(rest
.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name");
544 this
.aux_builds
.push(name
.map(Into
::into
));
546 "edition" => (this
, args
, span
){
547 let prev
= this
.edition
.set((*args
).into(), args
.span());
548 this
.check(span
, prev
.is_none(), "cannot specify `edition` twice");
550 "check-pass" => (this
, _args
, span
){
551 let prev
= this
.mode
.set(Mode
::Pass
, span
);
552 // args are ignored (can be used as comment)
556 "cannot specify test mode changes twice",
559 "run" => (this
, args
, span
){
563 "cannot specify test mode changes twice",
565 let mut set
= |exit_code
| this
.mode
.set(Mode
::Run { exit_code }
, args
.span());
570 Ok(exit_code
) => {set(exit_code);}
,
571 Err(err
) => this
.error(args
.span(), err
.to_string()),
575 "require-annotations-for-level" => (this
, args
, span
){
576 let args
= args
.trim();
577 let prev
= match args
.parse() {
578 Ok(it
) => this
.require_annotations_for_level
.set(it
, args
.span()),
580 this
.error(args
.span(), msg
);
588 "cannot specify `require-annotations-for-level` twice",
595 fn parse_command(&mut self, command
: Spanned
<&str>, args
: Spanned
<&str>) {
596 if let Some(command_handler
) = self.commands
.get(*command
) {
597 command_handler(self, args
, command
.span());
598 } else if let Some(s
) = command
.strip_prefix("ignore-") {
599 // args are ignored (can be used as comment)
600 match Condition
::parse(*s
) {
601 Ok(cond
) => self.ignore
.push(cond
),
602 Err(msg
) => self.error(s
.span(), msg
),
604 } else if let Some(s
) = command
.strip_prefix("only-") {
605 // args are ignored (can be used as comment)
606 match Condition
::parse(*s
) {
607 Ok(cond
) => self.only
.push(cond
),
608 Err(msg
) => self.error(s
.span(), msg
),
611 let best_match
= self
614 .min_by_key(|key
| levenshtein
::levenshtein(key
, *command
))
619 "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?",
627 impl<CommentsType
> CommentParser
<CommentsType
> {
628 fn parse_regex(&mut self, regex
: Spanned
<&str>) -> Option
<Regex
> {
629 match Regex
::new(*regex
) {
630 Ok(regex
) => Some(regex
),
632 self.error(regex
.span(), format
!("invalid regex: {err:?}"));
638 /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is
639 /// returned in the first component. `\` can be used to escape arbitrary character.
640 /// Second return component is the rest of the string with leading whitespace removed.
641 fn parse_str
<'a
>(&mut self, s
: Spanned
<&'a
str>) -> (Spanned
<&'a
str>, Spanned
<&'a
str>) {
642 match s
.strip_prefix("\"") {
644 let mut escaped
= false;
645 for (i
, c
) in s
.char_indices() {
647 // Accept any character as literal after a `\`.
650 let (a, b) = s.split_at(i);
651 let b = b.split_at(1).1;
652 return (a, b.trim_start());
657 self.error(s.span(), format!("no closing quotes found
for {}
", *s));
658 (s, Spanned::new("", s.span()))
662 self.error(s.span(), "expected quoted string
, but found end of line
")
666 format!("expected `
\"`
, got `{}`
", s.chars().next().unwrap()),
669 (s, Spanned::new("", s.span()))
674 // parse something like \[[a-z]+(,[a-z]+)*\]
675 fn parse_revisions<'a>(
677 pattern: Spanned<&'a str>,
678 ) -> (Spanned<Vec<String>>, Spanned<&'a str>) {
679 match pattern.strip_prefix("[") {
682 let end = s.char_indices().find_map(|(i, c)| match c {
686 let Some(end) = end else {
687 self.error(s.span(), "`
[` without corresponding `
]`
");
689 Spanned::new(vec![], pattern.span().shrink_to_start()),
693 let (revision, pattern) = s.split_at(end);
694 let revisions = revision.split(',').map(|s| s.trim().to_string()).collect();
696 Spanned::new(revisions, revision.span()),
697 // 1.. because `split_at` includes the separator
698 pattern.split_at(1).1.trim_start(),
702 Spanned::new(vec![], pattern.span().shrink_to_start()),
709 impl CommentParser<&mut Revisioned> {
710 // parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*)
711 fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) {
712 let (match_line, pattern) = match pattern.chars().next() {
714 match fallthrough_to {
715 Some(fallthrough) => *fallthrough,
717 self.error(pattern.span(), "`
//~|` pattern without preceding line");
721 pattern
.split_at(1).1,
724 let offset
= pattern
.chars().take_while(|&c
| c
== '
^').count();
730 .and_then(NonZeroUsize
::new
)
732 // lines are one-indexed, so a target line of 0 is invalid, but also
733 // prevented via `NonZeroUsize`
734 Some(match_line
) => (match_line
, pattern
.split_at(offset
).1),
736 self.error(pattern
.span(), format
!(
737 "//~^ pattern is trying to refer to {} lines above, but there are only {} lines above",
739 pattern
.line().get() - 1,
745 Some(_
) => (pattern
.span().line_start
, pattern
),
747 self.error(pattern
.span(), "no pattern specified");
752 let pattern
= pattern
.trim_start();
753 let offset
= match pattern
.chars().position(|c
| !c
.is_ascii_alphabetic()) {
754 Some(offset
) => offset
,
756 self.error(pattern
.span(), "pattern without level");
761 let (level
, pattern
) = pattern
.split_at(offset
);
762 let level
= match (*level
).parse() {
765 self.error(level
.span(), msg
);
769 let pattern
= match pattern
.strip_prefix(":") {
770 Some(offset
) => offset
,
772 self.error(pattern
.span(), "no `:` after level found");
777 let pattern
= pattern
.trim();
779 self.check(pattern
.span(), !pattern
.is_empty(), "no pattern specified");
781 let pattern
= self.parse_error_pattern(pattern
);
783 *fallthrough_to
= Some(match_line
);
785 self.error_matches
.push(ErrorMatch
{
794 pub(crate) fn matches(&self, message
: &str) -> bool
{
796 Pattern
::SubString(s
) => message
.contains(s
),
797 Pattern
::Regex(r
) => r
.is_match(message
.as_bytes()),
802 impl<CommentsType
> CommentParser
<CommentsType
> {
803 fn parse_error_pattern(&mut self, pattern
: Spanned
<&str>) -> Spanned
<Pattern
> {
804 if let Some(regex
) = pattern
.strip_prefix("/") {
805 match regex
.strip_suffix("/") {
806 Some(regex
) => match self.parse_regex(regex
) {
807 Some(r
) => Spanned
::new(Pattern
::Regex(r
), regex
.span()),
808 None
=> Spanned
::new(Pattern
::SubString(pattern
.to_string()), regex
.span()),
813 "expected regex pattern due to leading `/`, but found no closing `/`",
815 Spanned
::new(Pattern
::SubString(pattern
.to_string()), regex
.span())
819 Spanned
::new(Pattern
::SubString(pattern
.to_string()), pattern
.span())