diff options
Diffstat (limited to 'third_party/rust/paste/src')
-rw-r--r-- | third_party/rust/paste/src/attr.rs | 164 | ||||
-rw-r--r-- | third_party/rust/paste/src/error.rs | 47 | ||||
-rw-r--r-- | third_party/rust/paste/src/lib.rs | 453 | ||||
-rw-r--r-- | third_party/rust/paste/src/segment.rs | 233 |
4 files changed, 897 insertions, 0 deletions
diff --git a/third_party/rust/paste/src/attr.rs b/third_party/rust/paste/src/attr.rs new file mode 100644 index 0000000000..d66b843b27 --- /dev/null +++ b/third_party/rust/paste/src/attr.rs @@ -0,0 +1,164 @@ +use crate::error::Result; +use crate::segment::{self, Segment}; +use proc_macro::{Delimiter, Group, Spacing, Span, TokenStream, TokenTree}; +use std::iter; +use std::mem; +use std::str::FromStr; + +pub fn expand_attr( + attr: TokenStream, + span: Span, + contains_paste: &mut bool, +) -> Result<TokenStream> { + let mut tokens = attr.clone().into_iter(); + let mut leading_colons = 0; // $(::)? + let mut leading_path = 0; // $($ident)::+ + + let mut token; + let group = loop { + token = tokens.next(); + match token { + // colon after `$(:)?` + Some(TokenTree::Punct(ref punct)) + if punct.as_char() == ':' && leading_colons < 2 && leading_path == 0 => + { + leading_colons += 1; + } + // ident after `$(::)? $($ident ::)*` + Some(TokenTree::Ident(_)) if leading_colons != 1 && leading_path % 3 == 0 => { + leading_path += 1; + } + // colon after `$(::)? $($ident ::)* $ident $(:)?` + Some(TokenTree::Punct(ref punct)) if punct.as_char() == ':' && leading_path % 3 > 0 => { + leading_path += 1; + } + // eq+value after `$(::)? $($ident)::+` + Some(TokenTree::Punct(ref punct)) + if punct.as_char() == '=' && leading_path % 3 == 1 => + { + let mut count = 0; + if tokens.inspect(|_| count += 1).all(|tt| is_stringlike(&tt)) && count > 1 { + *contains_paste = true; + let leading = leading_colons + leading_path; + return do_paste_name_value_attr(attr, span, leading); + } + return Ok(attr); + } + // parens after `$(::)? $($ident)::+` + Some(TokenTree::Group(ref group)) + if group.delimiter() == Delimiter::Parenthesis && leading_path % 3 == 1 => + { + break group; + } + // bail out + _ => return Ok(attr), + } + }; + + // There can't be anything else after the first group in a valid attribute. + if tokens.next().is_some() { + return Ok(attr); + } + + let mut group_contains_paste = false; + let mut expanded = TokenStream::new(); + let mut nested_attr = TokenStream::new(); + for tt in group.stream() { + match &tt { + TokenTree::Punct(punct) if punct.as_char() == ',' => { + expanded.extend(expand_attr( + nested_attr, + group.span(), + &mut group_contains_paste, + )?); + expanded.extend(iter::once(tt)); + nested_attr = TokenStream::new(); + } + _ => nested_attr.extend(iter::once(tt)), + } + } + + if !nested_attr.is_empty() { + expanded.extend(expand_attr( + nested_attr, + group.span(), + &mut group_contains_paste, + )?); + } + + if group_contains_paste { + *contains_paste = true; + let mut group = Group::new(Delimiter::Parenthesis, expanded); + group.set_span(span); + Ok(attr + .into_iter() + // Just keep the initial ident in `#[ident(...)]`. + .take(leading_colons + leading_path) + .chain(iter::once(TokenTree::Group(group))) + .collect()) + } else { + Ok(attr) + } +} + +fn do_paste_name_value_attr(attr: TokenStream, span: Span, leading: usize) -> Result<TokenStream> { + let mut expanded = TokenStream::new(); + let mut tokens = attr.into_iter().peekable(); + expanded.extend(tokens.by_ref().take(leading + 1)); // `doc =` + + let mut segments = segment::parse(&mut tokens)?; + + for segment in &mut segments { + if let Segment::String(string) = segment { + if let Some(open_quote) = string.value.find('"') { + if open_quote == 0 { + string.value.truncate(string.value.len() - 1); + string.value.remove(0); + } else { + let begin = open_quote + 1; + let end = string.value.rfind('"').unwrap(); + let raw_string = mem::replace(&mut string.value, String::new()); + for ch in raw_string[begin..end].chars() { + string.value.extend(ch.escape_default()); + } + } + } + } + } + + let mut lit = segment::paste(&segments)?; + lit.insert(0, '"'); + lit.push('"'); + + let mut lit = TokenStream::from_str(&lit) + .unwrap() + .into_iter() + .next() + .unwrap(); + lit.set_span(span); + expanded.extend(iter::once(lit)); + Ok(expanded) +} + +fn is_stringlike(token: &TokenTree) -> bool { + match token { + TokenTree::Ident(_) => true, + TokenTree::Literal(literal) => { + let repr = literal.to_string(); + !repr.starts_with('b') && !repr.starts_with('\'') + } + TokenTree::Group(group) => { + if group.delimiter() != Delimiter::None { + return false; + } + let mut inner = group.stream().into_iter(); + match inner.next() { + Some(first) => inner.next().is_none() && is_stringlike(&first), + None => false, + } + } + TokenTree::Punct(punct) => { + punct.as_char() == '\'' || punct.as_char() == ':' && punct.spacing() == Spacing::Alone + } + } +} diff --git a/third_party/rust/paste/src/error.rs b/third_party/rust/paste/src/error.rs new file mode 100644 index 0000000000..7c5badb257 --- /dev/null +++ b/third_party/rust/paste/src/error.rs @@ -0,0 +1,47 @@ +use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree}; +use std::iter::FromIterator; + +pub type Result<T> = std::result::Result<T, Error>; + +pub struct Error { + begin: Span, + end: Span, + msg: String, +} + +impl Error { + pub fn new(span: Span, msg: &str) -> Self { + Self::new2(span, span, msg) + } + + pub fn new2(begin: Span, end: Span, msg: &str) -> Self { + Error { + begin, + end, + msg: msg.to_owned(), + } + } + + pub fn to_compile_error(&self) -> TokenStream { + // compile_error! { $msg } + TokenStream::from_iter(vec![ + TokenTree::Ident(Ident::new("compile_error", self.begin)), + TokenTree::Punct({ + let mut punct = Punct::new('!', Spacing::Alone); + punct.set_span(self.begin); + punct + }), + TokenTree::Group({ + let mut group = Group::new(Delimiter::Brace, { + TokenStream::from_iter(vec![TokenTree::Literal({ + let mut string = Literal::string(&self.msg); + string.set_span(self.end); + string + })]) + }); + group.set_span(self.end); + group + }), + ]) + } +} diff --git a/third_party/rust/paste/src/lib.rs b/third_party/rust/paste/src/lib.rs new file mode 100644 index 0000000000..80c1b98f46 --- /dev/null +++ b/third_party/rust/paste/src/lib.rs @@ -0,0 +1,453 @@ +//! [![github]](https://github.com/dtolnay/paste) [![crates-io]](https://crates.io/crates/paste) [![docs-rs]](https://docs.rs/paste) +//! +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logo=docs.rs +//! +//! <br> +//! +//! The nightly-only [`concat_idents!`] macro in the Rust standard library is +//! notoriously underpowered in that its concatenated identifiers can only refer to +//! existing items, they can never be used to define something new. +//! +//! [`concat_idents!`]: https://doc.rust-lang.org/std/macro.concat_idents.html +//! +//! This crate provides a flexible way to paste together identifiers in a macro, +//! including using pasted identifiers to define new items. +//! +//! This approach works with any Rust compiler 1.31+. +//! +//! <br> +//! +//! # Pasting identifiers +//! +//! Within the `paste!` macro, identifiers inside `[<`...`>]` are pasted +//! together to form a single identifier. +//! +//! ``` +//! use paste::paste; +//! +//! paste! { +//! // Defines a const called `QRST`. +//! const [<Q R S T>]: &str = "success!"; +//! } +//! +//! fn main() { +//! assert_eq!( +//! paste! { [<Q R S T>].len() }, +//! 8, +//! ); +//! } +//! ``` +//! +//! <br><br> +//! +//! # More elaborate example +//! +//! The next example shows a macro that generates accessor methods for some +//! struct fields. It demonstrates how you might find it useful to bundle a +//! paste invocation inside of a macro\_rules macro. +//! +//! ``` +//! use paste::paste; +//! +//! macro_rules! make_a_struct_and_getters { +//! ($name:ident { $($field:ident),* }) => { +//! // Define a struct. This expands to: +//! // +//! // pub struct S { +//! // a: String, +//! // b: String, +//! // c: String, +//! // } +//! pub struct $name { +//! $( +//! $field: String, +//! )* +//! } +//! +//! // Build an impl block with getters. This expands to: +//! // +//! // impl S { +//! // pub fn get_a(&self) -> &str { &self.a } +//! // pub fn get_b(&self) -> &str { &self.b } +//! // pub fn get_c(&self) -> &str { &self.c } +//! // } +//! paste! { +//! impl $name { +//! $( +//! pub fn [<get_ $field>](&self) -> &str { +//! &self.$field +//! } +//! )* +//! } +//! } +//! } +//! } +//! +//! make_a_struct_and_getters!(S { a, b, c }); +//! +//! fn call_some_getters(s: &S) -> bool { +//! s.get_a() == s.get_b() && s.get_c().is_empty() +//! } +//! # +//! # fn main() {} +//! ``` +//! +//! <br><br> +//! +//! # Case conversion +//! +//! Use `$var:lower` or `$var:upper` in the segment list to convert an +//! interpolated segment to lower- or uppercase as part of the paste. For +//! example, `[<ld_ $reg:lower _expr>]` would paste to `ld_bc_expr` if invoked +//! with $reg=`Bc`. +//! +//! Use `$var:snake` to convert CamelCase input to snake\_case. +//! Use `$var:camel` to convert snake\_case to CamelCase. +//! These compose, so for example `$var:snake:upper` would give you SCREAMING\_CASE. +//! +//! The precise Unicode conversions are as defined by [`str::to_lowercase`] and +//! [`str::to_uppercase`]. +//! +//! [`str::to_lowercase`]: https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase +//! [`str::to_uppercase`]: https://doc.rust-lang.org/std/primitive.str.html#method.to_uppercase +//! +//! <br> +//! +//! # Pasting documentation strings +//! +//! Within the `paste!` macro, arguments to a #\[doc ...\] attribute are +//! implicitly concatenated together to form a coherent documentation string. +//! +//! ``` +//! use paste::paste; +//! +//! macro_rules! method_new { +//! ($ret:ident) => { +//! paste! { +//! #[doc = "Create a new `" $ret "` object."] +//! pub fn new() -> $ret { todo!() } +//! } +//! }; +//! } +//! +//! pub struct Paste {} +//! +//! method_new!(Paste); // expands to #[doc = "Create a new `Paste` object"] +//! ``` + +#![allow( + clippy::derive_partial_eq_without_eq, + clippy::doc_markdown, + clippy::match_same_arms, + clippy::module_name_repetitions, + clippy::needless_doctest_main, + clippy::too_many_lines +)] + +extern crate proc_macro; + +mod attr; +mod error; +mod segment; + +use crate::attr::expand_attr; +use crate::error::{Error, Result}; +use crate::segment::Segment; +use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; +use std::char; +use std::iter; +use std::panic; + +#[proc_macro] +pub fn paste(input: TokenStream) -> TokenStream { + let mut contains_paste = false; + let flatten_single_interpolation = true; + match expand( + input.clone(), + &mut contains_paste, + flatten_single_interpolation, + ) { + Ok(expanded) => { + if contains_paste { + expanded + } else { + input + } + } + Err(err) => err.to_compile_error(), + } +} + +#[doc(hidden)] +#[proc_macro] +pub fn item(input: TokenStream) -> TokenStream { + paste(input) +} + +#[doc(hidden)] +#[proc_macro] +pub fn expr(input: TokenStream) -> TokenStream { + paste(input) +} + +fn expand( + input: TokenStream, + contains_paste: &mut bool, + flatten_single_interpolation: bool, +) -> Result<TokenStream> { + let mut expanded = TokenStream::new(); + let mut lookbehind = Lookbehind::Other; + let mut prev_none_group = None::<Group>; + let mut tokens = input.into_iter().peekable(); + loop { + let token = tokens.next(); + if let Some(group) = prev_none_group.take() { + if match (&token, tokens.peek()) { + (Some(TokenTree::Punct(fst)), Some(TokenTree::Punct(snd))) => { + fst.as_char() == ':' && snd.as_char() == ':' && fst.spacing() == Spacing::Joint + } + _ => false, + } { + expanded.extend(group.stream()); + *contains_paste = true; + } else { + expanded.extend(iter::once(TokenTree::Group(group))); + } + } + match token { + Some(TokenTree::Group(group)) => { + let delimiter = group.delimiter(); + let content = group.stream(); + let span = group.span(); + if delimiter == Delimiter::Bracket && is_paste_operation(&content) { + let segments = parse_bracket_as_segments(content, span)?; + let pasted = segment::paste(&segments)?; + let tokens = pasted_to_tokens(pasted, span)?; + expanded.extend(tokens); + *contains_paste = true; + } else if flatten_single_interpolation + && delimiter == Delimiter::None + && is_single_interpolation_group(&content) + { + expanded.extend(content); + *contains_paste = true; + } else { + let mut group_contains_paste = false; + let is_attribute = delimiter == Delimiter::Bracket + && (lookbehind == Lookbehind::Pound || lookbehind == Lookbehind::PoundBang); + let mut nested = expand( + content, + &mut group_contains_paste, + flatten_single_interpolation && !is_attribute, + )?; + if is_attribute { + nested = expand_attr(nested, span, &mut group_contains_paste)?; + } + let group = if group_contains_paste { + let mut group = Group::new(delimiter, nested); + group.set_span(span); + *contains_paste = true; + group + } else { + group.clone() + }; + if delimiter != Delimiter::None { + expanded.extend(iter::once(TokenTree::Group(group))); + } else if lookbehind == Lookbehind::DoubleColon { + expanded.extend(group.stream()); + *contains_paste = true; + } else { + prev_none_group = Some(group); + } + } + lookbehind = Lookbehind::Other; + } + Some(TokenTree::Punct(punct)) => { + lookbehind = match punct.as_char() { + ':' if lookbehind == Lookbehind::JointColon => Lookbehind::DoubleColon, + ':' if punct.spacing() == Spacing::Joint => Lookbehind::JointColon, + '#' => Lookbehind::Pound, + '!' if lookbehind == Lookbehind::Pound => Lookbehind::PoundBang, + _ => Lookbehind::Other, + }; + expanded.extend(iter::once(TokenTree::Punct(punct))); + } + Some(other) => { + lookbehind = Lookbehind::Other; + expanded.extend(iter::once(other)); + } + None => return Ok(expanded), + } + } +} + +#[derive(PartialEq)] +enum Lookbehind { + JointColon, + DoubleColon, + Pound, + PoundBang, + Other, +} + +// https://github.com/dtolnay/paste/issues/26 +fn is_single_interpolation_group(input: &TokenStream) -> bool { + #[derive(PartialEq)] + enum State { + Init, + Ident, + Literal, + Apostrophe, + Lifetime, + Colon1, + Colon2, + } + + let mut state = State::Init; + for tt in input.clone() { + state = match (state, &tt) { + (State::Init, TokenTree::Ident(_)) => State::Ident, + (State::Init, TokenTree::Literal(_)) => State::Literal, + (State::Init, TokenTree::Punct(punct)) if punct.as_char() == '\'' => State::Apostrophe, + (State::Apostrophe, TokenTree::Ident(_)) => State::Lifetime, + (State::Ident, TokenTree::Punct(punct)) + if punct.as_char() == ':' && punct.spacing() == Spacing::Joint => + { + State::Colon1 + } + (State::Colon1, TokenTree::Punct(punct)) + if punct.as_char() == ':' && punct.spacing() == Spacing::Alone => + { + State::Colon2 + } + (State::Colon2, TokenTree::Ident(_)) => State::Ident, + _ => return false, + }; + } + + state == State::Ident || state == State::Literal || state == State::Lifetime +} + +fn is_paste_operation(input: &TokenStream) -> bool { + let mut tokens = input.clone().into_iter(); + + match &tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == '<' => {} + _ => return false, + } + + let mut has_token = false; + loop { + match &tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == '>' => { + return has_token && tokens.next().is_none(); + } + Some(_) => has_token = true, + None => return false, + } + } +} + +fn parse_bracket_as_segments(input: TokenStream, scope: Span) -> Result<Vec<Segment>> { + let mut tokens = input.into_iter().peekable(); + + match &tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == '<' => {} + Some(wrong) => return Err(Error::new(wrong.span(), "expected `<`")), + None => return Err(Error::new(scope, "expected `[< ... >]`")), + } + + let mut segments = segment::parse(&mut tokens)?; + + match &tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == '>' => {} + Some(wrong) => return Err(Error::new(wrong.span(), "expected `>`")), + None => return Err(Error::new(scope, "expected `[< ... >]`")), + } + + if let Some(unexpected) = tokens.next() { + return Err(Error::new( + unexpected.span(), + "unexpected input, expected `[< ... >]`", + )); + } + + for segment in &mut segments { + if let Segment::String(string) = segment { + if string.value.starts_with("'\\u{") { + let hex = &string.value[4..string.value.len() - 2]; + if let Ok(unsigned) = u32::from_str_radix(hex, 16) { + if let Some(ch) = char::from_u32(unsigned) { + string.value.clear(); + string.value.push(ch); + continue; + } + } + } + if string.value.contains(&['#', '\\', '.', '+'][..]) + || string.value.starts_with("b'") + || string.value.starts_with("b\"") + || string.value.starts_with("br\"") + { + return Err(Error::new(string.span, "unsupported literal")); + } + let mut range = 0..string.value.len(); + if string.value.starts_with("r\"") { + range.start += 2; + range.end -= 1; + } else if string.value.starts_with(&['"', '\''][..]) { + range.start += 1; + range.end -= 1; + } + string.value = string.value[range].replace('-', "_"); + } + } + + Ok(segments) +} + +fn pasted_to_tokens(mut pasted: String, span: Span) -> Result<TokenStream> { + let mut tokens = TokenStream::new(); + + #[cfg(not(no_literal_fromstr))] + { + use proc_macro::{LexError, Literal}; + use std::str::FromStr; + + if pasted.starts_with(|ch: char| ch.is_ascii_digit()) { + let literal = match panic::catch_unwind(|| Literal::from_str(&pasted)) { + Ok(Ok(literal)) => TokenTree::Literal(literal), + Ok(Err(LexError { .. })) | Err(_) => { + return Err(Error::new( + span, + &format!("`{:?}` is not a valid literal", pasted), + )); + } + }; + tokens.extend(iter::once(literal)); + return Ok(tokens); + } + } + + if pasted.starts_with('\'') { + let mut apostrophe = TokenTree::Punct(Punct::new('\'', Spacing::Joint)); + apostrophe.set_span(span); + tokens.extend(iter::once(apostrophe)); + pasted.remove(0); + } + + let ident = match panic::catch_unwind(|| Ident::new(&pasted, span)) { + Ok(ident) => TokenTree::Ident(ident), + Err(_) => { + return Err(Error::new( + span, + &format!("`{:?}` is not a valid identifier", pasted), + )); + } + }; + + tokens.extend(iter::once(ident)); + Ok(tokens) +} diff --git a/third_party/rust/paste/src/segment.rs b/third_party/rust/paste/src/segment.rs new file mode 100644 index 0000000000..592a047021 --- /dev/null +++ b/third_party/rust/paste/src/segment.rs @@ -0,0 +1,233 @@ +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) +} |