ui_test/
parser.rs

1use crate::{
2    custom_flags::Flag, diagnostics::Level, filter::Match, test_result::Errored, Config, Error,
3};
4use bstr::{ByteSlice, Utf8Error};
5use color_eyre::eyre::Result;
6use regex::bytes::Regex;
7pub use spanned::*;
8use std::{
9    collections::{BTreeMap, HashMap},
10    num::NonZeroUsize,
11};
12
13mod spanned;
14#[cfg(test)]
15mod tests;
16
17/// This crate supports various magic comments that get parsed as file-specific
18/// configuration values. This struct parses them all in one go and then they
19/// get processed by their respective use sites.
20#[derive(Debug, Clone)]
21pub struct Comments {
22    /// List of revision names to execute. Can only be specified once
23    pub revisions: Option<Vec<String>>,
24    /// Comments that are only available under specific revisions.
25    /// The defaults are in key `vec![]`
26    pub revisioned: BTreeMap<Vec<String>, Revisioned>,
27}
28
29impl Default for Comments {
30    fn default() -> Self {
31        let mut this = Self {
32            revisions: Default::default(),
33            revisioned: Default::default(),
34        };
35        this.revisioned.insert(vec![], Revisioned::default());
36        this
37    }
38}
39
40impl Comments {
41    /// Check that a comment isn't specified twice across multiple differently revisioned statements.
42    /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up
43    /// specifying two error patterns that are available in revision `foo`.
44    pub fn find_one_for_revision<'a, T: 'a>(
45        &'a self,
46        revision: &'a str,
47        kind: &str,
48        f: impl Fn(&'a Revisioned) -> OptWithLine<T>,
49    ) -> Result<OptWithLine<T>, Errored> {
50        let mut result = None;
51        let mut errors = vec![];
52        for (k, rev) in &self.revisioned {
53            if !k.iter().any(|r| r == revision) {
54                continue;
55            }
56            if let Some(found) = f(rev).into_inner() {
57                if result.is_some() {
58                    errors.push(found.span);
59                } else {
60                    result = found.into();
61                }
62            }
63        }
64        if result.is_none() {
65            result = f(&self.revisioned[&[][..]]).into_inner();
66        }
67        if errors.is_empty() {
68            Ok(result.into())
69        } else {
70            Err(Errored {
71                command: format!("<finding flags for revision `{revision}`>"),
72                errors: vec![Error::MultipleRevisionsWithResults {
73                    kind: kind.to_string(),
74                    lines: errors,
75                }],
76                stderr: vec![],
77                stdout: vec![],
78            })
79        }
80    }
81
82    /// Returns an iterator over all revisioned comments that match the revision.
83    pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> {
84        [&self.revisioned[&[][..]]].into_iter().chain(
85            self.revisioned
86                .iter()
87                .filter_map(move |(k, v)| k.iter().any(|rev| rev == revision).then_some(v)),
88        )
89    }
90
91    /// The comments set for all revisions
92    pub fn base(&mut self) -> &mut Revisioned {
93        self.revisioned.get_mut(&[][..]).unwrap()
94    }
95
96    /// The comments set for all revisions
97    pub fn base_immut(&self) -> &Revisioned {
98        self.revisioned.get(&[][..]).unwrap()
99    }
100
101    pub(crate) fn exit_status(&self, revision: &str) -> Result<Option<Spanned<i32>>, Errored> {
102        Ok(self
103            .find_one_for_revision(revision, "`exit_status` annotations", |r| {
104                r.exit_status.clone()
105            })?
106            .into_inner())
107    }
108
109    pub(crate) fn require_annotations(&self, revision: &str) -> Option<Spanned<bool>> {
110        self.for_revision(revision).fold(None, |acc, elem| {
111            elem.require_annotations.as_ref().cloned().or(acc)
112        })
113    }
114}
115
116#[derive(Debug, Clone, Default)]
117/// Comments that can be filtered for specific revisions.
118pub struct Revisioned {
119    /// The character range in which this revisioned item was first added.
120    /// Used for reporting errors on unknown revisions.
121    pub span: Span,
122    /// Don't run this test if any of these filters apply
123    pub ignore: Vec<Condition>,
124    /// Only run this test if all of these filters apply
125    pub only: Vec<Condition>,
126    /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar
127    pub stderr_per_bitwidth: bool,
128    /// Additional flags to pass to the executable
129    pub compile_flags: Vec<String>,
130    /// Additional env vars to set for the executable
131    pub env_vars: Vec<(String, String)>,
132    /// Normalizations to apply to the stderr output before emitting it to disk
133    pub normalize_stderr: Vec<(Match, Vec<u8>)>,
134    /// Normalizations to apply to the stdout output before emitting it to disk
135    pub normalize_stdout: Vec<(Match, Vec<u8>)>,
136    /// Arbitrary patterns to look for in the stderr.
137    /// The error must be from another file, as errors from the current file must be
138    /// checked via `error_matches`.
139    pub(crate) error_in_other_files: Vec<Spanned<Pattern>>,
140    pub(crate) error_matches: Vec<ErrorMatch>,
141    /// Ignore diagnostics below this level.
142    /// `None` means pick the lowest level from the `error_pattern`s.
143    pub require_annotations_for_level: OptWithLine<Level>,
144    /// The exit status that the driver is expected to emit.
145    /// If `None`, any exit status is accepted.
146    pub exit_status: OptWithLine<i32>,
147    /// `Some(true)` means annotations are required
148    /// `Some(false)` means annotations are forbidden
149    /// `None` means this revision does not change the base annoatation requirement.
150    pub require_annotations: OptWithLine<bool>,
151    /// Prefix added to all diagnostic code matchers. Note this will make it impossible
152    /// match codes which do not contain this prefix.
153    pub diagnostic_code_prefix: OptWithLine<String>,
154    /// Tester-specific flags.
155    /// The keys are just labels for overwriting or retrieving the value later.
156    /// They are mostly used by `Config::custom_comments` handlers,
157    /// `ui_test` itself only ever looks at the values, not the keys.
158    ///
159    /// You usually don't modify this directly but use the `add_custom` or `set_custom_once`
160    /// helpers.
161    pub custom: BTreeMap<&'static str, Spanned<Vec<Box<dyn Flag>>>>,
162}
163
164impl Revisioned {
165    /// Append another flag to an existing or new key
166    pub fn add_custom(&mut self, key: &'static str, custom: impl Flag + 'static) {
167        self.add_custom_spanned(key, custom, Span::default())
168    }
169
170    /// Append another flag to an existing or new key
171    pub fn add_custom_spanned(
172        &mut self,
173        key: &'static str,
174        custom: impl Flag + 'static,
175        span: Span,
176    ) {
177        self.custom
178            .entry(key)
179            .or_insert_with(|| Spanned::new(vec![], span))
180            .content
181            .push(Box::new(custom));
182    }
183    /// Override or set a flag
184    pub fn set_custom(&mut self, key: &'static str, custom: impl Flag + 'static) {
185        self.custom
186            .insert(key, Spanned::dummy(vec![Box::new(custom)]));
187    }
188}
189
190/// Main entry point to parsing comments and handling parsing errors.
191#[derive(Debug)]
192pub struct CommentParser<T> {
193    /// The comments being built.
194    comments: T,
195    /// Any errors that ocurred during comment parsing.
196    errors: Vec<Error>,
197    /// The available commands and their parsing logic
198    commands: HashMap<&'static str, CommandParserFunc>,
199    /// The symbol(s) that signify the start of a comment.
200    comment_start: &'static str,
201}
202
203/// Command parser function type.
204pub type CommandParserFunc =
205    fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span);
206
207impl<T> std::ops::Deref for CommentParser<T> {
208    type Target = T;
209
210    fn deref(&self) -> &Self::Target {
211        &self.comments
212    }
213}
214
215impl<T> std::ops::DerefMut for CommentParser<T> {
216    fn deref_mut(&mut self) -> &mut Self::Target {
217        &mut self.comments
218    }
219}
220
221/// The conditions used for "ignore" and "only" filters.
222#[derive(Debug, Clone)]
223pub enum Condition {
224    /// One of the given strings must appear in the host triple.
225    Host(Vec<TargetSubStr>),
226    /// One of the given string must appear in the target triple.
227    Target(Vec<TargetSubStr>),
228    /// Tests that the bitwidth is one of the given ones.
229    Bitwidth(Vec<u8>),
230    /// Tests that the target is the host.
231    OnHost,
232}
233
234/// A sub string of a target (or a whole target).
235///
236/// Effectively a `String` that only allows lowercase chars, integers and dashes.
237#[derive(Debug, Clone)]
238pub struct TargetSubStr(String);
239
240impl PartialEq<&str> for TargetSubStr {
241    fn eq(&self, other: &&str) -> bool {
242        self.0 == *other
243    }
244}
245
246impl std::ops::Deref for TargetSubStr {
247    type Target = str;
248
249    fn deref(&self) -> &Self::Target {
250        &self.0
251    }
252}
253
254impl TryFrom<String> for TargetSubStr {
255    type Error = String;
256
257    fn try_from(value: String) -> std::result::Result<Self, Self::Error> {
258        if value
259            .chars()
260            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
261        {
262            Ok(Self(value))
263        } else {
264            Err(format!(
265                "target strings can only contain integers, basic alphabet characters or dashes"
266            ))
267        }
268    }
269}
270
271/// An error pattern parsed from a `//~` comment.
272#[derive(Debug, Clone)]
273pub enum Pattern {
274    /// A substring that must appear in the error message.
275    SubString(String),
276    /// A regex that must match the error message.
277    Regex(Regex),
278}
279
280#[derive(Debug, Clone)]
281pub(crate) enum ErrorMatchKind {
282    /// A level and pattern pair parsed from a `//~ LEVEL: Message` comment.
283    Pattern {
284        pattern: Spanned<Pattern>,
285        level: Level,
286    },
287    /// An error code parsed from a `//~ error_code` comment.
288    Code(Spanned<String>),
289}
290
291#[derive(Debug, Clone)]
292pub(crate) struct ErrorMatch {
293    pub(crate) kind: ErrorMatchKind,
294    /// The line this pattern is expecting to find a message in.
295    pub(crate) line: NonZeroUsize,
296}
297
298impl Condition {
299    fn parse(c: &str, args: &str) -> std::result::Result<Self, String> {
300        let args = args.split_whitespace();
301        match c {
302            "on-host" => Ok(Condition::OnHost),
303            "bitwidth" => {
304                let bits = args.map(|arg| arg.parse::<u8>().map_err(|_err| {
305                    format!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith")
306                })).collect::<Result<Vec<_>, _>>()?;
307                Ok(Condition::Bitwidth(bits))
308            }
309            "target" => Ok(Condition::Target(args.take_while(|&arg| arg != "#").map(|arg|TargetSubStr::try_from(arg.to_owned())).collect::<Result<_, _>>()?)),
310            "host" => Ok(Condition::Host(args.take_while(|&arg| arg != "#").map(|arg|TargetSubStr::try_from(arg.to_owned())).collect::<Result<_, _>>()?)),
311            _ => Err(format!("`{c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/")),
312        }
313    }
314}
315
316enum ParsePatternResult {
317    Other,
318    ErrorAbove {
319        match_line: NonZeroUsize,
320    },
321    ErrorBelow {
322        span: Span,
323        match_line: NonZeroUsize,
324    },
325    Fallthrough {
326        span: Span,
327        idx: usize,
328    },
329}
330
331impl Comments {
332    /// Parse comments in `content`.
333    /// `path` is only used to emit diagnostics if parsing fails.
334    pub(crate) fn parse(
335        content: Spanned<&[u8]>,
336        config: &Config,
337    ) -> std::result::Result<Self, Vec<Error>> {
338        CommentParser::new(config).parse(content)
339    }
340}
341
342impl CommentParser<Comments> {
343    fn new(config: &Config) -> Self {
344        let mut this = Self {
345            comments: config.comment_defaults.clone(),
346            errors: vec![],
347            commands: Self::commands(),
348            comment_start: config.comment_start,
349        };
350        this.commands
351            .extend(config.custom_comments.iter().map(|(&k, &v)| (k, v)));
352        this
353    }
354
355    fn parse(mut self, content: Spanned<&[u8]>) -> std::result::Result<Comments, Vec<Error>> {
356        // We take out the existing flags so that we can ensure every test only sets them once
357        // by checking that they haven't already been set.
358        let mut defaults = std::mem::take(self.comments.revisioned.get_mut(&[][..]).unwrap());
359
360        let mut delayed_fallthrough = Vec::new();
361        let mut fallthrough_to = None; // The line that a `|` will refer to.
362        let mut last_line = 0;
363        for (l, line) in content.lines().enumerate() {
364            last_line = l + 1;
365            let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1
366            match self.parse_checked_line(fallthrough_to, l, line) {
367                Ok(ParsePatternResult::Other) => {
368                    fallthrough_to = None;
369                }
370                Ok(ParsePatternResult::ErrorAbove { match_line }) => {
371                    fallthrough_to = Some(match_line);
372                }
373                Ok(ParsePatternResult::Fallthrough { span, idx }) => {
374                    delayed_fallthrough.push((span, l, idx));
375                }
376                Ok(ParsePatternResult::ErrorBelow { span, match_line }) => {
377                    if fallthrough_to.is_some() {
378                        self.error(
379                            span,
380                            "`//~v` comment immediately following a `//~^` comment chain",
381                        );
382                    }
383
384                    for (span, line, idx) in delayed_fallthrough.drain(..) {
385                        if let Some(rev) =
386                            self.comments.revisioned.values_mut().find(|rev| {
387                                rev.error_matches.get(idx).is_some_and(|m| m.line == line)
388                            })
389                        {
390                            rev.error_matches[idx].line = match_line;
391                        } else {
392                            self.error(span, "`//~|` comment not attached to anchoring matcher");
393                        }
394                    }
395                }
396                Err(e) => self.error(e.span, format!("Comment is not utf8: {:?}", e.content)),
397            }
398        }
399        if let Some(revisions) = &self.comments.revisions {
400            for (key, revisioned) in &self.comments.revisioned {
401                for rev in key {
402                    if !revisions.contains(rev) {
403                        self.errors.push(Error::InvalidComment {
404                            msg: format!("the revision `{rev}` is not known"),
405                            span: revisioned.span.clone(),
406                        })
407                    }
408                }
409            }
410        } else {
411            for (key, revisioned) in &self.comments.revisioned {
412                if !key.is_empty() {
413                    self.errors.push(Error::InvalidComment {
414                        msg: "there are no revisions in this test".into(),
415                        span: revisioned.span.clone(),
416                    })
417                }
418            }
419        }
420
421        for revisioned in self.comments.revisioned.values() {
422            for m in &revisioned.error_matches {
423                if m.line.get() > last_line {
424                    let span = match &m.kind {
425                        ErrorMatchKind::Pattern { pattern, .. } => pattern.span(),
426                        ErrorMatchKind::Code(code) => code.span(),
427                    };
428                    self.errors.push(Error::InvalidComment {
429                        msg: format!(
430                            "//~v pattern is trying to refer to line {}, but the file only has {} lines",
431                            m.line.get(),
432                            last_line,
433                        ),
434                        span,
435                    });
436                }
437            }
438        }
439
440        for (span, ..) in delayed_fallthrough {
441            self.error(span, "`//~|` comment not attached to anchoring matcher");
442        }
443
444        let Revisioned {
445            span,
446            ignore,
447            only,
448            stderr_per_bitwidth,
449            compile_flags,
450            env_vars,
451            normalize_stderr,
452            normalize_stdout,
453            error_in_other_files,
454            error_matches,
455            require_annotations_for_level,
456            exit_status,
457            require_annotations,
458            diagnostic_code_prefix,
459            custom,
460        } = &mut defaults;
461
462        // We insert into the defaults so that the defaults are first in case of sorted lists
463        // like `normalize_stderr`, `compile_flags`, or `env_vars`
464        let base = std::mem::take(self.comments.base());
465        if span.is_dummy() {
466            *span = base.span;
467        }
468        ignore.extend(base.ignore);
469        only.extend(base.only);
470        *stderr_per_bitwidth |= base.stderr_per_bitwidth;
471        compile_flags.extend(base.compile_flags);
472        env_vars.extend(base.env_vars);
473        normalize_stderr.extend(base.normalize_stderr);
474        normalize_stdout.extend(base.normalize_stdout);
475        error_in_other_files.extend(base.error_in_other_files);
476        error_matches.extend(base.error_matches);
477        if base.require_annotations_for_level.is_some() {
478            *require_annotations_for_level = base.require_annotations_for_level;
479        }
480        if base.exit_status.is_some() {
481            *exit_status = base.exit_status;
482        }
483        if base.require_annotations.is_some() {
484            *require_annotations = base.require_annotations;
485        }
486        if base.diagnostic_code_prefix.is_some() {
487            *diagnostic_code_prefix = base.diagnostic_code_prefix;
488        }
489
490        for (k, v) in base.custom {
491            custom.insert(k, v);
492        }
493
494        *self.base() = defaults;
495
496        if self.errors.is_empty() {
497            Ok(self.comments)
498        } else {
499            Err(self.errors)
500        }
501    }
502}
503
504impl CommentParser<Comments> {
505    fn parse_checked_line(
506        &mut self,
507        fallthrough_to: Option<NonZeroUsize>,
508        current_line: NonZeroUsize,
509        line: Spanned<&[u8]>,
510    ) -> std::result::Result<ParsePatternResult, Spanned<Utf8Error>> {
511        let mut res = ParsePatternResult::Other;
512
513        if let Some((_, comment)) =
514            line.split_once_str(self.comment_start)
515                .filter(|(pre, c)| match &c[..] {
516                    [b'@', ..] => pre.is_empty(),
517                    [b'~', ..] => true,
518                    _ => false,
519                })
520        {
521            if let Some(command) = comment.strip_prefix(b"@") {
522                self.parse_command(command.to_str()?.trim())
523            } else if let Some(pattern) = comment.strip_prefix(b"~") {
524                let (revisions, pattern) = self.parse_revisions(pattern.to_str()?);
525                self.revisioned(revisions, |this| {
526                    res = this.parse_pattern(pattern, fallthrough_to, current_line);
527                })
528            } else {
529                unreachable!()
530            }
531        } else {
532            for pos in line.clone().find_iter(self.comment_start) {
533                let (_, rest) = line.clone().to_str()?.split_at(pos + 2);
534                for rest in std::iter::once(rest.clone()).chain(rest.strip_prefix(" ")) {
535                    let c = rest.chars().next();
536                    if let Some(Spanned {
537                        content: '@' | '~' | '[' | ']' | '^' | '|',
538                        span,
539                    }) = c
540                    {
541                        self.error(
542                            span,
543                            format!(
544                                "comment looks suspiciously like a test suite command: `{}`\n\
545                             All `{}@` test suite commands must be at the start of the line.\n\
546                             The `{}` must be directly followed by `@` or `~`.",
547                                *rest, self.comment_start, self.comment_start,
548                            ),
549                        );
550                    } else {
551                        let mut parser = Self {
552                            errors: vec![],
553                            comments: Comments::default(),
554                            commands: std::mem::take(&mut self.commands),
555                            comment_start: self.comment_start,
556                        };
557                        let span = rest.span();
558                        parser.parse_command(rest);
559                        if parser.errors.is_empty() {
560                            self.error(
561                                span,
562                                format!(
563                                    "a compiletest-rs style comment was detected.\n\
564                                Please use text that could not also be interpreted as a command,\n\
565                                and prefix all actual commands with `{}@`",
566                                    self.comment_start
567                                ),
568                            );
569                        }
570                        self.commands = parser.commands;
571                    }
572                }
573            }
574        }
575        Ok(res)
576    }
577}
578
579impl<CommentsType> CommentParser<CommentsType> {
580    /// Emits an [`InvalidComment`](Error::InvalidComment) error with the given span and message.
581    pub fn error(&mut self, span: Span, s: impl Into<String>) {
582        self.errors.push(Error::InvalidComment {
583            msg: s.into(),
584            span,
585        });
586    }
587
588    /// Checks a condition and emits an error if it is not met.
589    pub fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) {
590        if !cond {
591            self.error(span, s);
592        }
593    }
594
595    /// Checks an option and emits an error if it is `None`.
596    pub fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> {
597        self.check(span, opt.is_some(), s);
598        opt
599    }
600}
601
602impl CommentParser<Comments> {
603    fn parse_command(&mut self, command: Spanned<&str>) {
604        let (revisions, command) = self.parse_revisions(command);
605
606        // Commands are letters or dashes, grab everything until the first character that is neither of those.
607        let (command, args) = match command
608            .char_indices()
609            .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i))
610        {
611            None => {
612                let span = command.span().shrink_to_end();
613                (command, Spanned::new("", span))
614            }
615            Some(i) => {
616                let (command, args) = command.split_at(i);
617                // Commands are separated from their arguments by ':'
618                let next = args
619                    .chars()
620                    .next()
621                    .expect("the `position` above guarantees that there is at least one char");
622                let pos = next.len_utf8();
623                self.check(
624                    next.span,
625                    next.content == ':',
626                    "test command must be followed by `:` (or end the line)",
627                );
628                (command, args.split_at(pos).1.trim())
629            }
630        };
631
632        if *command == "revisions" {
633            self.check(
634                revisions.span(),
635                revisions.is_empty(),
636                "revisions cannot be declared under a revision",
637            );
638            self.check(
639                revisions.span(),
640                self.revisions.is_none(),
641                "cannot specify `revisions` twice",
642            );
643            self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect());
644            return;
645        }
646        self.revisioned(revisions, |this| this.parse_command(command, args));
647    }
648
649    fn revisioned(
650        &mut self,
651        revisions: Spanned<Vec<String>>,
652        f: impl FnOnce(&mut CommentParser<&mut Revisioned>),
653    ) {
654        let Spanned {
655            content: revisions,
656            span,
657        } = revisions;
658        let mut this = CommentParser {
659            comment_start: self.comment_start,
660            errors: std::mem::take(&mut self.errors),
661            commands: std::mem::take(&mut self.commands),
662            comments: self
663                .revisioned
664                .entry(revisions)
665                .or_insert_with(|| Revisioned {
666                    span,
667                    ..Default::default()
668                }),
669        };
670        f(&mut this);
671        let CommentParser {
672            errors, commands, ..
673        } = this;
674        self.commands = commands;
675        self.errors = errors;
676    }
677}
678
679impl CommentParser<&mut Revisioned> {
680    fn parse_normalize_test(
681        &mut self,
682        args: Spanned<&str>,
683        mode: &str,
684    ) -> Option<(Regex, Vec<u8>)> {
685        let (from, rest) = self.parse_str(args);
686
687        let to = match rest.strip_prefix("->") {
688            Some(v) => v,
689            None => {
690                self.error(
691                    rest.span(),
692                    format!(
693                        "normalize-{mode}-test needs a pattern and replacement separated by `->`"
694                    ),
695                );
696                return None;
697            }
698        }
699        .trim_start();
700        let (to, rest) = self.parse_str(to);
701
702        self.check(
703            rest.span(),
704            rest.is_empty(),
705            "trailing text after pattern replacement",
706        );
707
708        let regex = self.parse_regex(from)?.content;
709        Some((regex, to.as_bytes().to_owned()))
710    }
711
712    /// Adds a flag, or errors if it already existed.
713    pub fn set_custom_once(&mut self, key: &'static str, custom: impl Flag + 'static, span: Span) {
714        let prev = self
715            .custom
716            .insert(key, Spanned::new(vec![Box::new(custom)], span.clone()));
717        self.check(
718            span,
719            prev.is_none(),
720            format!("cannot specify `{key}` twice"),
721        );
722    }
723}
724
725impl CommentParser<Comments> {
726    fn commands() -> HashMap<&'static str, CommandParserFunc> {
727        let mut commands = HashMap::<_, CommandParserFunc>::new();
728        macro_rules! commands {
729            ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => {
730                $(commands.insert($name, |$this, $args, $span| {
731                    $block
732                });)*
733            };
734        }
735        commands! {
736            "compile-flags" => (this, args, _span){
737                if let Some(parsed) = comma::parse_command(*args) {
738                    this.compile_flags.extend(parsed);
739                } else {
740                    this.error(args.span(), format!("`{}` contains an unclosed quotation mark", *args));
741                }
742            }
743            "rustc-env" => (this, args, _span){
744                for env in args.split_whitespace() {
745                    if let Some((k, v)) = this.check_some(
746                        args.span(),
747                        env.split_once('='),
748                        "environment variables must be key/value pairs separated by a `=`",
749                    ) {
750                        this.env_vars.push((k.to_string(), v.to_string()));
751                    }
752                }
753            }
754            "normalize-stderr-test" => (this, args, _span){
755                if let Some((regex, replacement)) = this.parse_normalize_test(args, "stderr") {
756                    this.normalize_stderr.push((regex.into(), replacement))
757                }
758            }
759            "normalize-stdout-test" => (this, args, _span){
760                if let Some((regex, replacement)) = this.parse_normalize_test(args, "stdout") {
761                    this.normalize_stdout.push((regex.into(), replacement))
762                }
763            }
764            "error-pattern" => (this, _args, span){
765                this.error(span, "`error-pattern` has been renamed to `error-in-other-file`");
766            }
767            "error-in-other-file" => (this, args, _span){
768                let args = args.trim();
769                let pat = this.parse_error_pattern(args);
770                this.error_in_other_files.push(pat);
771            }
772            "stderr-per-bitwidth" => (this, _args, span){
773                // args are ignored (can be used as comment)
774                this.check(
775                    span,
776                    !this.stderr_per_bitwidth,
777                    "cannot specify `stderr-per-bitwidth` twice",
778                );
779                this.stderr_per_bitwidth = true;
780            }
781            "run-rustfix" => (this, _args, span){
782                this.error(span, "rustfix is now ran by default when applicable suggestions are found");
783            }
784            "check-pass" => (this, _args, span){
785                _ = this.exit_status.set(0, span.clone());
786                this.require_annotations = Spanned::new(false, span.clone()).into();
787            }
788            "require-annotations-for-level" => (this, args, span){
789                let args = args.trim();
790                let prev = match args.content.parse() {
791                    Ok(it) =>  this.require_annotations_for_level.set(it, args.span()),
792                    Err(msg) => {
793                        this.error(args.span(), msg);
794                        None
795                    },
796                };
797
798                this.check(
799                    span,
800                    prev.is_none(),
801                    "cannot specify `require-annotations-for-level` twice",
802                );
803            }
804        }
805        commands
806    }
807}
808
809impl CommentParser<&mut Revisioned> {
810    fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) {
811        if let Some(command_handler) = self.commands.get(*command) {
812            command_handler(self, args, command.span());
813        } else if let Some(rest) = command
814            .strip_prefix("ignore-")
815            .or_else(|| command.strip_prefix("only-"))
816        {
817            // args are ignored (can be used as comment)
818            match Condition::parse(*rest, *args) {
819                Ok(cond) => {
820                    if command.starts_with("ignore") {
821                        self.ignore.push(cond)
822                    } else {
823                        self.only.push(cond)
824                    }
825                }
826                Err(msg) => self.error(rest.span(), msg),
827            }
828        } else {
829            let best_match = self
830                .commands
831                .keys()
832                .min_by_key(|key| levenshtein::levenshtein(key, *command))
833                .unwrap();
834            self.error(
835                command.span(),
836                format!(
837                    "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?",
838                    *command
839                ),
840            );
841        }
842    }
843}
844
845impl<CommentsType> CommentParser<CommentsType> {
846    fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Spanned<Regex>> {
847        match Regex::new(*regex) {
848            Ok(r) => Some(regex.map(|_| r)),
849            Err(err) => {
850                self.error(regex.span(), format!("invalid regex: {err:?}"));
851                None
852            }
853        }
854    }
855
856    /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is
857    /// returned in the first component. `\` can be used to escape arbitrary character.
858    /// Second return component is the rest of the string with leading whitespace removed.
859    fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) {
860        match s.strip_prefix("\"") {
861            Some(s) => {
862                let mut escaped = false;
863                for (i, c) in s.char_indices() {
864                    if escaped {
865                        // Accept any character as literal after a `\`.
866                        escaped = false;
867                    } else if c == '"' {
868                        let (a, b) = s.split_at(i);
869                        let b = b.split_at(1).1;
870                        return (a, b.trim_start());
871                    } else {
872                        escaped = c == '\\';
873                    }
874                }
875                self.error(s.span(), format!("no closing quotes found for {}", *s));
876                let span = s.span();
877                (s, Spanned::new("", span))
878            }
879            None => {
880                if s.is_empty() {
881                    self.error(s.span(), "expected quoted string, but found end of line")
882                } else {
883                    let c = s.chars().next().unwrap();
884                    self.error(c.span, format!("expected `\"`, got `{}`", c.content))
885                }
886                let span = s.span();
887                (s, Spanned::new("", span))
888            }
889        }
890    }
891
892    // parse something like \[[a-z]+(,[a-z]+)*\]
893    fn parse_revisions<'a>(
894        &mut self,
895        pattern: Spanned<&'a str>,
896    ) -> (Spanned<Vec<String>>, Spanned<&'a str>) {
897        match pattern.strip_prefix("[") {
898            Some(s) => {
899                // revisions
900                let end = s.char_indices().find_map(|(i, c)| match c {
901                    ']' => Some(i),
902                    _ => None,
903                });
904                let Some(end) = end else {
905                    self.error(s.span(), "`[` without corresponding `]`");
906                    return (
907                        Spanned::new(vec![], pattern.span().shrink_to_start()),
908                        pattern,
909                    );
910                };
911                let (revision, pattern) = s.split_at(end);
912                let revisions = revision.split(',').map(|s| s.trim().to_string()).collect();
913                (
914                    Spanned::new(revisions, revision.span()),
915                    // 1.. because `split_at` includes the separator
916                    pattern.split_at(1).1.trim_start(),
917                )
918            }
919            _ => (
920                Spanned::new(vec![], pattern.span().shrink_to_start()),
921                pattern,
922            ),
923        }
924    }
925}
926
927impl CommentParser<&mut Revisioned> {
928    // parse something like:
929    // (\[[a-z]+(,[a-z]+)*\])?
930    // (?P<offset>\||[\^]+)? *
931    // ((?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*))|(?P<code>[a-z0-9_:]+)
932    fn parse_pattern(
933        &mut self,
934        pattern: Spanned<&str>,
935        fallthrough_to: Option<NonZeroUsize>,
936        current_line: NonZeroUsize,
937    ) -> ParsePatternResult {
938        let c = pattern.chars().next();
939        let mut res = ParsePatternResult::Other;
940
941        let (match_line, pattern) = match c {
942            Some(Spanned { content: '|', span }) => (
943                match fallthrough_to {
944                    Some(match_line) => {
945                        res = ParsePatternResult::ErrorAbove { match_line };
946                        match_line
947                    }
948                    None => {
949                        res = ParsePatternResult::Fallthrough {
950                            span,
951                            idx: self.error_matches.len(),
952                        };
953                        current_line
954                    }
955                },
956                pattern.split_at(1).1,
957            ),
958            Some(Spanned {
959                content: '^',
960                span: _,
961            }) => {
962                let offset = pattern.chars().take_while(|c| c.content == '^').count();
963                match current_line
964                    .get()
965                    .checked_sub(offset)
966                    .and_then(NonZeroUsize::new)
967                {
968                    // lines are one-indexed, so a target line of 0 is invalid, but also
969                    // prevented via `NonZeroUsize`
970                    Some(match_line) => {
971                        res = ParsePatternResult::ErrorAbove { match_line };
972                        (match_line, pattern.split_at(offset).1)
973                    }
974                    _ => {
975                        self.error(pattern.span(), format!(
976                            "{}~^ pattern is trying to refer to {} lines above, but there are only {} lines above",
977                            self.comment_start,
978                            offset,
979                            current_line.get() - 1,
980                        ));
981                        return ParsePatternResult::ErrorAbove {
982                            match_line: current_line,
983                        };
984                    }
985                }
986            }
987            Some(Spanned {
988                content: 'v',
989                span: _,
990            }) => {
991                let offset = pattern.chars().take_while(|c| c.content == 'v').count();
992                match current_line
993                    .get()
994                    .checked_add(offset)
995                    .and_then(NonZeroUsize::new)
996                {
997                    Some(match_line) => {
998                        res = ParsePatternResult::ErrorBelow {
999                            span: pattern.span(),
1000                            match_line,
1001                        };
1002                        (match_line, pattern.split_at(offset).1)
1003                    }
1004                    _ => {
1005                        // The line count of the file is not yet known so we can only check
1006                        // if the resulting line is in the range of a usize.
1007                        self.error(pattern.span(), format!(
1008                            "{}~v pattern is trying to refer to {} lines below, which is more than ui_test can count",
1009                            self.comment_start,
1010                            offset,
1011                        ));
1012                        return ParsePatternResult::ErrorBelow {
1013                            span: pattern.span(),
1014                            match_line: current_line,
1015                        };
1016                    }
1017                }
1018            }
1019            Some(_) => (current_line, pattern),
1020            None => {
1021                self.error(pattern.span(), "no pattern specified");
1022                return res;
1023            }
1024        };
1025
1026        let pattern = pattern.trim_start();
1027        let offset = pattern
1028            .bytes()
1029            .position(|c| !(c.is_ascii_alphanumeric() || c == b'_' || c == b':'))
1030            .unwrap_or(pattern.len());
1031
1032        let (level_or_code, pattern) = pattern.split_at(offset);
1033        if let Some(level) = level_or_code.strip_suffix(":") {
1034            let level = match (*level).parse() {
1035                Ok(level) => level,
1036                Err(msg) => {
1037                    self.error(level.span(), msg);
1038                    return res;
1039                }
1040            };
1041
1042            let pattern = pattern.trim();
1043
1044            self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
1045
1046            let pattern = self.parse_error_pattern(pattern);
1047
1048            self.error_matches.push(ErrorMatch {
1049                kind: ErrorMatchKind::Pattern { pattern, level },
1050                line: match_line,
1051            });
1052        } else if (*level_or_code).parse::<Level>().is_ok() {
1053            // Shouldn't conflict with any real diagnostic code
1054            self.error(level_or_code.span(), "no `:` after level found");
1055            return res;
1056        } else if !pattern.trim_start().is_empty() {
1057            self.error(
1058                pattern.span(),
1059                format!("text found after error code `{}`", *level_or_code),
1060            );
1061            return res;
1062        } else {
1063            self.error_matches.push(ErrorMatch {
1064                kind: ErrorMatchKind::Code(Spanned::new(
1065                    level_or_code.to_string(),
1066                    level_or_code.span(),
1067                )),
1068                line: match_line,
1069            });
1070        };
1071
1072        res
1073    }
1074}
1075
1076impl Pattern {
1077    pub(crate) fn matches(&self, message: &str) -> bool {
1078        match self {
1079            Pattern::SubString(s) => message.contains(s),
1080            Pattern::Regex(r) => r.is_match(message.as_bytes()),
1081        }
1082    }
1083}
1084
1085impl<CommentsType> CommentParser<CommentsType> {
1086    fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> {
1087        if let Some(regex) = pattern.strip_prefix("/") {
1088            match regex.strip_suffix("/") {
1089                Some(regex) => match self.parse_regex(regex) {
1090                    Some(r) => r.map(Pattern::Regex),
1091                    None => pattern.map(|p| Pattern::SubString(p.to_string())),
1092                },
1093                None => {
1094                    self.error(
1095                        regex.span(),
1096                        "expected regex pattern due to leading `/`, but found no closing `/`",
1097                    );
1098                    pattern.map(|p| Pattern::SubString(p.to_string()))
1099                }
1100            }
1101        } else {
1102            pattern.map(|p| Pattern::SubString(p.to_string()))
1103        }
1104    }
1105}