use proc_macro2::{Delimiter, Group, Span, TokenStream, TokenTree};
use crate::registry_core::declaration::FACE_FIELD_ORDER;
use super::{FaceSyntaxError, SyntaxLocation};
pub fn compact_tokens(tokens: TokenStream) -> String {
tokens
.to_string()
.replace(" :: ", "::")
.replace(" (", "(")
.replace(") ", ")")
}
#[doc(hidden)]
pub fn split_face_fields(tokens: TokenStream) -> Vec<TokenStream> {
let tokens = tokens.into_iter().collect::<Vec<_>>();
let mut fields = Vec::new();
let mut current = TokenStream::new();
let mut value_started = false;
let mut angles = 0usize;
let mut index = 0;
while index < tokens.len() {
if let TokenTree::Punct(punct) = &tokens[index] {
match punct.as_char() {
'<' => {
angles += if punct.spacing() == proc_macro2::Spacing::Joint {
2
} else {
1
};
current.extend([tokens[index].clone()]);
value_started = true;
index += 1;
continue;
}
'>' => {
let close = if punct.spacing() == proc_macro2::Spacing::Joint {
2
} else {
1
};
angles = angles.saturating_sub(close);
current.extend([tokens[index].clone()]);
index += 1;
continue;
}
',' | ';' if angles == 0 => {
if !current.is_empty() {
fields.push(std::mem::take(&mut current));
value_started = false;
}
index += 1;
continue;
}
_ => {}
}
}
if angles == 0 && value_started && starts_face_field(&tokens, index) {
fields.push(std::mem::take(&mut current));
value_started = false;
continue;
}
value_started = true;
current.extend([tokens[index].clone()]);
index += 1;
}
if !current.is_empty() {
fields.push(current);
}
fields
}
fn starts_face_field(tokens: &[TokenTree], index: usize) -> bool {
let Some(TokenTree::Ident(name)) = tokens.get(index) else {
return false;
};
if !FACE_FIELD_ORDER.contains(&name.to_string().as_str()) {
return false;
}
match tokens.get(index + 1) {
Some(TokenTree::Punct(colon)) if colon.as_char() == ':' => {
colon.spacing() == proc_macro2::Spacing::Alone
}
_ => false,
}
}
#[doc(hidden)]
pub fn split_top_level(tokens: TokenStream) -> Vec<TokenStream> {
let mut items = Vec::new();
let mut current = TokenStream::new();
for token in tokens {
if matches!(&token, TokenTree::Punct(punct) if punct.as_char() == ',') {
if !current.is_empty() {
items.push(current);
current = TokenStream::new();
}
} else {
current.extend([token]);
}
}
if !current.is_empty() {
items.push(current);
}
items
}
#[doc(hidden)]
pub fn path_to_string(path: &syn::Path) -> String {
let mut output = String::new();
if path.leading_colon.is_some() {
output.push_str("::");
}
for (index, segment) in path.segments.iter().enumerate() {
if index > 0 {
output.push_str("::");
}
output.push_str(&segment.ident.to_string());
}
output
}
#[doc(hidden)]
pub fn location(span: Span) -> SyntaxLocation {
let start = span.start();
SyntaxLocation {
line: start.line,
column: start.column + 1,
}
}
pub(super) fn end_location(span: Span) -> SyntaxLocation {
let end = span.end();
SyntaxLocation {
line: end.line,
column: end.column + 1,
}
}
#[doc(hidden)]
pub fn syntax_error(span: Span, message: impl Into<String>) -> FaceSyntaxError {
FaceSyntaxError {
message: message.into(),
location: Some(location(span)),
}
}
pub(super) fn literal_string(expression: &syn::Expr) -> Option<String> {
let syn::Expr::Lit(expression) = expression else {
return None;
};
let syn::Lit::Str(value) = &expression.lit else {
return None;
};
Some(value.value())
}
pub(super) fn only_group(tokens: &TokenStream, delimiter: Delimiter) -> Option<Group> {
let mut tokens = tokens.clone().into_iter();
let TokenTree::Group(group) = tokens.next()? else {
return None;
};
(group.delimiter() == delimiter && tokens.next().is_none()).then_some(group)
}
#[cfg(test)]
mod tests {
use super::split_face_fields;
use proc_macro2::TokenStream;
#[test]
fn face_field_splitting_survives_generics_and_closures() {
let body: TokenStream = syn::parse_str(
"kind: Tool, preset: crate::P<u8, u16>, flow: |a: u32| a, needs_registry: true",
)
.expect("token stream");
let fields = split_face_fields(body);
let names = fields
.iter()
.map(|field| {
field
.clone()
.into_iter()
.next()
.map_or_else(String::new, |token| token.to_string())
})
.collect::<Vec<_>>();
assert_eq!(names, ["kind", "preset", "flow", "needs_registry"]);
assert!(
fields[1].to_string().contains("P < u8 , u16 >"),
"the generic comma stays in the value: {}",
fields[1]
);
assert!(
fields[2].to_string().contains("| a : u32 | a"),
"the closure parameter stays in the value: {}",
fields[2]
);
assert_eq!(
split_face_fields(syn::parse_str("kind: X;").expect("token stream")).len(),
1
);
}
}