Merge branch 'compare_fields_macro'
All checks were successful
rustfmt Build rustfmt has succeeded
rust-build Build rust-build has succeeded
rust-test Build rust-test has succeeded
rust-foreign-document-test Build rust-foreign-document-test has succeeded

This commit is contained in:
Tom Alexander 2023-10-06 16:36:19 -04:00
commit e2bc58a469
Signed by: talexander
GPG Key ID: D3A179C9A53C0EDE
4 changed files with 264 additions and 25 deletions

View File

@ -0,0 +1,53 @@
use std::fmt::Debug;
use super::diff::DiffStatus;
use super::sexp::Token;
use super::util::get_property_quoted_string;
#[derive(Debug)]
pub(crate) enum EmacsField<'s> {
Required(&'s str),
#[allow(dead_code)]
Optional(&'s str),
}
/// Do no comparison.
///
/// This is for when you want to acknowledge that a field exists in the emacs token, but you do not have any validation for it when using the compare_properties!() macro. Ideally, this should be kept to a minimum since this represents untested values.
#[allow(dead_code)]
pub(crate) fn compare_noop<'b, 's, 'x, R, RG>(
_emacs: &'b Token<'s>,
_rust_node: R,
_emacs_field: &'x str,
_rust_value_getter: RG,
) -> Result<Option<(DiffStatus, Option<String>)>, Box<dyn std::error::Error>> {
Ok(None)
}
/// Do no comparison.
///
/// This is for when you want to acknowledge that a field exists in the emacs token, but you do not have any validation for it when using the compare_properties!() macro. Ideally, this should be kept to a minimum since this represents untested values.
#[allow(dead_code)]
pub(crate) fn compare_identity() -> () {
()
}
pub(crate) fn compare_property_quoted_string<'b, 's, 'x, R, RG: Fn(R) -> Option<&'s str>>(
emacs: &'b Token<'s>,
rust_node: R,
emacs_field: &'x str,
rust_value_getter: RG,
) -> Result<Option<(DiffStatus, Option<String>)>, Box<dyn std::error::Error>> {
let value = get_property_quoted_string(emacs, emacs_field)?;
let rust_value = rust_value_getter(rust_node);
if !rust_value.eq(&value.as_ref().map(String::as_str)) {
let this_status = DiffStatus::Bad;
let message = Some(format!(
"{} mismatch (emacs != rust) {:?} != {:?}",
emacs_field, value, rust_value
));
Ok(Some((this_status, message)))
} else {
Ok(None)
}
}

View File

