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/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/src/parser.rs')
-rw-r--r-- | vendor/ui_test/src/parser.rs | 822 |
1 files changed, 822 insertions, 0 deletions
diff --git a/vendor/ui_test/src/parser.rs b/vendor/ui_test/src/parser.rs new file mode 100644 index 000000000..62ee5a1d1 --- /dev/null +++ b/vendor/ui_test/src/parser.rs @@ -0,0 +1,822 @@ +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()) + } + } +} |