diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-07 05:48:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-07 05:48:48 +0000 |
commit | ef24de24a82fe681581cc130f342363c47c0969a (patch) | |
tree | 0d494f7e1a38b95c92426f58fe6eaa877303a86c /vendor/ui_test-0.20.0/src/parser.rs | |
parent | Releasing progress-linux version 1.74.1+dfsg1-1~progress7.99u1. (diff) | |
download | rustc-ef24de24a82fe681581cc130f342363c47c0969a.tar.xz rustc-ef24de24a82fe681581cc130f342363c47c0969a.zip |
Merging upstream version 1.75.0+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'vendor/ui_test-0.20.0/src/parser.rs')
-rw-r--r-- | vendor/ui_test-0.20.0/src/parser.rs | 822 |
1 files changed, 0 insertions, 822 deletions
diff --git a/vendor/ui_test-0.20.0/src/parser.rs b/vendor/ui_test-0.20.0/src/parser.rs deleted file mode 100644 index 62ee5a1d1..000000000 --- a/vendor/ui_test-0.20.0/src/parser.rs +++ /dev/null @@ -1,822 +0,0 @@ -use std::{ - collections::HashMap, - num::NonZeroUsize, - path::{Path, PathBuf}, - process::Command, -}; - -use bstr::{ByteSlice, Utf8Error}; -use regex::bytes::Regex; - -use crate::{ - rustc_stderr::{Level, Span}, - Error, Errored, Mode, -}; - -use color_eyre::eyre::{Context, Result}; - -pub(crate) use spanned::*; - -mod spanned; -#[cfg(test)] -mod tests; - -/// This crate supports various magic comments that get parsed as file-specific -/// configuration values. This struct parses them all in one go and then they -/// get processed by their respective use sites. -#[derive(Default, Debug)] -pub(crate) struct Comments { - /// List of revision names to execute. Can only be specified once - pub revisions: Option<Vec<String>>, - /// Comments that are only available under specific revisions. - /// The defaults are in key `vec![]` - pub revisioned: HashMap<Vec<String>, Revisioned>, -} - -impl Comments { - /// Check that a comment isn't specified twice across multiple differently revisioned statements. - /// e.g. `//@[foo, bar] error-in-other-file: bop` and `//@[foo, baz] error-in-other-file boop` would end up - /// specifying two error patterns that are available in revision `foo`. - pub fn find_one_for_revision<'a, T: 'a>( - &'a self, - revision: &'a str, - kind: &str, - f: impl Fn(&'a Revisioned) -> OptWithLine<T>, - ) -> Result<OptWithLine<T>, Errored> { - let mut result = None; - let mut errors = vec![]; - for rev in self.for_revision(revision) { - if let Some(found) = f(rev).into_inner() { - if result.is_some() { - errors.push(found.line()); - } else { - result = found.into(); - } - } - } - if errors.is_empty() { - Ok(result.into()) - } else { - Err(Errored { - command: Command::new(format!("<finding flags for revision `{revision}`>")), - errors: vec![Error::MultipleRevisionsWithResults { - kind: kind.to_string(), - lines: errors, - }], - stderr: vec![], - stdout: vec![], - }) - } - } - - /// Returns an iterator over all revisioned comments that match the revision. - pub fn for_revision<'a>(&'a self, revision: &'a str) -> impl Iterator<Item = &'a Revisioned> { - self.revisioned.iter().filter_map(move |(k, v)| { - if k.is_empty() || k.iter().any(|rev| rev == revision) { - Some(v) - } else { - None - } - }) - } - - pub(crate) fn edition( - &self, - revision: &str, - config: &crate::Config, - ) -> Result<Option<MaybeSpanned<String>>, Errored> { - let edition = - self.find_one_for_revision(revision, "`edition` annotations", |r| r.edition.clone())?; - let edition = edition - .into_inner() - .map(MaybeSpanned::from) - .or(config.edition.clone().map(MaybeSpanned::new_config)); - Ok(edition) - } -} - -#[derive(Debug)] -/// Comments that can be filtered for specific revisions. -pub(crate) struct Revisioned { - /// The character range in which this revisioned item was first added. - /// Used for reporting errors on unknown revisions. - pub span: Span, - /// Don't run this test if any of these filters apply - pub ignore: Vec<Condition>, - /// Only run this test if all of these filters apply - pub only: Vec<Condition>, - /// Generate one .stderr file per bit width, by prepending with `.64bit` and similar - pub stderr_per_bitwidth: bool, - /// Additional flags to pass to the executable - pub compile_flags: Vec<String>, - /// Additional env vars to set for the executable - pub env_vars: Vec<(String, String)>, - /// Normalizations to apply to the stderr output before emitting it to disk - pub normalize_stderr: Vec<(Regex, Vec<u8>)>, - /// Normalizations to apply to the stdout output before emitting it to disk - pub normalize_stdout: Vec<(Regex, Vec<u8>)>, - /// Arbitrary patterns to look for in the stderr. - /// The error must be from another file, as errors from the current file must be - /// checked via `error_matches`. - pub error_in_other_files: Vec<Spanned<Pattern>>, - pub error_matches: Vec<ErrorMatch>, - /// Ignore diagnostics below this level. - /// `None` means pick the lowest level from the `error_pattern`s. - pub require_annotations_for_level: OptWithLine<Level>, - pub aux_builds: Vec<Spanned<PathBuf>>, - pub edition: OptWithLine<String>, - /// Overwrites the mode from `Config`. - pub mode: OptWithLine<Mode>, - pub needs_asm_support: bool, - /// Don't run [`rustfix`] for this test - pub no_rustfix: OptWithLine<()>, -} - -#[derive(Debug)] -struct CommentParser<T> { - /// The comments being built. - comments: T, - /// Any errors that ocurred during comment parsing. - errors: Vec<Error>, - /// The available commands and their parsing logic - commands: HashMap<&'static str, CommandParserFunc>, -} - -type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: Spanned<&str>, span: Span); - -impl<T> std::ops::Deref for CommentParser<T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.comments - } -} - -impl<T> std::ops::DerefMut for CommentParser<T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.comments - } -} - -/// The conditions used for "ignore" and "only" filters. -#[derive(Debug)] -pub(crate) enum Condition { - /// The given string must appear in the host triple. - Host(String), - /// The given string must appear in the target triple. - Target(String), - /// Tests that the bitwidth is the given one. - Bitwidth(u8), - /// Tests that the target is the host. - OnHost, -} - -#[derive(Debug, Clone)] -/// An error pattern parsed from a `//~` comment. -pub enum Pattern { - SubString(String), - Regex(Regex), -} - -#[derive(Debug)] -pub(crate) struct ErrorMatch { - pub pattern: Spanned<Pattern>, - pub level: Level, - /// The line this pattern is expecting to find a message in. - pub line: NonZeroUsize, -} - -impl Condition { - fn parse(c: &str) -> std::result::Result<Self, String> { - if c == "on-host" { - Ok(Condition::OnHost) - } else if let Some(bits) = c.strip_suffix("bit") { - let bits: u8 = bits.parse().map_err(|_err| { - format!("invalid ignore/only filter ending in 'bit': {c:?} is not a valid bitwdith") - })?; - Ok(Condition::Bitwidth(bits)) - } else if let Some(triple_substr) = c.strip_prefix("target-") { - Ok(Condition::Target(triple_substr.to_owned())) - } else if let Some(triple_substr) = c.strip_prefix("host-") { - Ok(Condition::Host(triple_substr.to_owned())) - } else { - Err(format!( - "`{c}` is not a valid condition, expected `on-host`, /[0-9]+bit/, /host-.*/, or /target-.*/" - )) - } - } -} - -impl Comments { - pub(crate) fn parse_file(path: &Path) -> Result<std::result::Result<Self, Vec<Error>>> { - let content = - std::fs::read(path).wrap_err_with(|| format!("failed to read {}", path.display()))?; - Ok(Self::parse(&content)) - } - - /// Parse comments in `content`. - /// `path` is only used to emit diagnostics if parsing fails. - pub(crate) fn parse( - content: &(impl AsRef<[u8]> + ?Sized), - ) -> std::result::Result<Self, Vec<Error>> { - let mut parser = CommentParser { - comments: Comments::default(), - errors: vec![], - commands: CommentParser::<_>::commands(), - }; - - let mut fallthrough_to = None; // The line that a `|` will refer to. - for (l, line) in content.as_ref().lines().enumerate() { - let l = NonZeroUsize::new(l + 1).unwrap(); // enumerate starts at 0, but line numbers start at 1 - let span = Span { - line_start: l, - line_end: l, - column_start: NonZeroUsize::new(1).unwrap(), - column_end: NonZeroUsize::new(line.chars().count() + 1).unwrap(), - }; - match parser.parse_checked_line(&mut fallthrough_to, Spanned::new(line, span)) { - Ok(()) => {} - Err(e) => parser.error(span, format!("Comment is not utf8: {e:?}")), - } - } - if let Some(revisions) = &parser.comments.revisions { - for (key, revisioned) in &parser.comments.revisioned { - for rev in key { - if !revisions.contains(rev) { - parser.errors.push(Error::InvalidComment { - msg: format!("the revision `{rev}` is not known"), - span: revisioned.span, - }) - } - } - } - } else { - for (key, revisioned) in &parser.comments.revisioned { - if !key.is_empty() { - parser.errors.push(Error::InvalidComment { - msg: "there are no revisions in this test".into(), - span: revisioned.span, - }) - } - } - } - if parser.errors.is_empty() { - Ok(parser.comments) - } else { - Err(parser.errors) - } - } -} - -impl CommentParser<Comments> { - fn parse_checked_line( - &mut self, - fallthrough_to: &mut Option<NonZeroUsize>, - line: Spanned<&[u8]>, - ) -> std::result::Result<(), Utf8Error> { - if let Some(command) = line.strip_prefix(b"//@") { - self.parse_command(command.to_str()?.trim()) - } else if let Some((_, pattern)) = line.split_once_str("//~") { - let (revisions, pattern) = self.parse_revisions(pattern.to_str()?); - self.revisioned(revisions, |this| { - this.parse_pattern(pattern, fallthrough_to) - }) - } else { - *fallthrough_to = None; - for pos in line.find_iter("//") { - let (_, rest) = line.to_str()?.split_at(pos + 2); - for rest in std::iter::once(rest).chain(rest.strip_prefix(" ")) { - if let Some('@' | '~' | '[' | ']' | '^' | '|') = rest.chars().next() { - self.error( - rest.span(), - format!( - "comment looks suspiciously like a test suite command: `{}`\n\ - All `//@` test suite commands must be at the start of the line.\n\ - The `//` must be directly followed by `@` or `~`.", - *rest, - ), - ); - } else { - let mut parser = Self { - errors: vec![], - comments: Comments::default(), - commands: std::mem::take(&mut self.commands), - }; - parser.parse_command(rest); - if parser.errors.is_empty() { - self.error( - rest.span(), - "a compiletest-rs style comment was detected.\n\ - Please use text that could not also be interpreted as a command,\n\ - and prefix all actual commands with `//@`", - ); - } - self.commands = parser.commands; - } - } - } - } - Ok(()) - } -} - -impl<CommentsType> CommentParser<CommentsType> { - fn error(&mut self, span: Span, s: impl Into<String>) { - self.errors.push(Error::InvalidComment { - msg: s.into(), - span, - }); - } - - fn check(&mut self, span: Span, cond: bool, s: impl Into<String>) { - if !cond { - self.error(span, s); - } - } - - fn check_some<T>(&mut self, span: Span, opt: Option<T>, s: impl Into<String>) -> Option<T> { - self.check(span, opt.is_some(), s); - opt - } -} - -impl CommentParser<Comments> { - fn parse_command(&mut self, command: Spanned<&str>) { - let (revisions, command) = self.parse_revisions(command); - - // Commands are letters or dashes, grab everything until the first character that is neither of those. - let (command, args) = match command - .char_indices() - .find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i)) - { - None => (command, Spanned::new("", command.span().shrink_to_end())), - Some(i) => { - let (command, args) = command.split_at(i); - // Commands are separated from their arguments by ':' or ' ' - let next = args - .chars() - .next() - .expect("the `position` above guarantees that there is at least one char"); - self.check( - args.span().shrink_to_start(), - next == ':', - "test command must be followed by `:` (or end the line)", - ); - (command, args.split_at(next.len_utf8()).1.trim()) - } - }; - - if *command == "revisions" { - self.check( - revisions.span(), - revisions.is_empty(), - "revisions cannot be declared under a revision", - ); - self.check( - revisions.span(), - self.revisions.is_none(), - "cannot specify `revisions` twice", - ); - self.revisions = Some(args.split_whitespace().map(|s| s.to_string()).collect()); - return; - } - self.revisioned(revisions, |this| this.parse_command(command, args)); - } - - fn revisioned( - &mut self, - revisions: Spanned<Vec<String>>, - f: impl FnOnce(&mut CommentParser<&mut Revisioned>), - ) { - let span = revisions.span(); - let revisions = revisions.into_inner(); - let mut this = CommentParser { - errors: std::mem::take(&mut self.errors), - commands: std::mem::take(&mut self.commands), - comments: self - .revisioned - .entry(revisions) - .or_insert_with(|| Revisioned { - span, - ignore: Default::default(), - only: Default::default(), - stderr_per_bitwidth: Default::default(), - compile_flags: Default::default(), - env_vars: Default::default(), - normalize_stderr: Default::default(), - normalize_stdout: Default::default(), - error_in_other_files: Default::default(), - error_matches: Default::default(), - require_annotations_for_level: Default::default(), - aux_builds: Default::default(), - edition: Default::default(), - mode: Default::default(), - needs_asm_support: Default::default(), - no_rustfix: Default::default(), - }), - }; - f(&mut this); - let CommentParser { - errors, commands, .. - } = this; - self.commands = commands; - self.errors = errors; - } -} - -impl CommentParser<&mut Revisioned> { - fn parse_normalize_test( - &mut self, - args: Spanned<&str>, - mode: &str, - ) -> Option<(Regex, Vec<u8>)> { - let (from, rest) = self.parse_str(args); - - let to = match rest.strip_prefix("->") { - Some(v) => v, - None => { - self.error( - rest.span(), - format!( - "normalize-{mode}-test needs a pattern and replacement separated by `->`" - ), - ); - return None; - } - } - .trim_start(); - let (to, rest) = self.parse_str(to); - - self.check( - rest.span(), - rest.is_empty(), - "trailing text after pattern replacement", - ); - - let regex = self.parse_regex(from)?; - Some((regex, to.as_bytes().to_owned())) - } - - fn commands() -> HashMap<&'static str, CommandParserFunc> { - let mut commands = HashMap::<_, CommandParserFunc>::new(); - macro_rules! commands { - ($($name:expr => ($this:ident, $args:ident, $span:ident)$block:block)*) => { - $(commands.insert($name, |$this, $args, $span| { - $block - });)* - }; - } - commands! { - "compile-flags" => (this, args, _span){ - if let Some(parsed) = comma::parse_command(*args) { - this.compile_flags.extend(parsed); - } else { - this.error(args.span(), format!("`{}` contains an unclosed quotation mark", *args)); - } - } - "rustc-env" => (this, args, _span){ - for env in args.split_whitespace() { - if let Some((k, v)) = this.check_some( - args.span(), - env.split_once('='), - "environment variables must be key/value pairs separated by a `=`", - ) { - this.env_vars.push((k.to_string(), v.to_string())); - } - } - } - "normalize-stderr-test" => (this, args, _span){ - if let Some(res) = this.parse_normalize_test(args, "stderr") { - this.normalize_stderr.push(res) - } - } - "normalize-stdout-test" => (this, args, _span){ - if let Some(res) = this.parse_normalize_test(args, "stdout") { - this.normalize_stdout.push(res) - } - } - "error-pattern" => (this, _args, span){ - this.error(span, "`error-pattern` has been renamed to `error-in-other-file`"); - } - "error-in-other-file" => (this, args, _span){ - let args = args.trim(); - let pat = this.parse_error_pattern(args); - this.error_in_other_files.push(pat); - } - "stderr-per-bitwidth" => (this, _args, span){ - // args are ignored (can be used as comment) - this.check( - span, - !this.stderr_per_bitwidth, - "cannot specify `stderr-per-bitwidth` twice", - ); - this.stderr_per_bitwidth = true; - } - "run-rustfix" => (this, _args, span){ - this.error(span, "rustfix is now ran by default when applicable suggestions are found"); - } - "no-rustfix" => (this, _args, span){ - // args are ignored (can be used as comment) - let prev = this.no_rustfix.set((), span); - this.check( - span, - prev.is_none(), - "cannot specify `no-rustfix` twice", - ); - } - "needs-asm-support" => (this, _args, span){ - // args are ignored (can be used as comment) - this.check( - span, - !this.needs_asm_support, - "cannot specify `needs-asm-support` twice", - ); - this.needs_asm_support = true; - } - "aux-build" => (this, args, _span){ - let name = match args.split_once(":") { - Some((name, rest)) => { - this.error(rest.span(), "proc macros are now auto-detected, you can remove the `:proc-macro` after the file name"); - name - }, - None => args, - }; - this.aux_builds.push(name.map(Into::into)); - } - "edition" => (this, args, span){ - let prev = this.edition.set((*args).into(), args.span()); - this.check(span, prev.is_none(), "cannot specify `edition` twice"); - } - "check-pass" => (this, _args, span){ - let prev = this.mode.set(Mode::Pass, span); - // args are ignored (can be used as comment) - this.check( - span, - prev.is_none(), - "cannot specify test mode changes twice", - ); - } - "run" => (this, args, span){ - this.check( - span, - this.mode.is_none(), - "cannot specify test mode changes twice", - ); - let mut set = |exit_code| this.mode.set(Mode::Run { exit_code }, args.span()); - if args.is_empty() { - set(0); - } else { - match args.parse() { - Ok(exit_code) => {set(exit_code);}, - Err(err) => this.error(args.span(), err.to_string()), - } - } - } - "require-annotations-for-level" => (this, args, span){ - let args = args.trim(); - let prev = match args.parse() { - Ok(it) => this.require_annotations_for_level.set(it, args.span()), - Err(msg) => { - this.error(args.span(), msg); - None - }, - }; - - this.check( - span, - prev.is_none(), - "cannot specify `require-annotations-for-level` twice", - ); - } - } - commands - } - - fn parse_command(&mut self, command: Spanned<&str>, args: Spanned<&str>) { - if let Some(command_handler) = self.commands.get(*command) { - command_handler(self, args, command.span()); - } else if let Some(s) = command.strip_prefix("ignore-") { - // args are ignored (can be used as comment) - match Condition::parse(*s) { - Ok(cond) => self.ignore.push(cond), - Err(msg) => self.error(s.span(), msg), - } - } else if let Some(s) = command.strip_prefix("only-") { - // args are ignored (can be used as comment) - match Condition::parse(*s) { - Ok(cond) => self.only.push(cond), - Err(msg) => self.error(s.span(), msg), - } - } else { - let best_match = self - .commands - .keys() - .min_by_key(|key| levenshtein::levenshtein(key, *command)) - .unwrap(); - self.error( - command.span(), - format!( - "`{}` is not a command known to `ui_test`, did you mean `{best_match}`?", - *command - ), - ); - } - } -} - -impl<CommentsType> CommentParser<CommentsType> { - fn parse_regex(&mut self, regex: Spanned<&str>) -> Option<Regex> { - match Regex::new(*regex) { - Ok(regex) => Some(regex), - Err(err) => { - self.error(regex.span(), format!("invalid regex: {err:?}")); - None - } - } - } - - /// Parses a string literal. `s` has to start with `"`; everything until the next `"` is - /// returned in the first component. `\` can be used to escape arbitrary character. - /// Second return component is the rest of the string with leading whitespace removed. - fn parse_str<'a>(&mut self, s: Spanned<&'a str>) -> (Spanned<&'a str>, Spanned<&'a str>) { - match s.strip_prefix("\"") { - Some(s) => { - let mut escaped = false; - for (i, c) in s.char_indices() { - if escaped { - // Accept any character as literal after a `\`. - escaped = false; - } else if c == '"' { - let (a, b) = s.split_at(i); - let b = b.split_at(1).1; - return (a, b.trim_start()); - } else { - escaped = c == '\\'; - } - } - self.error(s.span(), format!("no closing quotes found for {}", *s)); - (s, Spanned::new("", s.span())) - } - None => { - if s.is_empty() { - self.error(s.span(), "expected quoted string, but found end of line") - } else { - self.error( - s.span(), - format!("expected `\"`, got `{}`", s.chars().next().unwrap()), - ) - } - (s, Spanned::new("", s.span())) - } - } - } - - // parse something like \[[a-z]+(,[a-z]+)*\] - fn parse_revisions<'a>( - &mut self, - pattern: Spanned<&'a str>, - ) -> (Spanned<Vec<String>>, Spanned<&'a str>) { - match pattern.strip_prefix("[") { - Some(s) => { - // revisions - let end = s.char_indices().find_map(|(i, c)| match c { - ']' => Some(i), - _ => None, - }); - let Some(end) = end else { - self.error(s.span(), "`[` without corresponding `]`"); - return ( - Spanned::new(vec![], pattern.span().shrink_to_start()), - pattern, - ); - }; - let (revision, pattern) = s.split_at(end); - let revisions = revision.split(',').map(|s| s.trim().to_string()).collect(); - ( - Spanned::new(revisions, revision.span()), - // 1.. because `split_at` includes the separator - pattern.split_at(1).1.trim_start(), - ) - } - _ => ( - Spanned::new(vec![], pattern.span().shrink_to_start()), - pattern, - ), - } - } -} - -impl CommentParser<&mut Revisioned> { - // parse something like (\[[a-z]+(,[a-z]+)*\])?(?P<offset>\||[\^]+)? *(?P<level>ERROR|HELP|WARN|NOTE): (?P<text>.*) - fn parse_pattern(&mut self, pattern: Spanned<&str>, fallthrough_to: &mut Option<NonZeroUsize>) { - let (match_line, pattern) = match pattern.chars().next() { - Some('|') => ( - match fallthrough_to { - Some(fallthrough) => *fallthrough, - None => { - self.error(pattern.span(), "`//~|` pattern without preceding line"); - return; - } - }, - pattern.split_at(1).1, - ), - Some('^') => { - let offset = pattern.chars().take_while(|&c| c == '^').count(); - match pattern - .span() - .line_start - .get() - .checked_sub(offset) - .and_then(NonZeroUsize::new) - { - // lines are one-indexed, so a target line of 0 is invalid, but also - // prevented via `NonZeroUsize` - Some(match_line) => (match_line, pattern.split_at(offset).1), - _ => { - self.error(pattern.span(), format!( - "//~^ pattern is trying to refer to {} lines above, but there are only {} lines above", - offset, - pattern.line().get() - 1, - )); - return; - } - } - } - Some(_) => (pattern.span().line_start, pattern), - None => { - self.error(pattern.span(), "no pattern specified"); - return; - } - }; - - let pattern = pattern.trim_start(); - let offset = match pattern.chars().position(|c| !c.is_ascii_alphabetic()) { - Some(offset) => offset, - None => { - self.error(pattern.span(), "pattern without level"); - return; - } - }; - - let (level, pattern) = pattern.split_at(offset); - let level = match (*level).parse() { - Ok(level) => level, - Err(msg) => { - self.error(level.span(), msg); - return; - } - }; - let pattern = match pattern.strip_prefix(":") { - Some(offset) => offset, - None => { - self.error(pattern.span(), "no `:` after level found"); - return; - } - }; - - let pattern = pattern.trim(); - - self.check(pattern.span(), !pattern.is_empty(), "no pattern specified"); - - let pattern = self.parse_error_pattern(pattern); - - *fallthrough_to = Some(match_line); - - self.error_matches.push(ErrorMatch { - pattern, - level, - line: match_line, - }); - } -} - -impl Pattern { - pub(crate) fn matches(&self, message: &str) -> bool { - match self { - Pattern::SubString(s) => message.contains(s), - Pattern::Regex(r) => r.is_match(message.as_bytes()), - } - } -} - -impl<CommentsType> CommentParser<CommentsType> { - fn parse_error_pattern(&mut self, pattern: Spanned<&str>) -> Spanned<Pattern> { - if let Some(regex) = pattern.strip_prefix("/") { - match regex.strip_suffix("/") { - Some(regex) => match self.parse_regex(regex) { - Some(r) => Spanned::new(Pattern::Regex(r), regex.span()), - None => Spanned::new(Pattern::SubString(pattern.to_string()), regex.span()), - }, - None => { - self.error( - regex.span(), - "expected regex pattern due to leading `/`, but found no closing `/`", - ); - Spanned::new(Pattern::SubString(pattern.to_string()), regex.span()) - } - } - } else { - Spanned::new(Pattern::SubString(pattern.to_string()), pattern.span()) - } - } -} |