@ -3,6 +3,7 @@ use std::borrow::Cow;
use std::collections::BTreeSet;
use std::collections::HashSet;
use super::compare_field::compare_property_quoted_string;
use super::elisp_fact::ElispFact;
use super::elisp_fact::GetElispFact;
use super::sexp::unquote;
@ -13,6 +14,8 @@ use super::util::get_property_boolean;
use super::util::get_property_numeric;
use super::util::get_property_quoted_string;
use super::util::get_property_unquoted_atom;
use crate::compare::compare_field::EmacsField;
use crate::compare::macros::compare_properties;
use crate::types::AngleLink;
use crate::types::AstNode;
use crate::types::BabelCall;
@ -124,7 +127,7 @@ pub struct DiffResult<'b, 's> {
}
#[derive(Debug, PartialEq)]
enum DiffStatus {
pub(crate) enum DiffStatus {
Good,
Bad,
}
@ -2572,9 +2575,14 @@ fn compare_bold<'b, 's>(
rust: &'b Bold<'s>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let this_status = DiffStatus::Good;
let mut this_status = DiffStatus::Good;
let mut child_status = Vec::new();
let message = None;
let mut message = None;
if let Some((new_status, new_message)) = compare_properties!(emacs)? {
this_status = new_status;
message = new_message;
}
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?);
@ -2597,9 +2605,14 @@ fn compare_italic<'b, 's>(
rust: &'b Italic<'s>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let this_status = DiffStatus::Good;
let mut this_status = DiffStatus::Good;
let mut child_status = Vec::new();
let message = None;
let mut message = None;
if let Some((new_status, new_message)) = compare_properties!(emacs)? {
this_status = new_status;
message = new_message;
}
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?);
@ -2622,9 +2635,14 @@ fn compare_underline<'b, 's>(
rust: &'b Underline<'s>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let this_status = DiffStatus::Good;
let mut this_status = DiffStatus::Good;
let mut child_status = Vec::new();
let message = None;
let mut message = None;
if let Some((new_status, new_message)) = compare_properties!(emacs)? {
this_status = new_status;
message = new_message;
}
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?);
@ -2649,14 +2667,17 @@ fn compare_verbatim<'b, 's>(
let mut this_status = DiffStatus::Good;
let mut message = None;
// Compare value
let value = get_property_quoted_string(emacs, ":value")?;
if value.as_ref().map(String::as_str) != Some(rust.contents) {
this_status = DiffStatus::Bad;
message = Some(format!(
"Value mismatch (emacs != rust) {:?} != {:?}",
value, rust.contents
));
if let Some((new_status, new_message)) = compare_properties!(
emacs,
rust,
(
EmacsField::Required(":value"),
|r| Some(r.contents),
compare_property_quoted_string
)
)? {
this_status = new_status;
message = new_message;
}
Ok(DiffResult {
@ -2678,14 +2699,17 @@ fn compare_code<'b, 's>(
let mut this_status = DiffStatus::Good;
let mut message = None;
// Compare value
let value = get_property_quoted_string(emacs, ":value")?;
if value.as_ref().map(String::as_str) != Some(rust.contents) {
this_status = DiffStatus::Bad;
message = Some(format!(
"Value mismatch (emacs != rust) {:?} != {:?}",
value, rust.contents
));
if let Some((new_status, new_message)) = compare_properties!(
emacs,
rust,
(
EmacsField::Required(":value"),
|r| Some(r.contents),
compare_property_quoted_string
)
)? {
this_status = new_status;
message = new_message;
}
Ok(DiffResult {
@ -2705,9 +2729,14 @@ fn compare_strike_through<'b, 's>(
rust: &'b StrikeThrough<'s>,
) -> Result<DiffEntry<'b, 's>, Box<dyn std::error::Error>> {
let children = emacs.as_list()?;
let this_status = DiffStatus::Good;
let mut this_status = DiffStatus::Good;
let mut child_status = Vec::new();
let message = None;
let mut message = None;
if let Some((new_status, new_message)) = compare_properties!(emacs)? {
this_status = new_status;
message = new_message;
}
for (emacs_child, rust_child) in children.iter().skip(2).zip(rust.children.iter()) {
child_status.push(compare_ast_node(source, emacs_child, rust_child.into())?);

155
src/compare/macros.rs Normal file
View File

@ -0,0 +1,155 @@
/// Assert only the listed properties exist on the Emacs AST node and compare their values to the values on the rust AST node.
///
/// Example invocation:
/// ```
/// if let Some((new_status, new_message)) = compare_properties!(
/// emacs,
/// rust,
/// (
/// EmacsField::Required(":key"),
/// |r| Some(r.key),
/// compare_property_quoted_string
/// ),
/// (
/// EmacsField::Required(":value"),
/// |r| Some(r.contents),
/// compare_property_quoted_string
/// ),
/// (EmacsField::Required(":pre-blank"), compare_identity, compare_noop)
/// )? {
/// this_status = new_status;
/// message = new_message;
/// }
/// ```
///
/// Or if the node has no properties aside from :standard-properties, we can still assert that the node has no unexpected properties:
/// ```
/// if let Some((new_status, new_message)) = compare_properties!(emacs)? {
/// this_status = new_status;
/// message = new_message;
/// }
/// ```
macro_rules! compare_properties {
($emacs:expr, $rust:expr, $(($emacs_field:expr, $rust_value_getter:expr, $compare_fn: expr)),+) => {
{
let mut this_status = DiffStatus::Good;
let mut message: Option<String> = None;
let children = $emacs.as_list()?;
let attributes_child = children
.iter()
.nth(1)
.ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let mut emacs_keys: BTreeSet<&str> = attributes_map.keys().map(|s| *s).collect();
if emacs_keys.contains(":standard-properties") {
emacs_keys.remove(":standard-properties");
} else {
this_status = DiffStatus::Bad;
message = Some(format!(
"Emacs token lacks :standard-properties field.",
));
}
$(
match $emacs_field {
EmacsField::Required(name) if emacs_keys.contains(name) => {
emacs_keys.remove(name);
},
EmacsField::Optional(name) if emacs_keys.contains(name) => {
emacs_keys.remove(name);
},
EmacsField::Required(name) => {
this_status = DiffStatus::Bad;
message = Some(format!(
"Emacs token lacks required field: {}",
name
));
},
EmacsField::Optional(_name) => {},
}
)+
if !emacs_keys.is_empty() {
let unexpected_keys: Vec<&str> = emacs_keys.into_iter().collect();
let unexpected_keys = unexpected_keys.join(", ");
this_status = DiffStatus::Bad;
message = Some(format!(
"Emacs token had extra field(s): {}",
unexpected_keys
));
}
$(
let emacs_name = match $emacs_field {
EmacsField::Required(name) => {
name
},
EmacsField::Optional(name) => {
name
},
};
let result = $compare_fn($emacs, $rust, emacs_name, $rust_value_getter)?;
match result {
Some((DiffStatus::Good, _)) => unreachable!("No comparison functions should return Some() when DiffStatus is good."),
Some((status, msg)) => {
this_status = status;
message = msg;
},
_ => {}
}
)+
match this_status {
DiffStatus::Good => {
let result: Result<_, Box<dyn std::error::Error>> = Ok(None);
result
},
_ => {
Ok(Some((this_status, message)))
}
}
}
};
// Default case for when there are no expected properties except for :standard-properties
($emacs:expr) => {
{
let mut this_status = DiffStatus::Good;
let mut message: Option<String> = None;
let children = $emacs.as_list()?;
let attributes_child = children
.iter()
.nth(1)
.ok_or("Should have an attributes child.")?;
let attributes_map = attributes_child.as_map()?;
let mut emacs_keys: BTreeSet<&str> = attributes_map.keys().map(|s| *s).collect();
if emacs_keys.contains(":standard-properties") {
emacs_keys.remove(":standard-properties");
} else {
this_status = DiffStatus::Bad;
message = Some(format!(
"Emacs token lacks :standard-properties field.",
));
}
if !emacs_keys.is_empty() {
let unexpected_keys: Vec<&str> = emacs_keys.into_iter().collect();
let unexpected_keys = unexpected_keys.join(", ");
this_status = DiffStatus::Bad;
message = Some(format!(
"Emacs token had extra field(s): {}",
unexpected_keys
));
}
match this_status {
DiffStatus::Good => {
let result: Result<_, Box<dyn std::error::Error>> = Ok(None);
result
},
_ => {
Ok(Some((this_status, message)))
}
}
}
};
}
pub(crate) use compare_properties;

View File

@ -1,6 +1,8 @@
mod compare;
mod compare_field;
mod diff;
mod elisp_fact;
mod macros;
mod parse;
mod sexp;
mod util;