#![warn(missing_docs)]
use proc_macro2::{Delimiter, Literal, Span, TokenStream, TokenTree};
mod literal;
mod macros;
pub use literal::*;
#[inline]
pub fn extract<T: literal::FromLit>(input: TokenStream) -> Result<T, TokenStream> {
let mut iter = input.into_iter();
let Some(token) = iter.next() else {
#[cold]
fn got_nothing() -> TokenStream {
comperr::error(Span::call_site(), "expected a literal, got nothing")
}
return Err(got_nothing());
};
match token {
TokenTree::Literal(lit) => {
if iter.next().is_some() {
return Err(comperr::error(lit.span(), "expected exactly one literal"));
}
T::from_lit(lit)
}
TokenTree::Ident(ident) => {
if iter.next().is_some() {
return Err(comperr::error(ident.span(), "expected exactly one literal"));
}
T::from_ident(ident)
}
TokenTree::Punct(p) if p.as_char() == '-' => match iter.next() {
Some(TokenTree::Literal(lit)) => {
if iter.next().is_some() {
return Err(comperr::error(p.span(), "expected exactly one literal"));
}
T::from_negative_lit(lit)
}
Some(other) => Err(comperr::error(other.span(), "expected a literal after `-`")),
None => Err(comperr::error(
p.span(),
"expected a literal after `-`, got nothing",
)),
},
TokenTree::Punct(p) => Err(comperr::error(
p.span(),
"expected a literal, found punctuation",
)),
TokenTree::Group(g) if g.delimiter() == Delimiter::None => extract::<T>(g.stream()),
TokenTree::Group(g) => Err(comperr::error(g.span(), "expected a literal, found group")),
}
}
#[doc(hidden)]
pub fn extract_from_iter<T: literal::FromLit>(
iter: &mut proc_macro2::token_stream::IntoIter,
) -> Result<T, comperr::Error> {
match iter.next() {
Some(TokenTree::Literal(lit)) => {
T::from_lit(lit).map_err(comperr::Error::from_token_stream)
}
Some(TokenTree::Ident(ident)) => {
T::from_ident(ident).map_err(comperr::Error::from_token_stream)
}
Some(TokenTree::Punct(p)) if p.as_char() == '-' => match iter.next() {
Some(TokenTree::Literal(lit)) => {
T::from_negative_lit(lit).map_err(comperr::Error::from_token_stream)
}
Some(other) => Err(comperr::Error::new(
other.span(),
"litext: expected a literal after `-`",
)),
None => Err(comperr::Error::new(
p.span(),
"litext: expected a literal after `-`, got nothing",
)),
},
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::None => {
let mut inner = g.stream().into_iter();
let result = extract_from_iter::<T>(&mut inner);
if inner.next().is_some() {
return Err(comperr::Error::new(g.span(), "expected a literal"));
}
result
}
Some(other) => Err(comperr::Error::new(other.span(), "expected a literal")),
None => Err(comperr::Error::new(
proc_macro2::Span::call_site(),
"expected a literal, got nothing",
)),
}
}
#[doc(hidden)]
pub fn consume_sep(
iter: &mut proc_macro2::token_stream::IntoIter,
expected: &str,
span: Span,
) -> Result<(), comperr::Error> {
let mut chars = expected.chars();
let first_ch = chars.next().expect("separator must be non-empty");
let second_ch = chars.next();
match iter.next() {
Some(TokenTree::Punct(p)) if p.as_char() == first_ch => {
if let Some(second_ch) = second_ch {
if p.spacing() != proc_macro2::Spacing::Joint {
return Err(comperr::Error::new(
p.span(),
format!("expected `{expected}` separator"),
));
}
match iter.next() {
Some(TokenTree::Punct(p2)) if p2.as_char() == second_ch => Ok(()),
Some(other) => Err(comperr::Error::new(
other.span(),
format!("expected `{expected}` separator"),
)),
None => Err(comperr::Error::new(
p.span(),
format!("expected `{expected}` separator, got nothing after `{first_ch}`"),
)),
}
} else {
Ok(())
}
}
Some(other) => Err(comperr::Error::new(
other.span(),
format!("expected `{expected}` separator"),
)),
None => Err(comperr::Error::new(
span,
format!("expected `{expected}` separator, got nothing"),
)),
}
}
#[inline]
pub(crate) fn parse_lit(lit: &Literal) -> Result<String, TokenStream> {
let raw = lit.to_string();
let span = lit.span();
if raw.starts_with('b') && raw.len() > 1 {
let c = raw.chars().nth(1).unwrap();
if c == '"' || c == 'r' {
return Err(comperr::error(
span,
"expected a string literal, not a byte string",
));
}
}
if raw.starts_with('r') {
return parse_raw(&raw).ok_or_else(|| comperr::error(span, "malformed raw string literal"));
}
if raw.starts_with('"') && raw.ends_with('"') && raw.len() >= 2 {
return unescape(&raw[1..raw.len() - 1], span);
}
Err(comperr::error(span, "expected a string literal"))
}
#[inline]
pub(crate) fn parse_raw(raw: &str) -> Option<String> {
let rest = raw.strip_prefix('r')?;
let hashes = rest.chars().take_while(|c| *c == '#').count();
let hash_str = "#".repeat(hashes);
let inner = rest
.strip_prefix(&hash_str)?
.strip_prefix('"')?
.strip_suffix(&hash_str)?
.strip_suffix('"')?;
Some(inner.to_string())
}
#[inline]
pub(crate) fn unescape(s: &str, span: Span) -> Result<String, TokenStream> {
let mut output = String::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
output.push(c);
continue;
}
match chars.next() {
Some('n') => output.push('\n'),
Some('r') => output.push('\r'),
Some('t') => output.push('\t'),
Some('\\') => output.push('\\'),
Some('"') => output.push('"'),
Some('0') => output.push('\0'),
Some('x') => {
let h1 = chars
.next()
.ok_or_else(|| comperr::error(span, "invalid \\x escape"))?;
let h2 = chars
.next()
.ok_or_else(|| comperr::error(span, "invalid \\x escape"))?;
let hex = format!("{h1}{h2}");
let byte = u8::from_str_radix(&hex, 16)
.map_err(|_| comperr::error(span, "invalid \\x escape"))?;
if byte > 0x7F {
return Err(comperr::error(
span,
"\\x escape must be in range 0x00..=0x7F",
));
}
output.push(byte as char);
}
Some('u') => {
match chars.next() {
Some('{') => {}
_ => return Err(comperr::error(span, "invalid \\u escape, expected '{'")),
}
let mut hex = String::new();
loop {
match chars.next() {
Some('}') => break,
Some(c) => hex.push(c),
None => return Err(comperr::error(span, "unterminated \\u escape")),
}
}
let codepoint = u32::from_str_radix(&hex, 16)
.map_err(|_| comperr::error(span, "invalid \\u codepoint"))?;
let ch = char::from_u32(codepoint)
.ok_or_else(|| comperr::error(span, "invalid unicode codepoint"))?;
output.push(ch);
}
Some('\n') => {
while let Some(&c) = chars.as_str().chars().next().as_ref() {
if c.is_whitespace() {
chars.next();
} else {
break;
}
}
}
_ => return Err(comperr::error(span, "invalid escape sequence")),
}
}
Ok(output)
}
#[inline]
pub(crate) fn unescape_bytes(s: &str, span: Span) -> Result<Vec<u8>, TokenStream> {
let mut output = Vec::with_capacity(s.len());
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c != '\\' {
output.push(c as u8);
continue;
}
match chars.next() {
Some('n') => output.push(b'\n'),
Some('r') => output.push(b'\r'),
Some('t') => output.push(b'\t'),
Some('\\') => output.push(b'\\'),
Some('"') => output.push(b'"'),
Some('\'') => output.push(b'\''),
Some('0') => output.push(0),
Some('x') => {
let h1 = chars
.next()
.ok_or_else(|| comperr::error(span, "invalid \\x escape"))?;
let h2 = chars
.next()
.ok_or_else(|| comperr::error(span, "invalid \\x escape"))?;
let hex = format!("{h1}{h2}");
let byte = u8::from_str_radix(&hex, 16)
.map_err(|_| comperr::error(span, "invalid \\x escape"))?;
output.push(byte);
}
Some('\n') => {
while let Some(&c) = chars.as_str().chars().next().as_ref() {
if c.is_whitespace() {
chars.next();
} else {
break;
}
}
}
_ => return Err(comperr::error(span, "invalid escape sequence")),
}
}
Ok(output)
}