#![allow(
clippy::derive_partial_eq_without_eq,
clippy::module_name_repetitions,
clippy::needless_doctest_main,
clippy::needless_pass_by_value,
clippy::trivially_copy_pass_by_ref,
clippy::type_complexity
)]
mod error;
mod expr;
mod unindent;
use crate::error::{Error, Result};
use crate::expr::Expr;
use crate::unindent::unindent;
use proc_macro::token_stream::IntoIter as TokenIter;
use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};
use std::iter::{self, FromIterator};
use std::str::FromStr;
#[derive(Copy, Clone, PartialEq)]
enum Macro {
Indoc,
Format,
Print,
Eprint,
Write,
}
#[proc_macro]
pub fn indoc(input: TokenStream) -> TokenStream {
expand(input, Macro::Indoc)
}
#[proc_macro]
pub fn formatdoc(input: TokenStream) -> TokenStream {
expand(input, Macro::Format)
}
#[proc_macro]
pub fn printdoc(input: TokenStream) -> TokenStream {
expand(input, Macro::Print)
}
#[proc_macro]
pub fn eprintdoc(input: TokenStream) -> TokenStream {
expand(input, Macro::Eprint)
}
#[proc_macro]
pub fn writedoc(input: TokenStream) -> TokenStream {
expand(input, Macro::Write)
}
fn expand(input: TokenStream, mode: Macro) -> TokenStream {
match try_expand(input, mode) {
Ok(tokens) => tokens,
Err(err) => err.to_compile_error(),
}
}
fn try_expand(input: TokenStream, mode: Macro) -> Result<TokenStream> {
let mut input = input.into_iter();
let prefix = if mode == Macro::Write {
Some(expr::parse(&mut input)?)
} else {
None
};
let first = input.next().ok_or_else(|| {
Error::new(
Span::call_site(),
"unexpected end of macro invocation, expected format string",
)
})?;
let unindented_lit = lit_indoc(first, mode)?;
let macro_name = match mode {
Macro::Indoc => {
require_empty_or_trailing_comma(&mut input)?;
return Ok(TokenStream::from(TokenTree::Literal(unindented_lit)));
}
Macro::Format => "format",
Macro::Print => "print",
Macro::Eprint => "eprint",
Macro::Write => "write",
};
Ok(TokenStream::from_iter(vec![
TokenTree::Ident(Ident::new(macro_name, Span::call_site())),
TokenTree::Punct(Punct::new('!', Spacing::Alone)),
TokenTree::Group(Group::new(
Delimiter::Brace,
prefix
.map_or_else(TokenStream::new, Expr::into_tokens)
.into_iter()
.chain(iter::once(TokenTree::Literal(unindented_lit)))
.chain(input)
.collect(),
)),
]))
}
fn lit_indoc(token: TokenTree, mode: Macro) -> Result<Literal> {
let span = token.span();
let mut single_token = Some(token);
while let Some(TokenTree::Group(group)) = single_token {
single_token = if group.delimiter() == Delimiter::None {
let mut token_iter = group.stream().into_iter();
token_iter.next().xor(token_iter.next())
} else {
None
};
}
let single_token =
single_token.ok_or_else(|| Error::new(span, "argument must be a single string literal"))?;
let repr = single_token.to_string();
let is_string = repr.starts_with('"') || repr.starts_with('r');
let is_byte_string = repr.starts_with("b\"") || repr.starts_with("br");
if !is_string && !is_byte_string {
return Err(Error::new(span, "argument must be a single string literal"));
}
if is_byte_string && mode != Macro::Indoc {
return Err(Error::new(
span,
"byte strings are not supported in formatting macros",
));
}
let begin = repr.find('"').unwrap() + 1;
let end = repr.rfind('"').unwrap();
let repr = format!(
"{open}{content}{close}",
open = &repr[..begin],
content = unindent(&repr[begin..end]),
close = &repr[end..],
);
match TokenStream::from_str(&repr)
.unwrap()
.into_iter()
.next()
.unwrap()
{
TokenTree::Literal(mut lit) => {
lit.set_span(span);
Ok(lit)
}
_ => unreachable!(),
}
}
fn require_empty_or_trailing_comma(input: &mut TokenIter) -> Result<()> {
let first = match input.next() {
Some(TokenTree::Punct(punct)) if punct.as_char() == ',' => match input.next() {
Some(second) => second,
None => return Ok(()),
},
Some(first) => first,
None => return Ok(()),
};
let last = input.last();
let begin_span = first.span();
let end_span = last.as_ref().map_or(begin_span, TokenTree::span);
let msg = format!(
"unexpected {token} in macro invocation; indoc argument must be a single string literal",
token = if last.is_some() { "tokens" } else { "token" }
);
Err(Error::new2(begin_span, end_span, &msg))
}