1use std::{
2 collections::HashMap,
3 num::NonZeroUsize,
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8use bstr::{ByteSlice, Utf8Error};
9use regex::bytes::Regex;
10
11use crate::{
12 rustc_stderr::{Level, Span},
13 Error, Errored, Mode,
14};
15
16use color_eyre::eyre::{Context, Result};
17
18pub(crate) use spanned::*;
19
20mod spanned;
21#[cfg(test)]
22mod tests;
23
24/// This crate supports various magic comments that get parsed as file-specific
25/// configuration values. This struct parses them all in one go and then they
26/// get processed by their respective use sites.
27#[derive(Default, Debug)]
28pub(crate) struct Comments {
29 /// List of revision names to execute. Can only be specified once
30 pub revisions: Option<Vec<String>>,
31 /// Comments that are only available under specific revisions.
32 /// The defaults are in key `vec![]`
33 pub revisioned: HashMap<Vec<String>, Revisioned>,
34}
35
36impl Comments {
37 /// Check that a comment isn't specified twice across multiple differently revisioned statements.
38 /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up
39 /// specifying two error patterns that are available in revision `foo`.
40 pub fn find_one_for_revision<'a, T: 'a>(
41 &'a self,
42 revision: &'a str,
43 kind: &str,
44 f: impl Fn(&'a Revisioned) -> OptWithLine<T>,
45 ) -> Result<OptWithLine<T>, Errored> {
46 let mut result = None;
47 let mut errors = vec![];
48 for rev in self.for_revision(revision) {
49 if let Some(found) = f(rev).into_inner() {
50 if result.is_some() {
51 errors.push(found.line());
52 } else {
53 result = found.into();
54 }
55 }
56 }
57 if errors.is_empty() {
58 Ok(result.into())
59 } else {
60 Err(Errored {
61 command: Command::new(format!("<finding flags for revision `{revision}`>")),
62 errors: vec![Error::MultipleRevisionsWithResults {
63 kind: kind.to_string(),
64 lines: errors,
65 }],
66 stderr: vec![],
67 stdout: vec![],
68 })
69 }
70 }
71
72 /// Returns an iterator over all revisioned comments that match the revision.
73 pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> {
74 self.revisioned.iter().filter_map(move |(k, v)| {
75 if k.is_empty() || k.iter().any(|rev| rev == revision) {
76 Some(v)
77 } else {
78 None
79 }
80 })
81 }
82
83 pub(crate) fn edition(
84 &self,
85 revision: &str,
86 config: &crate::Config,
87 ) -> Result<Option<MaybeSpanned<String>>, Errored> {
88 let edition =
89 self.find_one_for_revision(revision, "`edition` annotations", |r| r.edition.clone())?;
90 let edition = edition
91 .into_inner()
92 .map(MaybeSpanned::from)
93 .or(config.edition.clone().map(MaybeSpanned::new_config));
94 Ok(edition)
95 }
96}
97
98#[derive(Debug)]
99/// Comments that can be filtered for specific revisions.
100pub(crate) struct Revisioned {
101 /// The character range in which this revisioned item was first added.
102 /// Used for reporting errors on unknown revisions.
103 pub span: Span,
104 /// Don't run this test if any of these filters apply
105 pub ignore: Vec<Condition>,
106 /// Only run this test if all of these filters apply
107 pub only: Vec<Condition>,
108 /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar
109 pub stderr_per_bitwidth: bool,
110 /// Additional flags to pass to the executable
111 pub compile_flags: Vec<String>,
112 /// Additional env vars to set for the executable
113 pub env_vars: Vec<(String, String)>,
114 /// Normalizations to apply to the stderr output before emitting it to disk
115 pub normalize_stderr: Vec<(Regex, Vec<u8>)>,
116 /// Normalizations to apply to the stdout output before emitting it to disk
117 pub normalize_stdout: Vec<(Regex, Vec<u8>)>,
118 /// Arbitrary patterns to look for in the stderr.
119 /// The error must be from another file, as errors from the current file must be
120 /// checked via `error_matches`.
121 pub error_in_other_files: Vec<Spanned<Pattern>>,
122 pub error_matches: Vec<ErrorMatch>,
123 /// Ignore diagnostics below this level.
124 /// `None` means pick the lowest level from the `error_pattern`s.
125 pub require_annotations_for_level: OptWithLine<Level>,
126 pub aux_builds: Vec<Spanned<PathBuf>>,
127 pub edition: OptWithLine<String>,
128 /// Overwrites the mode from `Config`.
129 pub mode: OptWithLine<Mode>,
130 pub needs_asm_support: bool,
131 /// Don't run [`rustfix`] for this test
132 pub no_rustfix: OptWithLine<()>,
133}
134
135#[derive(Debug)]
136struct CommentParser<T> {
137 /// The comments being built.
138 comments: T,
139 /// Any errors that ocurred during comment parsing.
140 errors: Vec<Error>,
141 /// The available commands and their parsing logic
142 commands: HashMap<&'static str, CommandParserFunc>,
143}
144
145type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span);
146
147impl<T> std::ops::Deref for CommentParser<T> {
148 type Target = T;
149
150 fn deref(&self) -> &Self::Target {
151 &self.comments
152 }
153}
154
155impl<T> std::ops::DerefMut for CommentParser<T> {
156 fn deref_mut(&mut self) -> &mut Self::Target {
157 &mut self.comments
158 }
159}
160
161/// The conditions used for "ignore" and "only" filters.
162#[derive(Debug)]
163pub(crate) enum Condition {
164 /// The given string must appear in the host triple.
165 Host(String),
166 /// The given string must appear in the target triple.
167 Target(String),
168 /// Tests that the bitwidth is the given one.
169 Bitwidth(u8),
170 /// Tests that the target is the host.
171 OnHost,
172}
173
174#[derive(Debug, Clone)]
175/// An error pattern parsed from a `//~` comment.
176pub enum Pattern {
177 SubString(String),
178 Regex(Regex),
179}
180
181#[derive(Debug)]
182pub(crate) struct ErrorMatch {
183 pub pattern: Spanned<Pattern>,
184 pub level: Level,
185 /// The line this pattern is expecting to find a message in.
186 pub line: NonZeroUsize,
187}
188
189impl Condition {
190 fn parse(c: &str) -> std::result::Result<Self, String> {
191 if c == "on-host" {
192 Ok(Condition::OnHost)
193 } else if let Some(bits: &str) = c.strip_suffix("bit") {
194 let bits: u8 = bits.parse().map_err(|_err: ParseIntError| {
195 format!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith")
196 })?;
197 Ok(Condition::Bitwidth(bits))
198 } else if let Some(triple_substr: &str) = c.strip_prefix("target-") {
199 Ok(Condition::Target(triple_substr.to_owned()))
200 } else if let Some(triple_substr: &str) = c.strip_prefix("host-") {
201 Ok(Condition::Host(triple_substr.to_owned()))
202 } else {
203 Err(format!(
204 "`{c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/"
205 ))
206 }
207 }
208}
209
210impl Comments {
211 pub(crate) fn parse_file(path: &Path) -> Result<std::result::Result<Self, Vec<Error>>> {
212 let content =
213 std::fs::read(path).wrap_err_with(|| format!("failed to read {}", path.display()))?;
214 Ok(Self::parse(&content))
215 }
216
217 /// Parse comments in `content`.
218 /// `path` is only used to emit diagnostics if parsing fails.
219 pub(crate) fn parse(
220 content: &(impl AsRef<[u8]> + ?Sized),
221 ) -> std::result::Result<Self, Vec<Error>> {
222 let mut parser = CommentParser {
223 comments: Comments::default(),
224 errors: vec![],
225 commands: CommentParser::<_>::commands(),
226 };
227
228 let mut fallthrough_to = None; // The line that a `|` will refer to.
229 for (l, line) in content.as_ref().lines().enumerate() {
230 let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1
231 let span = Span {
232 line_start: l,
233 line_end: l,
234 column_start: NonZeroUsize::new(1).unwrap(),
235 column_end: NonZeroUsize::new(line.chars().count() + 1).unwrap(),
236 };
237 match parser.parse_checked_line(&mut fallthrough_to, Spanned::new(line, span)) {
238 Ok(()) => {}
239 Err(e) => parser.error(span, format!("Comment is not utf8: {e:?}")),
240 }
241 }
242 if let Some(revisions) = &parser.comments.revisions {
243 for (key, revisioned) in &parser.comments.revisioned {
244 for rev in key {
245 if !revisions.contains(rev) {
246 parser.errors.push(Error::InvalidComment {
247 msg: format!("the revision `{rev}` is not known"),
248 span: revisioned.span,
249 })
250 }
251 }
252 }
253 } else {
254 for (key, revisioned) in &parser.comments.revisioned {
255 if !key.is_empty() {
256 parser.errors.push(Error::InvalidComment {
257 msg: "there are no revisions in this test".into(),
258 span: revisioned.span,
259 })
260 }
261 }
262 }
263 if parser.errors.is_empty() {
264 Ok(parser.comments)
265 } else {
266 Err(parser.errors)
267 }
268 }
269}
270
271impl CommentParser<Comments> {
272 fn parse_checked_line(
273 &mut self,
274 fallthrough_to: &mut Option<NonZeroUsize>,
275 line: Spanned<&[u8]>,
276 ) -> std::result::Result<(), Utf8Error> {
277 if let Some(command) = line.strip_prefix(b"//@") {
278 self.parse_command(command.to_str()?.trim())
279 } else if let Some((_, pattern)) = line.split_once_str("//~") {
280 let (revisions, pattern) = self.parse_revisions(pattern.to_str()?);
281 self.revisioned(revisions, |this| {
282 this.parse_pattern(pattern, fallthrough_to)
283 })
284 } else {
285 *fallthrough_to = None;
286 for pos in line.find_iter("//") {
287 let (_, rest) = line.to_str()?.split_at(pos + 2);
288 for rest in std::iter::once(rest).chain(rest.strip_prefix(" ")) {
289 if let Some('@' | '~' | '[' | ']' | '^' | '|') = rest.chars().next() {
290 self.error(
291 rest.span(),
292 format!(
293 "comment looks suspiciously like a test suite command: `{}`\n\
294 All `//@` test suite commands must be at the start of the line.\n\
295 The `//` must be directly followed by `@` or `~`.",
296 *rest,
297 ),
298 );
299 } else {
300 let mut parser = Self {
301 errors: vec![],
302 comments: Comments::default(),
303 commands: std::mem::take(&mut self.commands),
304 };
305 parser.parse_command(rest);
306 if parser.errors.is_empty() {
307 self.error(
308 rest.span(),
309 "a compiletest-rs style comment was detected.\n\
310 Please use text that could not also be interpreted as a command,\n\
311 and prefix all actual commands with `//@`",
312 );
313 }
314 self.commands = parser.commands;
315 }
316 }
317 }
318 }
319 Ok(())
320 }
321}
322
323impl<CommentsType> CommentParser<CommentsType> {
324 fn error(&mut self, span: Span, s: impl Into<String>) {
325 self.errors.push(Error::InvalidComment {
326 msg: s.into(),
327 span,
328 });
329 }
330
331 fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) {
332 if !cond {
333 self.error(span, s);
334 }
335 }
336
337 fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> {
338 self.check(span, cond:opt.is_some(), s);
339 opt
340 }
341}
342
343impl CommentParser<Comments> {
344 fn parse_command(&mut self, command: Spanned<&str>) {
345 let (revisions, command) = self.parse_revisions(command);
346
347 // Commands are letters or dashes, grab everything until the first character that is neither of those.
348 let (command, args) = match command
349 .char_indices()
350 .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i))
351 {
352 None => (command, Spanned::new("", command.span().shrink_to_end())),
353 Some(i) => {
354 let (command, args) = command.split_at(i);
355 // Commands are separated from their arguments by ':' or ' '
356 let next = args
357 .chars()
358 .next()
359 .expect("the `position` above guarantees that there is at least one char");
360 self.check(
361 args.span().shrink_to_start(),
362 next == ':',
363 "test command must be followed by `:` (or end the line)",
364 );
365 (command, args.split_at(next.len_utf8()).1.trim())
366 }
367 };
368
369 if *command == "revisions" {
370 self.check(
371 revisions.span(),
372 revisions.is_empty(),
373 "revisions cannot be declared under a revision",
374 );
375 self.check(
376 revisions.span(),
377 self.revisions.is_none(),
378 "cannot specify `revisions` twice",
379 );
380 self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect());
381 return;
382 }
383 self.revisioned(revisions, |this| this.parse_command(command, args));
384 }
385
386 fn revisioned(
387 &mut self,
388 revisions: Spanned<Vec<String>>,
389 f: impl FnOnce(&mut CommentParser<&mut Revisioned>),
390 ) {
391 let span = revisions.span();
392 let revisions = revisions.into_inner();
393 let mut this = CommentParser {
394 errors: std::mem::take(&mut self.errors),
395 commands: std::mem::take(&mut self.commands),
396 comments: self
397 .revisioned
398 .entry(revisions)
399 .or_insert_with(|| Revisioned {
400 span,
401 ignore: Default::default(),
402 only: Default::default(),
403 stderr_per_bitwidth: Default::default(),
404 compile_flags: Default::default(),
405 env_vars: Default::default(),
406 normalize_stderr: Default::default(),
407 normalize_stdout: Default::default(),
408 error_in_other_files: Default::default(),
409 error_matches: Default::default(),
410 require_annotations_for_level: Default::default(),
411 aux_builds: Default::default(),
412 edition: Default::default(),
413 mode: Default::default(),
414 needs_asm_support: Default::default(),
415 no_rustfix: Default::default(),
416 }),
417 };
418 f(&mut this);
419 let CommentParser {
420 errors, commands, ..
421 } = this;
422 self.commands = commands;
423 self.errors = errors;
424 }
425}
426
427impl CommentParser<&mut Revisioned> {
428 fn parse_normalize_test(
429 &mut self,
430 args: Spanned<&str>,
431 mode: &str,
432 ) -> Option<(Regex, Vec<u8>)> {
433 let (from, rest) = self.parse_str(args);
434
435 let to = match rest.strip_prefix("->") {
436 Some(v) => v,
437 None => {
438 self.error(
439 rest.span(),
440 format!(
441 "normalize-{mode}-test needs a pattern and replacement separated by `->`"
442 ),
443 );
444 return None;
445 }
446 }
447 .trim_start();
448 let (to, rest) = self.parse_str(to);
449
450 self.check(
451 rest.span(),
452 rest.is_empty(),
453 "trailing text after pattern replacement",
454 );
455
456 let regex = self.parse_regex(from)?;
457 Some((regex, to.as_bytes().to_owned()))
458 }
459
460 fn commands() -> HashMap<&'static str, CommandParserFunc> {
461 let mut commands = HashMap::<_, CommandParserFunc>::new();
462 macro_rules! commands {
463 ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => {
464 $(commands.insert($name, |$this, $args, $span| {
465 $block
466 });)*
467 };
468 }
469 commands! {
470 "compile-flags" => (this, args, _span){
471 if let Some(parsed) = comma::parse_command(*args) {
472 this.compile_flags.extend(parsed);
473 } else {
474 this.error(args.span(), format!("`{}` contains an unclosed quotation mark", *args));
475 }
476 }
477 "rustc-env" => (this, args, _span){
478 for env in args.split_whitespace() {
479 if let Some((k, v)) = this.check_some(
480 args.span(),
481 env.split_once('='),
482 "environment variables must be key/value pairs separated by a `=`",
483 ) {
484 this.env_vars.push((k.to_string(), v.to_string()));
485 }
486 }
487 }
488 "normalize-stderr-test" => (this, args, _span){
489 if let Some(res) = this.parse_normalize_test(args, "stderr") {
490 this.normalize_stderr.push(res)
491 }
492 }
493 "normalize-stdout-test" => (this, args, _span){
494 if let Some(res) = this.parse_normalize_test(args, "stdout") {
495 this.normalize_stdout.push(res)
496 }
497 }
498 "error-pattern" => (this, _args, span){
499 this.error(span, "`error-pattern` has been renamed to `error-in-other-file`");
500 }
501 "error-in-other-file" => (this, args, _span){
502 let args = args.trim();
503 let pat = this.parse_error_pattern(args);
504 this.error_in_other_files.push(pat);
505 }
506 "stderr-per-bitwidth" => (this, _args, span){
507 // args are ignored (can be used as comment)
508 this.check(
509 span,
510 !this.stderr_per_bitwidth,
511 "cannot specify `stderr-per-bitwidth` twice",
512 );
513 this.stderr_per_bitwidth = true;
514 }
515 "run-rustfix" => (this, _args, span){
516 this.error(span, "rustfix is now ran by default when applicable suggestions are found");
517 }
518 "no-rustfix" => (this, _args, span){
519 // args are ignored (can be used as comment)
520 let prev = this.no_rustfix.set((), span);
521 this.check(
522 span,
523 prev.is_none(),
524 "cannot specify `no-rustfix` twice",
525 );
526 }
527 "needs-asm-support" => (this, _args, span){
528 // args are ignored (can be used as comment)
529 this.check(
530 span,
531 !this.needs_asm_support,
532 "cannot specify `needs-asm-support` twice",
533 );
534 this.needs_asm_support = true;
535 }
536 "aux-build" => (this, args, _span){
537 let name = match args.split_once(":") {
538 Some((name, rest)) => {
539 this.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name");
540 name
541 },
542 None => args,
543 };
544 this.aux_builds.push(name.map(Into::into));
545 }
546 "edition" => (this, args, span){
547 let prev = this.edition.set((*args).into(), args.span());
548 this.check(span, prev.is_none(), "cannot specify `edition` twice");
549 }
550 "check-pass" => (this, _args, span){
551 let prev = this.mode.set(Mode::Pass, span);
552 // args are ignored (can be used as comment)
553 this.check(
554 span,
555 prev.is_none(),
556 "cannot specify test mode changes twice",
557 );
558 }
559 "run" => (this, args, span){
560 this.check(
561 span,
562 this.mode.is_none(),
563 "cannot specify test mode changes twice",
564 );
565 let mut set = |exit_code| this.mode.set(Mode::Run { exit_code }, args.span());
566 if args.is_empty() {
567 set(0);
568 } else {
569 match args.parse() {
570 Ok(exit_code) => {set(exit_code);},
571 Err(err) => this.error(args.span(), err.to_string()),
572 }
573 }
574 }
575 "require-annotations-for-level" => (this, args, span){
576 let args = args.trim();
577 let prev = match args.parse() {
578 Ok(it) => this.require_annotations_for_level.set(it, args.span()),
579 Err(msg) => {
580 this.error(args.span(), msg);
581 None
582 },
583 };
584
585 this.check(
586 span,
587 prev.is_none(),
588 "cannot specify `require-annotations-for-level` twice",
589 );
590 }
591 }
592 commands
593 }
594
595 fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) {
596 if let Some(command_handler) = self.commands.get(*command) {
597 command_handler(self, args, command.span());
598 } else if let Some(s) = command.strip_prefix("ignore-") {
599 // args are ignored (can be used as comment)
600 match Condition::parse(*s) {
601 Ok(cond) => self.ignore.push(cond),
602 Err(msg) => self.error(s.span(), msg),
603 }
604 } else if let Some(s) = command.strip_prefix("only-") {
605 // args are ignored (can be used as comment)
606 match Condition::parse(*s) {
607 Ok(cond) => self.only.push(cond),
608 Err(msg) => self.error(s.span(), msg),
609 }
610 } else {
611 let best_match = self
612 .commands
613 .keys()
614 .min_by_key(|key| levenshtein::levenshtein(key, *command))
615 .unwrap();
616 self.error(
617 command.span(),
618 format!(
619 "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?",
620 *command
621 ),
622 );
623 }
624 }
625}
626
627impl<CommentsType> CommentParser<CommentsType> {
628 fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Regex> {
629 match Regex::new(*regex) {
630 Ok(regex) => Some(regex),
631 Err(err) => {
632 self.error(regex.span(), format!("invalid regex: {err:?}"));
633 None
634 }
635 }
636 }
637
638 /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is
639 /// returned in the first component. `\` can be used to escape arbitrary character.
640 /// Second return component is the rest of the string with leading whitespace removed.
641 fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) {
642 match s.strip_prefix("\"") {
643 Some(s) => {
644 let mut escaped = false;
645 for (i, c) in s.char_indices() {
646 if escaped {
647 // Accept any character as literal after a `\`.
648 escaped = false;
649 } else if c == '"' {
650 let (a, b) = s.split_at(i);
651 let b = b.split_at(1).1;
652 return (a, b.trim_start());
653 } else {
654 escaped = c == '\\';
655 }
656 }
657 self.error(s.span(), format!("no closing quotes found for {}", *s));
658 (s, Spanned::new("", s.span()))
659 }
660 None => {
661 if s.is_empty() {
662 self.error(s.span(), "expected quoted string, but found end of line")
663 } else {
664 self.error(
665 s.span(),
666 format!("expected `\"`, got `{}`", s.chars().next().unwrap()),
667 )
668 }
669 (s, Spanned::new("", s.span()))
670 }
671 }
672 }
673
674 // parse something like \[[a-z]+(,[a-z]+)*\]
675 fn parse_revisions<'a>(
676 &mut self,
677 pattern: Spanned<&'a str>,
678 ) -> (Spanned<Vec<String>>, Spanned<&'a str>) {
679 match pattern.strip_prefix("[") {
680 Some(s) => {
681 // revisions
682 let end = s.char_indices().find_map(|(i, c)| match c {
683 ']' => Some(i),
684 _ => None,
685 });
686 let Some(end) = end else {
687 self.error(s.span(), "`[` without corresponding `]`");
688 return (
689 Spanned::new(vec![], pattern.span().shrink_to_start()),
690 pattern,
691 );
692 };
693 let (revision, pattern) = s.split_at(end);
694 let revisions = revision.split(',').map(|s| s.trim().to_string()).collect();
695 (
696 Spanned::new(revisions, revision.span()),
697 // 1.. because `split_at` includes the separator
698 pattern.split_at(1).1.trim_start(),
699 )
700 }
701 _ => (
702 Spanned::new(vec![], pattern.span().shrink_to_start()),
703 pattern,
704 ),
705 }
706 }
707}
708
709impl CommentParser<&mut Revisioned> {
710 // parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*)
711 fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) {
712 let (match_line, pattern) = match pattern.chars().next() {
713 Some('|') => (
714 match fallthrough_to {
715 Some(fallthrough) => *fallthrough,
716 None => {
717 self.error(pattern.span(), "`//~|` pattern without preceding line");
718 return;
719 }
720 },
721 pattern.split_at(1).1,
722 ),
723 Some('^') => {
724 let offset = pattern.chars().take_while(|&c| c == '^').count();
725 match pattern
726 .span()
727 .line_start
728 .get()
729 .checked_sub(offset)
730 .and_then(NonZeroUsize::new)
731 {
732 // lines are one-indexed, so a target line of 0 is invalid, but also
733 // prevented via `NonZeroUsize`
734 Some(match_line) => (match_line, pattern.split_at(offset).1),
735 _ => {
736 self.error(pattern.span(), format!(
737 "//~^ pattern is trying to refer to {} lines above, but there are only {} lines above",
738 offset,
739 pattern.line().get() - 1,
740 ));
741 return;
742 }
743 }
744 }
745 Some(_) => (pattern.span().line_start, pattern),
746 None => {
747 self.error(pattern.span(), "no pattern specified");
748 return;
749 }
750 };
751
752 let pattern = pattern.trim_start();
753 let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) {
754 Some(offset) => offset,
755 None => {
756 self.error(pattern.span(), "pattern without level");
757 return;
758 }
759 };
760
761 let (level, pattern) = pattern.split_at(offset);
762 let level = match (*level).parse() {
763 Ok(level) => level,
764 Err(msg) => {
765 self.error(level.span(), msg);
766 return;
767 }
768 };
769 let pattern = match pattern.strip_prefix(":") {
770 Some(offset) => offset,
771 None => {
772 self.error(pattern.span(), "no `:` after level found");
773 return;
774 }
775 };
776
777 let pattern = pattern.trim();
778
779 self.check(pattern.span(), !pattern.is_empty(), "no pattern specified");
780
781 let pattern = self.parse_error_pattern(pattern);
782
783 *fallthrough_to = Some(match_line);
784
785 self.error_matches.push(ErrorMatch {
786 pattern,
787 level,
788 line: match_line,
789 });
790 }
791}
792
793impl Pattern {
794 pub(crate) fn matches(&self, message: &str) -> bool {
795 match self {
796 Pattern::SubString(s: &String) => message.contains(s),
797 Pattern::Regex(r: &Regex) => r.is_match(text:message.as_bytes()),
798 }
799 }
800}
801
802impl<CommentsType> CommentParser<CommentsType> {
803 fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> {
804 if let Some(regex: Spanned<&str>) = pattern.strip_prefix("/") {
805 match regex.strip_suffix("/") {
806 Some(regex: Spanned<&str>) => match self.parse_regex(regex) {
807 Some(r: Regex) => Spanned::new(data:Pattern::Regex(r), regex.span()),
808 None => Spanned::new(data:Pattern::SubString(pattern.to_string()), regex.span()),
809 },
810 None => {
811 self.error(
812 regex.span(),
813 s:"expected regex pattern due to leading `/`, but found no closing `/`",
814 );
815 Spanned::new(data:Pattern::SubString(pattern.to_string()), regex.span())
816 }
817 }
818 } else {
819 Spanned::new(data:Pattern::SubString(pattern.to_string()), pattern.span())
820 }
821 }
822}
823