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#[derive(Debug, Clone)]
21pub struct Comments {
22 pub revisions: Option<Vec<String>>,
24 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 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 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 pub fn base(&mut self) -> &mut Revisioned {
93 self.revisioned.get_mut(&[][..]).unwrap()
94 }
95
96 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)]
117pub struct Revisioned {
119 pub span: Span,
122 pub ignore: Vec<Condition>,
124 pub only: Vec<Condition>,
126 pub stderr_per_bitwidth: bool,
128 pub compile_flags: Vec<String>,
130 pub env_vars: Vec<(String, String)>,
132 pub normalize_stderr: Vec<(Match, Vec<u8>)>,
134 pub normalize_stdout: Vec<(Match, Vec<u8>)>,
136 pub(crate) error_in_other_files: Vec<Spanned<Pattern>>,
140 pub(crate) error_matches: Vec<ErrorMatch>,
141 pub require_annotations_for_level: OptWithLine<Level>,
144 pub exit_status: OptWithLine<i32>,
147 pub require_annotations: OptWithLine<bool>,
151 pub diagnostic_code_prefix: OptWithLine<String>,
154 pub custom: BTreeMap<&'static str, Spanned<Vec<Box<dyn Flag>>>>,
162}
163
164impl Revisioned {
165 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 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 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#[derive(Debug)]
192pub struct CommentParser<T> {
193 comments: T,
195 errors: Vec<Error>,
197 commands: HashMap<&'static str, CommandParserFunc>,
199 comment_start: &'static str,
201}
202
203pub 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#[derive(Debug, Clone)]
223pub enum Condition {
224 Host(Vec<TargetSubStr>),
226 Target(Vec<TargetSubStr>),
228 Bitwidth(Vec<u8>),
230 OnHost,
232}
233
234#[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#[derive(Debug, Clone)]
273pub enum Pattern {
274 SubString(String),
276 Regex(Regex),
278}
279
280#[derive(Debug, Clone)]
281pub(crate) enum ErrorMatchKind {
282 Pattern {
284 pattern: Spanned<Pattern>,
285 level: Level,
286 },
287 Code(Spanned<String>),
289}
290
291#[derive(Debug, Clone)]
292pub(crate) struct ErrorMatch {
293 pub(crate) kind: ErrorMatchKind,
294 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 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 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; 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(); 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}