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: Comments = Self {
32 revisions: Default::default(),
33 revisioned: Default::default(),
34 };
35 this.revisioned.insert(key:vec![], value: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 valueChars<'_>
259 .chars()
260 .all(|c: char| 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: SplitWhitespace<'_> = args.split_whitespace();
301 match c {
302 "on-host" => Ok(Condition::OnHost),
303 "bitwidth" => {
304 let bits: Vec = args.map(|arg: &str| arg.parse::<u8>().map_err(|_err: ParseIntError| {
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: &str| arg != "#").map(|arg: &str|TargetSubStr::try_from(arg.to_owned())).collect::<Result<_, _>>()?)),
310 "host" => Ok(Condition::Host(args.take_while(|&arg: &str| arg != "#").map(|arg: &str|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) = self
386 .comments
387 .revisioned
388 .values_mut()
389 .find(|rev| rev.error_matches[idx].line == line)
390 {
391 rev.error_matches[idx].line = match_line;
392 } else {
393 self.error(span, "`//~|` comment not attached to anchoring matcher");
394 }
395 }
396 }
397 Err(e) => self.error(e.span, format!("Comment is not utf8: {:?}", e.content)),
398 }
399 }
400 if let Some(revisions) = &self.comments.revisions {
401 for (key, revisioned) in &self.comments.revisioned {
402 for rev in key {
403 if !revisions.contains(rev) {
404 self.errors.push(Error::InvalidComment {
405 msg: format!("the revision `{rev}` is not known"),
406 span: revisioned.span.clone(),
407 })
408 }
409 }
410 }
411 } else {
412 for (key, revisioned) in &self.comments.revisioned {
413 if !key.is_empty() {
414 self.errors.push(Error::InvalidComment {
415 msg: "there are no revisions in this test".into(),
416 span: revisioned.span.clone(),
417 })
418 }
419 }
420 }
421
422 for revisioned in self.comments.revisioned.values() {
423 for m in &revisioned.error_matches {
424 if m.line.get() > last_line {
425 let span = match &m.kind {
426 ErrorMatchKind::Pattern { pattern, .. } => pattern.span(),
427 ErrorMatchKind::Code(code) => code.span(),
428 };
429 self.errors.push(Error::InvalidComment {
430 msg: format!(
431 "//~v pattern is trying to refer to line {}, but the file only has {} lines",
432 m.line.get(),
433 last_line,
434 ),
435 span,
436 });
437 }
438 }
439 }
440
441 for (span, ..) in delayed_fallthrough {
442 self.error(span, "`//~|` comment not attached to anchoring matcher");
443 }
444
445 let Revisioned {
446 span,
447 ignore,
448 only,
449 stderr_per_bitwidth,
450 compile_flags,
451 env_vars,
452 normalize_stderr,
453 normalize_stdout,
454 error_in_other_files,
455 error_matches,
456 require_annotations_for_level,
457 exit_status,
458 require_annotations,
459 diagnostic_code_prefix,
460 custom,
461 } = &mut defaults;
462
463 // We insert into the defaults so that the defaults are first in case of sorted lists
464 // like `normalize_stderr`, `compile_flags`, or `env_vars`
465 let base = std::mem::take(self.comments.base());
466 if span.is_dummy() {
467 *span = base.span;
468 }
469 ignore.extend(base.ignore);
470 only.extend(base.only);
471 *stderr_per_bitwidth |= base.stderr_per_bitwidth;
472 compile_flags.extend(base.compile_flags);
473 env_vars.extend(base.env_vars);
474 normalize_stderr.extend(base.normalize_stderr);
475 normalize_stdout.extend(base.normalize_stdout);
476 error_in_other_files.extend(base.error_in_other_files);
477 error_matches.extend(base.error_matches);
478 if base.require_annotations_for_level.is_some() {
479 *require_annotations_for_level = base.require_annotations_for_level;
480 }
481 if base.exit_status.is_some() {
482 *exit_status = base.exit_status;
483 }
484 if base.require_annotations.is_some() {
485 *require_annotations = base.require_annotations;
486 }
487 if base.diagnostic_code_prefix.is_some() {
488 *diagnostic_code_prefix = base.diagnostic_code_prefix;
489 }
490
491 for (k, v) in base.custom {
492 custom.insert(k, v);
493 }
494
495 *self.base() = defaults;
496
497 if self.errors.is_empty() {
498 Ok(self.comments)
499 } else {
500 Err(self.errors)
501 }
502 }
503}
504
505impl CommentParser<Comments> {
506 fn parse_checked_line(
507 &mut self,
508 fallthrough_to: Option<NonZeroUsize>,
509 current_line: NonZeroUsize,
510 line: Spanned<&[u8]>,
511 ) -> std::result::Result<ParsePatternResult, Spanned<Utf8Error>> {
512 let mut res = ParsePatternResult::Other;
513
514 if let Some((_, comment)) =
515 line.split_once_str(self.comment_start)
516 .filter(|(pre, c)| match &c[..] {
517 [b'@', ..] => pre.is_empty(),
518 [b'~', ..] => true,
519 _ => false,
520 })
521 {
522 if let Some(command) = comment.strip_prefix(b"@") {
523 self.parse_command(command.to_str()?.trim())
524 } else if let Some(pattern) = comment.strip_prefix(b"~") {
525 let (revisions, pattern) = self.parse_revisions(pattern.to_str()?);
526 self.revisioned(revisions, |this| {
527 res = this.parse_pattern(pattern, fallthrough_to, current_line);
528 })
529 } else {
530 unreachable!()
531 }
532 } else {
533 for pos in line.clone().find_iter(self.comment_start) {
534 let (_, rest) = line.clone().to_str()?.split_at(pos + 2);
535 for rest in std::iter::once(rest.clone()).chain(rest.strip_prefix(" ")) {
536 let c = rest.chars().next();
537 if let Some(Spanned {
538 content: '@' | '~' | '[' | ']' | '^' | '|',
539 span,
540 }) = c
541 {
542 self.error(
543 span,
544 format!(
545 "comment looks suspiciously like a test suite command: `{}`\n\
546 All `{}@` test suite commands must be at the start of the line.\n\
547 The `{}` must be directly followed by `@` or `~`.",
548 *rest, self.comment_start, self.comment_start,
549 ),
550 );
551 } else {
552 let mut parser = Self {
553 errors: vec![],
554 comments: Comments::default(),
555 commands: std::mem::take(&mut self.commands),
556 comment_start: self.comment_start,
557 };
558 let span = rest.span();
559 parser.parse_command(rest);
560 if parser.errors.is_empty() {
561 self.error(
562 span,
563 format!(
564 "a compiletest-rs style comment was detected.\n\
565 Please use text that could not also be interpreted as a command,\n\
566 and prefix all actual commands with `{}@`",
567 self.comment_start
568 ),
569 );
570 }
571 self.commands = parser.commands;
572 }
573 }
574 }
575 }
576 Ok(res)
577 }
578}
579
580impl<CommentsType> CommentParser<CommentsType> {
581 /// Emits an [`InvalidComment`](Error::InvalidComment) error with the given span and message.
582 pub fn error(&mut self, span: Span, s: impl Into<String>) {
583 self.errors.push(Error::InvalidComment {
584 msg: s.into(),
585 span,
586 });
587 }
588
589 /// Checks a condition and emits an error if it is not met.
590 pub fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) {
591 if !cond {
592 self.error(span, s);
593 }
594 }
595
596 /// Checks an option and emits an error if it is `None`.
597 pub fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> {
598 self.check(span, cond:opt.is_some(), s);
599 opt
600 }
601}
602
603impl CommentParser<Comments> {
604 fn parse_command(&mut self, command: Spanned<&str>) {
605 let (revisions, command) = self.parse_revisions(command);
606
607 // Commands are letters or dashes, grab everything until the first character that is neither of those.
608 let (command, args) = match command
609 .char_indices()
610 .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i))
611 {
612 None => {
613 let span = command.span().shrink_to_end();
614 (command, Spanned::new("", span))
615 }
616 Some(i) => {
617 let (command, args) = command.split_at(i);
618 // Commands are separated from their arguments by ':'
619 let next = args
620 .chars()
621 .next()
622 .expect("the `position` above guarantees that there is at least one char");
623 let pos = next.len_utf8();
624 self.check(
625 next.span,
626 next.content == ':',
627 "test command must be followed by `:` (or end the line)",
628 );
629 (command, args.split_at(pos).1.trim())
630 }
631 };
632
633 if *command == "revisions" {
634 self.check(
635 revisions.span(),
636 revisions.is_empty(),
637 "revisions cannot be declared under a revision",
638 );
639 self.check(
640 revisions.span(),
641 self.revisions.is_none(),
642 "cannot specify `revisions` twice",
643 );
644 self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect());
645 return;
646 }
647 self.revisioned(revisions, |this| this.parse_command(command, args));
648 }
649
650 fn revisioned(
651 &mut self,
652 revisions: Spanned<Vec<String>>,
653 f: impl FnOnce(&mut CommentParser<&mut Revisioned>),
654 ) {
655 let Spanned {
656 content: revisions,
657 span,
658 } = revisions;
659 let mut this = CommentParser {
660 comment_start: self.comment_start,
661 errors: std::mem::take(&mut self.errors),
662 commands: std::mem::take(&mut self.commands),
663 comments: self
664 .revisioned
665 .entry(revisions)
666 .or_insert_with(|| Revisioned {
667 span,
668 ..Default::default()
669 }),
670 };
671 f(&mut this);
672 let CommentParser {
673 errors, commands, ..
674 } = this;
675 self.commands = commands;
676 self.errors = errors;
677 }
678}
679
680impl CommentParser<&mut Revisioned> {
681 fn parse_normalize_test(
682 &mut self,
683 args: Spanned<&str>,
684 mode: &str,
685 ) -> Option<(Regex, Vec<u8>)> {
686 let (from, rest) = self.parse_str(args);
687
688 let to = match rest.strip_prefix("->") {
689 Some(v) => v,
690 None => {
691 self.error(
692 rest.span(),
693 format!(
694 "normalize-{mode}-test needs a pattern and replacement separated by `->`"
695 ),
696 );
697 return None;
698 }
699 }
700 .trim_start();
701 let (to, rest) = self.parse_str(to);
702
703 self.check(
704 rest.span(),
705 rest.is_empty(),
706 "trailing text after pattern replacement",
707 );
708
709 let regex = self.parse_regex(from)?.content;
710 Some((regex, to.as_bytes().to_owned()))
711 }
712
713 /// Adds a flag, or errors if it already existed.
714 pub fn set_custom_once(&mut self, key: &'static str, custom: impl Flag + 'static, span: Span) {
715 let prev = self
716 .custom
717 .insert(key, Spanned::new(vec![Box::new(custom)], span.clone()));
718 self.check(
719 span,
720 prev.is_none(),
721 format!("cannot specify `{key}` twice"),
722 );
723 }
724}
725
726impl CommentParser<Comments> {
727 fn commands() -> HashMap<&'static str, CommandParserFunc> {
728 let mut commands = HashMap::<_, CommandParserFunc>::new();
729 macro_rules! commands {
730 ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => {
731 $(commands.insert($name, |$this, $args, $span| {
732 $block
733 });)*
734 };
735 }
736 commands! {
737 "compile-flags" => (this, args, _span){
738 if let Some(parsed) = comma::parse_command(*args) {
739 this.compile_flags.extend(parsed);
740 } else {
741 this.error(args.span(), format!("`{}` contains an unclosed quotation mark", *args));
742 }
743 }
744 "rustc-env" => (this, args, _span){
745 for env in args.split_whitespace() {
746 if let Some((k, v)) = this.check_some(
747 args.span(),
748 env.split_once('='),
749 "environment variables must be key/value pairs separated by a `=`",
750 ) {
751 this.env_vars.push((k.to_string(), v.to_string()));
752 }
753 }
754 }
755 "normalize-stderr-test" => (this, args, _span){
756 if let Some((regex, replacement)) = this.parse_normalize_test(args, "stderr") {
757 this.normalize_stderr.push((regex.into(), replacement))
758 }
759 }
760 "normalize-stdout-test" => (this, args, _span){
761 if let Some((regex, replacement)) = this.parse_normalize_test(args, "stdout") {
762 this.normalize_stdout.push((regex.into(), replacement))
763 }
764 }
765 "error-pattern" => (this, _args, span){
766 this.error(span, "`error-pattern` has been renamed to `error-in-other-file`");
767 }
768 "error-in-other-file" => (this, args, _span){
769 let args = args.trim();
770 let pat = this.parse_error_pattern(args);
771 this.error_in_other_files.push(pat);
772 }
773 "stderr-per-bitwidth" => (this, _args, span){
774 // args are ignored (can be used as comment)
775 this.check(
776 span,
777 !this.stderr_per_bitwidth,
778 "cannot specify `stderr-per-bitwidth` twice",
779 );
780 this.stderr_per_bitwidth = true;
781 }
782 "run-rustfix" => (this, _args, span){
783 this.error(span, "rustfix is now ran by default when applicable suggestions are found");
784 }
785 "check-pass" => (this, _args, span){
786 _ = this.exit_status.set(0, span.clone());
787 this.require_annotations = Spanned::new(false, span.clone()).into();
788 }
789 "require-annotations-for-level" => (this, args, span){
790 let args = args.trim();
791 let prev = match args.content.parse() {
792 Ok(it) => this.require_annotations_for_level.set(it, args.span()),
793 Err(msg) => {
794 this.error(args.span(), msg);
795 None
796 },
797 };
798
799 this.check(
800 span,
801 prev.is_none(),
802 "cannot specify `require-annotations-for-level` twice",
803 );
804 }
805 }
806 commands
807 }
808}
809
810impl CommentParser<&mut Revisioned> {
811 fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) {
812 if let Some(command_handler) = self.commands.get(*command) {
813 command_handler(self, args, command.span());
814 } else if let Some(rest) = command
815 .strip_prefix("ignore-")
816 .or_else(|| command.strip_prefix("only-"))
817 {
818 // args are ignored (can be used as comment)
819 match Condition::parse(*rest, *args) {
820 Ok(cond) => {
821 if command.starts_with("ignore") {
822 self.ignore.push(cond)
823 } else {
824 self.only.push(cond)
825 }
826 }
827 Err(msg) => self.error(rest.span(), msg),
828 }
829 } else {
830 let best_match = self
831 .commands
832 .keys()
833 .min_by_key(|key| levenshtein::levenshtein(key, *command))
834 .unwrap();
835 self.error(
836 command.span(),
837 format!(
838 "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?",
839 *command
840 ),
841 );
842 }
843 }
844}
845
846impl<CommentsType> CommentParser<CommentsType> {
847 fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Spanned<Regex>> {
848 match Regex::new(*regex) {
849 Ok(r) => Some(regex.map(|_| r)),
850 Err(err) => {
851 self.error(regex.span(), format!("invalid regex: {err:?}"));
852 None
853 }
854 }
855 }
856
857 /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is
858 /// returned in the first component. `\` can be used to escape arbitrary character.
859 /// Second return component is the rest of the string with leading whitespace removed.
860 fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) {
861 match s.strip_prefix("\"") {
862 Some(s) => {
863 let mut escaped = false;
864 for (i, c) in s.char_indices() {
865 if escaped {
866 // Accept any character as literal after a `\`.
867 escaped = false;
868 } else if c == '"' {
869 let (a, b) = s.split_at(i);
870 let b = b.split_at(1).1;
871 return (a, b.trim_start());
872 } else {
873 escaped = c == '\\';
874 }
875 }
876 self.error(s.span(), format!("no closing quotes found for {}", *s));
877 let span = s.span();
878 (s, Spanned::new("", span))
879 }
880 None => {
881 if s.is_empty() {
882 self.error(s.span(), "expected quoted string, but found end of line")
883 } else {
884 let c = s.chars().next().unwrap();
885 self.error(c.span, format!("expected `\"`, got `{}`", c.content))
886 }
887 let span = s.span();
888 (s, Spanned::new("", span))
889 }
890 }
891 }
892
893 // parse something like \[[a-z]+(,[a-z]+)*\]
894 fn parse_revisions<'a>(
895 &mut self,
896 pattern: Spanned<&'a str>,
897 ) -> (Spanned<Vec<String>>, Spanned<&'a str>) {
898 match pattern.strip_prefix("[") {
899 Some(s) => {
900 // revisions
901 let end = s.char_indices().find_map(|(i, c)| match c {
902 ']' => Some(i),
903 _ => None,
904 });
905 let Some(end) = end else {
906 self.error(s.span(), "`[` without corresponding `]`");
907 return (
908 Spanned::new(vec![], pattern.span().shrink_to_start()),
909 pattern,
910 );
911 };
912 let (revision, pattern) = s.split_at(end);
913 let revisions = revision.split(',').map(|s| s.trim().to_string()).collect();
914 (
915 Spanned::new(revisions, revision.span()),
916 // 1.. because `split_at` includes the separator
917 pattern.split_at(1).1.trim_start(),
918 )
919 }
920 _ => (
921 Spanned::new(vec![], pattern.span().shrink_to_start()),
922 pattern,
923 ),
924 }
925 }
926}
927
928impl CommentParser<&mut Revisioned> {
929 // parse something like:
930 // (\[[a-z]+(,[a-z]+)*\])?
931 // (?P<offset>\||[\^]+)? *
932 // ((?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*))|(?P<code>[a-z0-9_:]+)
933 fn parse_pattern(
934 &mut self,
935 pattern: Spanned<&str>,
936 fallthrough_to: Option<NonZeroUsize>,
937 current_line: NonZeroUsize,
938 ) -> ParsePatternResult {
939 let c = pattern.chars().next();
940 let mut res = ParsePatternResult::Other;
941
942 let (match_line, pattern) = match c {
943 Some(Spanned { content: '|', span }) => (
944 match fallthrough_to {
945 Some(match_line) => {
946 res = ParsePatternResult::ErrorAbove { match_line };
947 match_line
948 }
949 None => {
950 res = ParsePatternResult::Fallthrough {
951 span,
952 idx: self.error_matches.len(),
953 };
954 current_line
955 }
956 },
957 pattern.split_at(1).1,
958 ),
959 Some(Spanned {
960 content: '^',
961 span: _,
962 }) => {
963 let offset = pattern.chars().take_while(|c| c.content == '^').count();
964 match current_line
965 .get()
966 .checked_sub(offset)
967 .and_then(NonZeroUsize::new)
968 {
969 // lines are one-indexed, so a target line of 0 is invalid, but also
970 // prevented via `NonZeroUsize`
971 Some(match_line) => {
972 res = ParsePatternResult::ErrorAbove { match_line };
973 (match_line, pattern.split_at(offset).1)
974 }
975 _ => {
976 self.error(pattern.span(), format!(
977 "{}~^ pattern is trying to refer to {} lines above, but there are only {} lines above",
978 self.comment_start,
979 offset,
980 current_line.get() - 1,
981 ));
982 return ParsePatternResult::ErrorAbove {
983 match_line: current_line,
984 };
985 }
986 }
987 }
988 Some(Spanned {
989 content: 'v',
990 span: _,
991 }) => {
992 let offset = pattern.chars().take_while(|c| c.content == 'v').count();
993 match current_line
994 .get()
995 .checked_add(offset)
996 .and_then(NonZeroUsize::new)
997 {
998 Some(match_line) => {
999 res = ParsePatternResult::ErrorBelow {
1000 span: pattern.span(),
1001 match_line,
1002 };
1003 (match_line, pattern.split_at(offset).1)
1004 }
1005 _ => {
1006 // The line count of the file is not yet known so we can only check
1007 // if the resulting line is in the range of a usize.
1008 self.error(pattern.span(), format!(
1009 "{}~v pattern is trying to refer to {} lines below, which is more than ui_test can count",
1010 self.comment_start,
1011 offset,
1012 ));
1013 return ParsePatternResult::ErrorBelow {
1014 span: pattern.span(),
1015 match_line: current_line,
1016 };
1017 }
1018 }
1019 }
1020 Some(_) => (current_line, pattern),
1021 None => {
1022 self.error(pattern.span(), "no pattern specified");
1023 return res;
1024 }
1025 };
1026
1027 let pattern = pattern.trim_start();
1028 let offset = pattern
1029 .bytes()
1030 .position(|c| !(c.is_ascii_alphanumeric() || c == b'_' || c == b':'))
1031 .unwrap_or(pattern.len());
1032
1033 let (level_or_code, pattern) = pattern.split_at(offset);
1034 if let Some(level) = level_or_code.strip_suffix(":") {
1035 let level = match (*level).parse() {
1036 Ok(level) => level,
1037 Err(msg) => {
1038 self.error(level.span(), msg);
1039 return res;
1040 }
1041 };
1042
1043 let pattern = pattern.trim();
1044
1045 self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
1046
1047 let pattern = self.parse_error_pattern(pattern);
1048
1049 self.error_matches.push(ErrorMatch {
1050 kind: ErrorMatchKind::Pattern { pattern, level },
1051 line: match_line,
1052 });
1053 } else if (*level_or_code).parse::<Level>().is_ok() {
1054 // Shouldn't conflict with any real diagnostic code
1055 self.error(level_or_code.span(), "no `:` after level found");
1056 return res;
1057 } else if !pattern.trim_start().is_empty() {
1058 self.error(
1059 pattern.span(),
1060 format!("text found after error code `{}`", *level_or_code),
1061 );
1062 return res;
1063 } else {
1064 self.error_matches.push(ErrorMatch {
1065 kind: ErrorMatchKind::Code(Spanned::new(
1066 level_or_code.to_string(),
1067 level_or_code.span(),
1068 )),
1069 line: match_line,
1070 });
1071 };
1072
1073 res
1074 }
1075}
1076
1077impl Pattern {
1078 pub(crate) fn matches(&self, message: &str) -> bool {
1079 match self {
1080 Pattern::SubString(s: &String) => message.contains(s),
1081 Pattern::Regex(r: &Regex) => r.is_match(haystack:message.as_bytes()),
1082 }
1083 }
1084}
1085
1086impl<CommentsType> CommentParser<CommentsType> {
1087 fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> {
1088 if let Some(regex: Spanned<&str>) = pattern.strip_prefix("/") {
1089 match regex.strip_suffix("/") {
1090 Some(regex: Spanned<&str>) => match self.parse_regex(regex) {
1091 Some(r: Spanned) => r.map(Pattern::Regex),
1092 None => pattern.map(|p: &str| Pattern::SubString(p.to_string())),
1093 },
1094 None => {
1095 self.error(
1096 regex.span(),
1097 s:"expected regex pattern due to leading `/`, but found no closing `/`",
1098 );
1099 pattern.map(|p: &str| Pattern::SubString(p.to_string()))
1100 }
1101 }
1102 } else {
1103 pattern.map(|p: &str| Pattern::SubString(p.to_string()))
1104 }
1105 }
1106}
1107