use crate::error::{Error, Result};
use proc_macro::{token_stream, Delimiter, Group, Ident, Literal, Span, TokenTree};
use std::iter::Peekable;
pub(crate) enum Segment {
String(LitStr),
Apostrophe(Span),
Env(LitStr),
Modifier(Colon, Ident),
Replace(Colon, Group),
}
pub(crate) struct LitStr {
pub value: String,
pub span: Span,
}
pub(crate) struct Colon {
pub span: Span,
}
pub(crate) fn parse(tokens: &mut Peekable<token_stream::IntoIter>) -> Result<Vec<Segment>> {
let mut segments = Vec::new();
while match tokens.peek() {
None => false,
Some(TokenTree::Punct(punct)) => punct.as_char() != '>',
Some(_) => true,
} {
match tokens.next().unwrap() {
TokenTree::Ident(ident) => {
let mut fragment = ident.to_string();
if fragment.starts_with("r#") {
fragment = fragment.split_off(2);
}
if fragment == "env"
&& match tokens.peek() {
Some(TokenTree::Punct(punct)) => punct.as_char() == '!',
_ => false,
}
{
let bang = tokens.next().unwrap(); let expect_group = tokens.next();
let parenthesized = match &expect_group {
Some(TokenTree::Group(group))
if group.delimiter() == Delimiter::Parenthesis =>
{
group
}
Some(wrong) => return Err(Error::new(wrong.span(), "expected `(`")),
None => {
return Err(Error::new2(
ident.span(),
bang.span(),
"expected `(` after `env!`",
));
}
};
let mut inner = parenthesized.stream().into_iter();
let lit = match inner.next() {
Some(TokenTree::Literal(lit)) => lit,
Some(wrong) => {
return Err(Error::new(wrong.span(), "expected string literal"))
}
None => {
return Err(Error::new2(
ident.span(),
parenthesized.span(),
"expected string literal as argument to env! macro",
))
}
};
segments.push(Segment::Env(LitStr {
value: get_literal_string_value(&lit, false, false)?,
span: lit.span(),
}));
if let Some(unexpected) = inner.next() {
return Err(Error::new(
unexpected.span(),
"unexpected token in env! macro",
));
}
} else {
segments.push(Segment::String(LitStr {
value: fragment,
span: ident.span(),
}));
}
}
TokenTree::Literal(lit) => {
segments.push(Segment::String(LitStr {
value: lit.to_string(),
span: lit.span(),
}));
}
TokenTree::Punct(punct) => match punct.as_char() {
'\'' => segments.push(Segment::Apostrophe(punct.span())),
':' => {
let colon_span = punct.span();
let colon = Colon { span: colon_span };
let ident = match tokens.next() {
Some(TokenTree::Ident(ident)) => ident,
wrong => {
let span = wrong.as_ref().map_or(colon_span, TokenTree::span);
return Err(Error::new(span, "expected identifier after `:`"));
}
};
if ident.to_string().as_str() == "replace" {
let replace = tokens.next();
match replace {
Some(TokenTree::Group(group))
if group.delimiter() == Delimiter::Parenthesis =>
{
segments.push(Segment::Replace(colon, group));
}
_ => {
return Err(Error::new2(
colon.span,
ident.span(),
"expected `(` after replace modifier",
));
}
}
} else {
segments.push(Segment::Modifier(colon, ident));
}
}
'#' => segments.push(Segment::String(LitStr {
value: "#".to_string(),
span: punct.span(),
})),
_ => return Err(Error::new(punct.span(), "unexpected punct")),
},
TokenTree::Group(group) => {
if group.delimiter() == Delimiter::None {
let mut inner = group.stream().into_iter().peekable();
let nested = parse(&mut inner)?;
if let Some(unexpected) = inner.next() {
return Err(Error::new(unexpected.span(), "unexpected token"));
}
segments.extend(nested);
} else {
return Err(Error::new(group.span(), "unexpected token"));
}
}
}
}
Ok(segments)
}
pub(crate) fn paste(segments: &[Segment]) -> Result<String> {
let mut evaluated = Vec::new();
let mut is_lifetime = false;
for (i, segment) in segments.iter().enumerate() {
match segment {
Segment::String(segment) => {
if segment.value.as_str() == "#" {
if i == 0 {
evaluated.push(String::from("r#"));
continue;
}
return Err(Error::new(
segment.span,
"`#` is reserved keyword and it enables the raw mode \
(i.e. generate Raw Identifiers) and it is only allowed in \
the beginning like `[< # ... >]`",
));
}
evaluated.push(segment.value.clone());
}
Segment::Apostrophe(span) => {
if is_lifetime {
return Err(Error::new(*span, "unexpected lifetime"));
}
is_lifetime = true;
}
Segment::Env(var) => {
let resolved = match std::env::var(&var.value) {
Ok(resolved) => resolved,
Err(_) => {
return Err(Error::new(
var.span,
&format!("no such env var: {:?}", var.value),
));
}
};
let resolved = resolved.replace('-', "_");
evaluated.push(resolved);
}
Segment::Modifier(colon, ident) => {
let last = match evaluated.pop() {
Some(last) => last,
None => {
return Err(Error::new2(colon.span, ident.span(), "unexpected modifier"))
}
};
match ident.to_string().as_str() {
"lower" => {
evaluated.push(last.to_lowercase());
}
"upper" => {
evaluated.push(last.to_uppercase());
}
"snake" => {
let mut acc = String::new();
let mut prev = '_';
for ch in last.chars() {
if ch.is_uppercase() && prev != '_' {
acc.push('_');
}
acc.push(ch);
prev = ch;
}
evaluated.push(acc.to_lowercase());
}
"camel" | "upper_camel" | "lower_camel" => {
let mut is_lower_camel = ident.to_string().as_str() == "lower_camel";
let mut acc = String::new();
let mut prev = '_';
for ch in last.chars() {
if ch != '_' {
if prev == '_' {
if is_lower_camel {
for chl in ch.to_lowercase() {
acc.push(chl);
}
is_lower_camel = false;
} else {
for chu in ch.to_uppercase() {
acc.push(chu);
}
}
} else if prev.is_uppercase() {
for chl in ch.to_lowercase() {
acc.push(chl);
}
} else {
acc.push(ch);
}
}
prev = ch;
}
evaluated.push(acc);
}
"camel_edge" => {
let mut acc = String::new();
let mut prev = '_';
for ch in last.chars() {
if ch != '_' {
if prev == '_' {
for chu in ch.to_uppercase() {
acc.push(chu);
}
} else if prev.is_uppercase() {
for chl in ch.to_lowercase() {
acc.push(chl);
}
} else {
acc.push(ch);
}
} else if prev == '_' {
acc.push(ch);
}
prev = ch;
}
evaluated.push(acc);
}
_ => {
return Err(Error::new2(
colon.span,
ident.span(),
"unsupported modifier",
));
}
}
}
Segment::Replace(colon, group) => {
let mut inner_stream = group.stream().into_iter();
let from = inner_stream.next();
let punct = inner_stream.next();
let to = inner_stream.next();
if let Some(unexpected_token) = inner_stream.next() {
return Err(Error::new(unexpected_token.span(), "expected `)`"));
}
match (from, punct, to) {
(Some(from), Some(TokenTree::Punct(punct)), Some(to))
if punct.as_char() == ',' =>
{
let last =
match evaluated.pop() {
Some(last) => last,
None => return Err(Error::new2(
colon.span,
group.span(),
"replace modifier requires a preceding value to operate on.",
)),
};
let from_str = get_token_tree_string_value(&from)?;
let to_str = get_token_tree_string_value(&to)?;
let new_ident = last.replace(&from_str, &to_str);
evaluated.push(new_ident);
}
_ => {
return Err(Error::new(
group.span(),
"expected replace modifier format: `:replace(\"from\", \"to\")`",
))
}
}
}
}
}
let mut pasted = evaluated.into_iter().collect::<String>();
if is_lifetime {
pasted.insert(0, '\'');
}
Ok(pasted)
}
fn is_quoted_string_or_char(l_str: &str, parse_char: bool) -> bool {
((l_str.starts_with('"') && l_str.ends_with('"'))
|| (parse_char && l_str.starts_with('\'') && l_str.ends_with('\'')))
&& l_str.len() >= 2
}
fn get_literal_string_value(l: &Literal, parse_char: bool, parse_numbers: bool) -> Result<String> {
let l_str = l.to_string();
if is_quoted_string_or_char(&l_str, parse_char) {
Ok(String::from(&l_str[1..l_str.len() - 1]))
} else if parse_numbers {
Ok(l_str)
} else {
Err(Error::new(l.span(), "expected string literal"))
}
}
pub(crate) fn get_token_tree_string_value(t: &TokenTree) -> Result<String> {
match t {
TokenTree::Ident(ident) => Ok(ident.to_string()),
TokenTree::Literal(literal) => get_literal_string_value(literal, true, true),
TokenTree::Group(group) if group.delimiter() == Delimiter::None => {
let mut inner = group.stream().into_iter();
if let Some(first) = inner.next() {
if inner.next().is_none() {
return get_token_tree_string_value(&first);
}
}
Err(Error::new(t.span(), "Expected either Ident, or Literal."))
}
_ => Err(Error::new(t.span(), "Expected either Ident, or Literal.")),
}
}
#[cfg(test)]
mod tests {
use super::is_quoted_string_or_char;
#[test]
fn test_is_quoted_string_or_char() {
assert!(is_quoted_string_or_char("\"hello\"", false));
assert!(!is_quoted_string_or_char("abc", false));
assert!(!is_quoted_string_or_char("\"", false));
assert!(!is_quoted_string_or_char("\"abc", false));
assert!(is_quoted_string_or_char("'a'", true));
assert!(!is_quoted_string_or_char("'abc", true));
assert!(!is_quoted_string_or_char("abc", true));
}
}
#[cfg(doctest)]
#[doc(hidden)]
mod doc_tests {
fn test_unexpected_punct() {}
fn test_unexpected_group() {}
fn test_env_missing_paren() {}
fn test_env_non_literal_arg() {}
fn test_env_empty() {}
fn test_env_unexpected_token() {}
fn test_no_ident_after_colon() {}
fn test_replace_missing_paren() {}
fn test_unsupported_modifier() {}
fn test_replace_extra_token() {}
fn test_replace_no_preceding_value() {}
fn test_replace_wrong_format() {}
fn test_raw_mode_wrong_position() {}
fn test_unexpected_lifetime() {}
fn test_no_such_env_var() {}
fn test_modifier_no_preceding_value() {}
fn test_env_bracket_group() {}
fn test_replace_bracket_group() {}
fn test_none_group_unexpected_token() {}
fn test_replace_wrong_separator() {}
fn test_env_numeric_literal() {}
fn test_env_char_literal() {}
fn test_replace_from_bracket_group() {}
fn test_replace_from_multi_token_none_group() {}
fn test_replace_to_multi_token_none_group() {}
}