use std::{
collections::HashMap,
num::NonZeroUsize,
path::{Path, PathBuf},
};
use bstr::{ByteSlice, Utf8Error};
use regex::bytes::Regex;
use crate::{rustc_stderr::Level, Error, Errors, Mode};
use color_eyre::eyre::{Context, Result};
pub(crate) use opt_with_line::*;
mod opt_with_line;
#[cfg(test)]
mod tests;
#[derive(Default, Debug)]
pub(crate) struct Comments {
pub revisions: Option<Vec<String>>,
pub revisioned: HashMap<Vec<String>, Revisioned>,
}
impl Comments {
pub fn find_one_for_revision<'a, T: 'a>(
&'a self,
revision: &'a str,
kind: &str,
f: impl Fn(&'a Revisioned) -> OptWithLine<T>,
) -> (OptWithLine<T>, Errors) {
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(Error::InvalidComment {
msg: format!("multiple {kind} found"),
line: found.line(),
});
} else {
result = found.into();
}
}
}
(result.into(), errors)
}
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,
) -> (Option<MaybeWithLine<String>>, Errors) {
let (edition, errors) =
self.find_one_for_revision(revision, "`edition` annotations", |r| r.edition.clone());
let edition = edition
.into_inner()
.map(MaybeWithLine::from)
.or(config.edition.clone().map(MaybeWithLine::new_config));
(edition, errors)
}
}
#[derive(Debug)]
pub(crate) struct Revisioned {
pub line: NonZeroUsize,
pub ignore: Vec<Condition>,
pub only: Vec<Condition>,
pub stderr_per_bitwidth: bool,
pub compile_flags: Vec<String>,
pub env_vars: Vec<(String, String)>,
pub normalize_stderr: Vec<(Regex, Vec<u8>)>,
pub error_in_other_files: Vec<WithLine<Pattern>>,
pub error_matches: Vec<ErrorMatch>,
pub require_annotations_for_level: OptWithLine<Level>,
pub aux_builds: Vec<WithLine<(PathBuf, String)>>,
pub edition: OptWithLine<String>,
pub mode: OptWithLine<Mode>,
pub needs_asm_support: bool,
pub no_rustfix: OptWithLine<()>,
}
#[derive(Debug)]
struct CommentParser<T> {
comments: T,
errors: Vec<Error>,
line: NonZeroUsize,
commands: HashMap<&'static str, CommandParserFunc>,
}
type CommandParserFunc = fn(&mut CommentParser<&mut Revisioned>, args: &str);
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
}
}
#[derive(Debug)]
pub(crate) enum Condition {
Host(String),
Target(String),
Bitwidth(u8),
OnHost,
}
#[derive(Debug, Clone)]
pub enum Pattern {
SubString(String),
Regex(Regex),
}
#[derive(Debug)]
pub(crate) struct ErrorMatch {
pub pattern: WithLine<Pattern>,
pub level: Level,
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))
}
pub(crate) fn parse(
content: &(impl AsRef<[u8]> + ?Sized),
) -> std::result::Result<Self, Vec<Error>> {
let mut parser = CommentParser {
comments: Comments::default(),
errors: vec![],
line: NonZeroUsize::MAX,
commands: CommentParser::<_>::commands(),
};
let mut fallthrough_to = None; for (l, line) in content.as_ref().lines().enumerate() {
let l = NonZeroUsize::new(l + 1).unwrap(); parser.line = l;
match parser.parse_checked_line(&mut fallthrough_to, line) {
Ok(()) => {}
Err(e) => parser.errors.push(Error::InvalidComment {
msg: format!("Comment is not utf8: {e:?}"),
line: l,
}),
}
}
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"),
line: revisioned.line,
})
}
}
}
} 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(),
line: revisioned.line,
})
}
}
}
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: &[u8],
) -> std::result::Result<(), Utf8Error> {
if let Some(command) = line.strip_prefix(b"//@") {
self.parse_command(command.trim().to_str()?)
} 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[pos + 2..];
for rest in std::iter::once(rest).chain(rest.strip_prefix(b" ")) {
if let Some('@' | '~' | '[' | ']' | '^' | '|') = rest.chars().next() {
self.errors.push(Error::InvalidComment {
msg: 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.to_str()?,
),
line: self.line,
})
} else {
let mut parser = Self {
line: NonZeroUsize::MAX,
errors: vec![],
comments: Comments::default(),
commands: std::mem::take(&mut self.commands),
};
parser.parse_command(rest.to_str()?);
if parser.errors.is_empty() {
self.error(
"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, s: impl Into<String>) {
self.errors.push(Error::InvalidComment {
msg: s.into(),
line: self.line,
});
}
fn check(&mut self, cond: bool, s: impl Into<String>) {
if !cond {
self.error(s);
}
}
fn check_some<T>(&mut self, opt: Option<T>, s: impl Into<String>) -> Option<T> {
self.check(opt.is_some(), s);
opt
}
}
impl CommentParser<Comments> {
fn parse_command(&mut self, command: &str) {
let (revisions, command) = self.parse_revisions(command);
let (command, args) = match command
.char_indices()
.find_map(|(i, c)| (!c.is_alphanumeric() && c != '-' && c != '_').then_some(i))
{
None => (command, ""),
Some(i) => {
let (command, args) = command.split_at(i);
let mut args = args.chars();
let next = args
.next()
.expect("the `position` above guarantees that there is at least one char");
self.check(
next == ':',
"test command must be followed by `:` (or end the line)",
);
(command, args.as_str().trim())
}
};
if command == "revisions" {
self.check(
revisions.is_empty(),
"revisions cannot be declared under a revision",
);
self.check(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: Vec<String>,
f: impl FnOnce(&mut CommentParser<&mut Revisioned>),
) {
let line = self.line;
let mut this = CommentParser {
errors: std::mem::take(&mut self.errors),
commands: std::mem::take(&mut self.commands),
line,
comments: self
.revisioned
.entry(revisions)
.or_insert_with(|| Revisioned {
line,
ignore: Default::default(),
only: Default::default(),
stderr_per_bitwidth: Default::default(),
compile_flags: Default::default(),
env_vars: Default::default(),
normalize_stderr: 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 commands() -> HashMap<&'static str, CommandParserFunc> {
let mut commands = HashMap::<_, CommandParserFunc>::new();
macro_rules! commands {
($($name:expr => ($this:ident, $args:ident)$block:block)*) => {
$(commands.insert($name, |$this, $args| {
$block
});)*
};
}
commands! {
"compile-flags" => (this, args){
if let Some(parsed) = comma::parse_command(args) {
this.compile_flags.extend(parsed);
} else {
this.error(format!("`{args}` contains an unclosed quotation mark"));
}
}
"rustc-env" => (this, args){
for env in args.split_whitespace() {
if let Some((k, v)) = this.check_some(
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){
let (from, rest) = this.parse_str(args);
let to = match rest.strip_prefix("->") {
Some(v) => v,
None => {
this.error("normalize-stderr-test needs a pattern and replacement separated by `->`");
return;
},
}.trim_start();
let (to, rest) = this.parse_str(to);
this.check(
rest.is_empty(),
format!("trailing text after pattern replacement: {rest}"),
);
if let Some(regex) = this.parse_regex(from) {
this.normalize_stderr
.push((regex, to.as_bytes().to_owned()))
}
}
"error-pattern" => (this, _args){
this.error("`error-pattern` has been renamed to `error-in-other-file`");
}
"error-in-other-file" => (this, args){
let pat = this.parse_error_pattern(args.trim());
let line = this.line;
this.error_in_other_files.push(WithLine::new(pat, line));
}
"stderr-per-bitwidth" => (this, _args){
this.check(
!this.stderr_per_bitwidth,
"cannot specify `stderr-per-bitwidth` twice",
);
this.stderr_per_bitwidth = true;
}
"run-rustfix" => (this, _args){
this.error("rustfix is now ran by default when applicable suggestions are found");
}
"no-rustfix" => (this, _args){
let line = this.line;
let prev = this.no_rustfix.set((), line);
this.check(
prev.is_none(),
"cannot specify `no-rustfix` twice",
);
}
"needs-asm-support" => (this, _args){
this.check(
!this.needs_asm_support,
"cannot specify `needs-asm-support` twice",
);
this.needs_asm_support = true;
}
"aux-build" => (this, args){
let (name, kind) = args.split_once(':').unwrap_or((args, "lib"));
let line = this.line;
this.aux_builds.push(WithLine::new((name.into(), kind.into()), line));
}
"edition" => (this, args){
let line = this.line;
let prev = this.edition.set(args.into(), line);
this.check(prev.is_none(), "cannot specify `edition` twice");
}
"check-pass" => (this, _args){
let line = this.line;
let prev = this.mode.set(Mode::Pass, line);
this.check(
prev.is_none(),
"cannot specify test mode changes twice",
);
}
"run" => (this, args){
this.check(
this.mode.is_none(),
"cannot specify test mode changes twice",
);
let line = this.line;
let mut set = |exit_code| this.mode.set(Mode::Run { exit_code }, line);
if args.is_empty() {
set(0);
} else {
match args.parse() {
Ok(exit_code) => {set(exit_code);},
Err(err) => this.error(err.to_string()),
}
}
}
"require-annotations-for-level" => (this, args){
let line = this.line;
let prev = match args.trim().parse() {
Ok(it) => this.require_annotations_for_level.set(it, line),
Err(msg) => {
this.error(msg);
None
},
};
this.check(
prev.is_none(),
"cannot specify `require-annotations-for-level` twice",
);
}
}
commands
}
fn parse_command(&mut self, command: &str, args: &str) {
if let Some(command) = self.commands.get(command) {
command(self, args);
} else if let Some(s) = command.strip_prefix("ignore-") {
match Condition::parse(s) {
Ok(cond) => self.ignore.push(cond),
Err(msg) => self.error(msg),
}
} else if let Some(s) = command.strip_prefix("only-") {
match Condition::parse(s) {
Ok(cond) => self.only.push(cond),
Err(msg) => self.error(msg),
}
} else {
let best_match = self
.commands
.keys()
.min_by_key(|key| distance::damerau_levenshtein(key, command))
.unwrap();
self.error(format!(
"`{command}` is not a command known to `ui_test`, did you mean `{best_match}`?"
));
}
}
}
impl<CommentsType> CommentParser<CommentsType> {
fn parse_regex(&mut self, regex: &str) -> Option<Regex> {
match Regex::new(regex) {
Ok(regex) => Some(regex),
Err(err) => {
self.error(format!("invalid regex: {err:?}"));
None
}
}
}
fn parse_str<'a>(&mut self, s: &'a str) -> (&'a str, &'a str) {
let mut chars = s.char_indices();
match chars.next() {
Some((_, '"')) => {
let s = chars.as_str();
let mut escaped = false;
for (i, c) in chars {
if escaped {
escaped = false;
} else if c == '"' {
return (&s[..(i - 1)], s[i..].trim_start());
} else {
escaped = c == '\\';
}
}
self.error(format!("no closing quotes found for {s}"));
(s, "")
}
Some((_, c)) => {
self.error(format!("expected `\"`, got `{c}`"));
(s, "")
}
None => {
self.error("expected quoted string, but found end of line");
(s, "")
}
}
}
fn parse_revisions<'a>(&mut self, pattern: &'a str) -> (Vec<String>, &'a str) {
match pattern.chars().next() {
Some('[') => {
let s = &pattern[1..];
let end = s.char_indices().find_map(|(i, c)| match c {
']' => Some(i),
_ => None,
});
let Some(end) = end else {
self.error("`[` without corresponding `]`");
return (vec![], pattern);
};
let (revision, pattern) = s.split_at(end);
(
revision.split(',').map(|s| s.trim().to_string()).collect(),
pattern[1..].trim_start(),
)
}
_ => (vec![], pattern),
}
}
}
impl CommentParser<&mut Revisioned> {
fn parse_pattern(&mut self, pattern: &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 without preceding line");
return;
}
},
&pattern[1..],
),
Some('^') => {
let offset = pattern.chars().take_while(|&c| c == '^').count();
match self
.line
.get()
.checked_sub(offset)
.and_then(NonZeroUsize::new)
{
Some(match_line) => (match_line, &pattern[offset..]),
_ => {
self.error(format!(
"//~^ pattern is trying to refer to {} lines above, but there are only {} lines above",
offset,
self.line.get() - 1
));
return;
}
}
}
Some(_) => (self.line, pattern),
None => {
self.error("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 without level");
return;
}
};
let level = match pattern[..offset].parse() {
Ok(level) => level,
Err(msg) => {
self.error(msg);
return;
}
};
let pattern = &pattern[offset..];
let pattern = match pattern.strip_prefix(':') {
Some(offset) => offset,
None => {
self.error("no `:` after level found");
return;
}
};
let pattern = pattern.trim();
self.check(!pattern.is_empty(), "no pattern specified");
let pattern = self.parse_error_pattern(pattern);
*fallthrough_to = Some(match_line);
let pattern = WithLine::new(pattern, self.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: &str) -> Pattern {
if let Some(regex) = pattern.strip_prefix('/') {
match regex.strip_suffix('/') {
Some(regex) => match self.parse_regex(regex) {
Some(regex) => Pattern::Regex(regex),
None => Pattern::SubString(pattern.to_string()),
},
None => {
self.error(
"expected regex pattern due to leading `/`, but found no closing `/`",
);
Pattern::SubString(pattern.to_string())
}
}
} else {
Pattern::SubString(pattern.to_string())
}
}
}