#![cfg_attr(nightly, feature(proc_macro_span))]
extern crate proc_macro;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use crate::whitespace::{OperatorWithSpacing, Whitespace};
type FormatArgs = syn::punctuated::Punctuated<syn::Expr, syn::token::Comma>;
mod assert;
mod check;
mod hygiene_bug;
mod whitespace;
#[doc(hidden)]
#[proc_macro]
pub fn assert_impl(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
hygiene_bug::fix(assert::assert(syn::parse_macro_input!(tokens)).into())
}
#[doc(hidden)]
#[proc_macro]
pub fn check_impl(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
hygiene_bug::fix(check::check(syn::parse_macro_input!(tokens)).into())
}
struct Args {
crate_name: syn::Path,
macro_name: syn::Expr,
expression: syn::Expr,
format_args: Option<FormatArgs>,
}
impl syn::parse::Parse for Args {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let crate_name = input.parse()?;
let _comma: syn::token::Comma = input.parse()?;
let macro_name = input.parse()?;
let _comma: syn::token::Comma = input.parse()?;
let expression = syn::Expr::parse(input)?;
let format_args = if input.is_empty() {
FormatArgs::new()
} else {
input.parse::<syn::token::Comma>()?;
FormatArgs::parse_terminated(input)?
};
let format_args = Some(format_args).filter(|x| !x.is_empty());
Ok(Self {
crate_name,
macro_name,
expression,
format_args,
})
}
}
impl Args {
fn into_context(self) -> Context {
let mut predicates = split_predicates(self.expression);
let multiline = reindent_predicates(&mut predicates, 4);
let mut fragments = Fragments::new();
let print_predicates = printable_predicates(&self.crate_name, &predicates, &mut fragments);
let custom_msg = match self.format_args {
Some(x) => quote!(::core::option::Option::Some(::core::format_args!(#x))),
None => quote!(::core::option::Option::None),
};
Context {
crate_name: self.crate_name,
macro_name: self.macro_name,
predicates,
print_predicates,
multiline,
fragments,
custom_msg,
}
}
}
struct Context {
crate_name: syn::Path,
macro_name: syn::Expr,
predicates: Vec<Predicate>,
print_predicates: TokenStream,
multiline: bool,
fragments: Fragments,
custom_msg: TokenStream,
}
struct Predicate {
prefix: PredicatePrefix,
expr: syn::Expr,
}
enum PredicatePrefix {
None,
Whitespace(Whitespace),
Operator(OperatorWithSpacing),
}
impl PredicatePrefix {
fn total_newlines(&self) -> usize {
match self {
Self::None => 0,
Self::Whitespace(x) => x.lines,
Self::Operator(x) => x.total_newlines(),
}
}
fn min_indent(&self) -> Option<usize> {
match self {
Self::None => None,
Self::Whitespace(x) => x.indentation(),
Self::Operator(x) => x.min_indent(),
}
}
fn adjust_indent(&mut self, adjust: isize) {
match self {
Self::None => (),
Self::Whitespace(x) => x.adjust_indent(adjust),
Self::Operator(x) => x.adjust_indent(adjust),
}
}
}
impl std::fmt::Display for PredicatePrefix {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => Ok(()),
Self::Whitespace(x) => x.fmt(f),
Self::Operator(x) => x.fmt(f),
}
}
}
fn split_predicates(input: syn::Expr) -> Vec<Predicate> {
let mut output = Vec::new();
let mut remaining = vec![
Predicate {
prefix: PredicatePrefix::None,
expr: input,
}
];
while let Some(outer_predicate) = remaining.pop() {
match outer_predicate.expr {
syn::Expr::Binary(expr) if matches!(expr.op, syn::BinOp::And(_)) => {
let inner_operator = whitespace::operator_with_whitespace(&expr)
.unwrap_or(OperatorWithSpacing::new_logical_and());
remaining.push(Predicate {
prefix: PredicatePrefix::Operator(inner_operator),
expr: *expr.right,
});
remaining.push(Predicate {
prefix: outer_predicate.prefix,
expr: *expr.left,
});
},
_ => output.push(outer_predicate),
}
}
output
}
fn reindent_predicates(predicates: &mut [Predicate], reindent: usize) -> bool {
let mut total_newlines = 0;
let mut min_indent = None;
for predicate in predicates.iter() {
total_newlines += predicate.prefix.total_newlines();
min_indent = [min_indent, predicate.prefix.min_indent()]
.into_iter()
.flatten()
.min()
}
if total_newlines == 0 {
return false;
}
let min_indent = min_indent.unwrap_or(0);
for (i, predicate) in predicates.iter_mut().enumerate() {
if i == 0 {
if total_newlines > 0 {
predicate.prefix = PredicatePrefix::Whitespace(Whitespace::new().with_lines(1).with_spaces(reindent));
}
} else {
predicate.prefix.adjust_indent(reindent as isize - min_indent as isize);
}
}
true
}
fn printable_predicates(crate_name: &syn::Path, predicates: &[Predicate], fragments: &mut Fragments) -> TokenStream {
let mut printable_predicates = Vec::new();
for predicate in predicates {
let prefix = predicate.prefix.to_string();
let expression = match &predicate.expr {
syn::Expr::Let(expr) => {
let pattern = expression_to_string(crate_name, expr.pat.to_token_stream(), fragments);
let expression = expression_to_string(crate_name, expr.expr.to_token_stream(), fragments);
quote! {
(
#prefix,
#crate_name::__assert2_impl::print::Predicate::Let {
pattern: #pattern,
expression: #expression,
},
)
}
},
syn::Expr::Binary(expr) => {
let left = expression_to_string(crate_name, expr.left.to_token_stream(), fragments);
let operator = tokens_to_string(expr.op.to_token_stream(), fragments);
let right = expression_to_string(crate_name, expr.right.to_token_stream(), fragments);
quote! {
(
#prefix,
#crate_name::__assert2_impl::print::Predicate::Binary {
left: #left,
operator: #operator,
right: #right,
},
)
}
},
expr => {
let expression = expression_to_string(crate_name, expr.to_token_stream(), fragments);
quote! {
(
#prefix,
#crate_name::__assert2_impl::print::Predicate::Bool {
expression: #expression,
},
)
}
},
};
printable_predicates.push(expression);
}
quote!( &[#(#printable_predicates),*] )
}
fn tokens_to_string(tokens: TokenStream, fragments: &mut Fragments) -> TokenStream {
#[cfg(feature = "nightly")]
{
use syn::spanned::Spanned;
find_macro_fragments(tokens.clone(), fragments);
if let Some(s) = tokens.span().unwrap().source_text() {
return quote!(#s);
}
}
#[cfg(not(feature = "nightly"))]
{
let _ = fragments;
}
#[cfg(feature = "span-locations")]
{
let mut output = String::new();
let mut end = None;
let mut streams = vec![(tokens.into_iter(), None::<(char, Whitespace, proc_macro2::Span)>)];
while let Some((mut stream, delimiter)) = streams.pop() {
let tree = match stream.next() {
None => {
if let Some((delimiter, whitespace, delim_span)) = delimiter {
output.push_str(&whitespace.to_string());
output.push(delimiter);
end = Some(delim_span);
}
continue;
},
Some(tree) => tree,
};
streams.push((stream, delimiter));
if let Some(end) = end {
match whitespace::whitespace_between(end, tree.span()) {
Some(whitespace) => output.push_str(&whitespace.to_string()),
None => {
print!("Failed to determine whitespace before tree");
output.push(' ');
},
};
};
match tree {
proc_macro2::TokenTree::Ident(ident) => {
output.push_str(&ident.to_string());
end = Some(ident.span());
},
proc_macro2::TokenTree::Punct(punct) => {
output.push(punct.as_char());
end = match punct.spacing() {
proc_macro2::Spacing::Joint => None,
proc_macro2::Spacing::Alone => Some(punct.span()),
};
},
proc_macro2::TokenTree::Literal(literal) => {
output.push_str(&literal.to_string());
end = Some(literal.span());
},
proc_macro2::TokenTree::Group(group) => {
let (whitespace_open, whitespace_close) = whitespace::whitespace_inside(&group)
.unwrap_or((Whitespace::new(), Whitespace::new()));
let (open, close) = match group.delimiter() {
proc_macro2::Delimiter::None => ('(', ')'),
proc_macro2::Delimiter::Parenthesis => ('(', ')'),
proc_macro2::Delimiter::Brace => ('{', '}'),
proc_macro2::Delimiter::Bracket => ('[', ']'),
};
output.push(open);
output.push_str(&whitespace_open.to_string());
let stream = group.stream();
if !stream.is_empty() {
end = None;
streams.push((stream.into_iter(), Some((close, whitespace_close, group.span_close()))));
} else {
output.push_str(&whitespace_close.to_string());
output.push(close);
end = Some(group.span_close());
}
},
}
}
quote!(#output)
}
#[cfg(not(feature = "span-locations"))]
{
let tokens = tokens.to_string();
quote!(#tokens)
}
}
fn expression_to_string(crate_name: &syn::Path, tokens: TokenStream, fragments: &mut Fragments) -> TokenStream {
#[cfg(feature = "nightly")]
{
let _ = crate_name;
use syn::spanned::Spanned;
find_macro_fragments(tokens.clone(), fragments);
if let Some(s) = tokens.span().unwrap().source_text() {
return quote!(#s);
}
}
#[cfg(feature = "span-locations")]
{
let _ = crate_name;
tokens_to_string(tokens, fragments)
}
#[cfg(not(feature = "span-locations"))]
{
let _ = fragments;
quote!(#crate_name::__assert2_stringify!(#tokens))
}
}
#[cfg(feature = "nightly")]
fn find_macro_fragments(tokens: TokenStream, f: &mut Fragments) {
use syn::spanned::Spanned;
use proc_macro2::{Delimiter, TokenTree};
for token in tokens {
if let TokenTree::Group(g) = token {
if g.delimiter() == Delimiter::None {
let name = g.span().unwrap().source_text().unwrap_or_else(|| "???".into());
let contents = g.stream();
let expansion = contents.span().unwrap().source_text().unwrap_or_else(|| contents.to_string());
if name != expansion {
let entry = (name, expansion);
if !f.list.contains(&entry) {
f.list.push(entry);
}
}
}
find_macro_fragments(g.stream(), f);
}
}
}
struct Fragments {
list: Vec<(String, String)>,
}
impl Fragments {
fn new() -> Self {
Self { list: Vec::new() }
}
}
impl quote::ToTokens for Fragments {
fn to_tokens(&self, tokens: &mut TokenStream) {
let mut t = TokenStream::new();
for (name, expansion) in &self.list {
t.extend(quote!((#name, #expansion),));
}
tokens.extend(quote!(&[#t]));
}
}