233 lines
9 KiB
Rust
233 lines
9 KiB
Rust
use crate::error::{Error, Result};
|
|
use proc_macro::{token_stream, Delimiter, Ident, Span, TokenTree};
|
|
use std::iter::Peekable;
|
|
|
|
pub(crate) enum Segment {
|
|
String(LitStr),
|
|
Apostrophe(Span),
|
|
Env(LitStr),
|
|
Modifier(Colon, Ident),
|
|
}
|
|
|
|
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",
|
|
))
|
|
}
|
|
};
|
|
let lit_string = lit.to_string();
|
|
if lit_string.starts_with('"')
|
|
&& lit_string.ends_with('"')
|
|
&& lit_string.len() >= 2
|
|
{
|
|
// TODO: maybe handle escape sequences in the string if
|
|
// someone has a use case.
|
|
segments.push(Segment::Env(LitStr {
|
|
value: lit_string[1..lit_string.len() - 1].to_owned(),
|
|
span: lit.span(),
|
|
}));
|
|
} else {
|
|
return Err(Error::new(lit.span(), "expected string literal"));
|
|
}
|
|
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::String(LitStr {
|
|
value: "_".to_owned(),
|
|
span: punct.span(),
|
|
})),
|
|
'\'' => 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 `:`"));
|
|
}
|
|
};
|
|
segments.push(Segment::Modifier(colon, ident));
|
|
}
|
|
_ => 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 segment in segments {
|
|
match segment {
|
|
Segment::String(segment) => {
|
|
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" => {
|
|
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);
|
|
}
|
|
}
|
|
prev = ch;
|
|
}
|
|
evaluated.push(acc);
|
|
}
|
|
_ => {
|
|
return Err(Error::new2(
|
|
colon.span,
|
|
ident.span(),
|
|
"unsupported modifier",
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut pasted = evaluated.into_iter().collect::<String>();
|
|
if is_lifetime {
|
|
pasted.insert(0, '\'');
|
|
}
|
|
Ok(pasted)
|
|
}
|