use convert_case::{Case, Casing};
use proc_macro::{Delimiter, Group, Ident, Spacing, Span, TokenStream, TokenTree};
use std::collections::HashMap;
fn compile_error_for(message: String) -> TokenStream {
format!("compile_error!({:?})", message)
.parse()
.expect("a compile_error! invocation with an escaped string literal always parses")
}
fn compile_error_at(message: &str, span: Span) -> TokenStream {
let mut literal = proc_macro::Literal::string(message);
literal.set_span(span);
let message_tokens = TokenStream::from(TokenTree::Literal(literal));
fill(
"compile_error!(MESSAGE);",
span,
&[("MESSAGE", &message_tokens)],
)
}
type FoundStructs = Vec<(Vec<TokenTree>, Vec<Vec<TokenTree>>)>;
#[proc_macro]
pub fn paths(struct_path_stream: TokenStream) -> TokenStream {
match paths_impl(struct_path_stream) {
Ok(stream) => stream,
Err(message) => compile_error_for(message),
}
}
fn paths_impl(struct_path_stream: TokenStream) -> Result<TokenStream, String> {
let top_level_tokens: Vec<TokenTree> = struct_path_stream.clone().into_iter().collect();
if let Some(result) = try_parse_nested_star(&top_level_tokens) {
return result;
}
if let Some(result) = try_parse_all_fields(&struct_path_stream) {
return result;
}
let mut current_struct_name_tokens: Vec<TokenTree> = Vec::new();
let mut current_struct_fields: Vec<Vec<TokenTree>> = Vec::with_capacity(16);
let mut opened_struct = false;
let mut colons_counter = 0;
let mut options_opened = false;
let mut current_field_tokens: Vec<TokenTree> = Vec::new();
let mut current_option_name: Option<String> = None;
let mut expect_option_value: bool = false;
let mut options: HashMap<String, String> = HashMap::new();
let mut found_structs: FoundStructs = Vec::new();
for token_tree in struct_path_stream.into_iter() {
match token_tree {
TokenTree::Ident(id) if current_struct_name_tokens.is_empty() => {
current_struct_name_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !current_struct_name_tokens.is_empty()
&& !opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
if colons_counter > 1 {
opened_struct = true;
}
}
TokenTree::Ident(id) if opened_struct => {
colons_counter = 0;
current_field_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !current_struct_name_tokens.is_empty()
&& opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
opened_struct = false;
let field_tokens = std::mem::take(&mut current_field_tokens);
if !fold_field_into_struct_name(field_tokens, &mut current_struct_name_tokens) {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
}
TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
if current_field_tokens.is_empty() {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
current_field_tokens.push(TokenTree::Punct(punct));
}
TokenTree::Group(group) if opened_struct && current_field_tokens.is_empty() => {
parse_multiple_fields(group.stream(), &mut current_struct_fields)?
}
TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
opened_struct = false;
colons_counter = 0;
if current_struct_name_tokens.is_empty() {
return Err("Unexpected comma with empty definitions!".to_string());
}
if !current_field_tokens.is_empty() {
current_struct_fields.push(std::mem::take(&mut current_field_tokens));
}
if current_struct_fields.is_empty() {
return Err(format!(
"Unexpected comma with empty fields for {}!",
render_text(¤t_struct_name_tokens)
));
}
found_structs.push((
std::mem::take(&mut current_struct_name_tokens),
std::mem::take(&mut current_struct_fields),
));
}
TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
options_opened = true;
opened_struct = false;
}
TokenTree::Ident(id) if options_opened && !expect_option_value => {
current_option_name = Some(id.to_string())
}
TokenTree::Ident(id) if options_opened && expect_option_value => {
expect_option_value = false;
match current_option_name.take() {
Some(option_name) => {
options.insert(option_name, id.to_string());
}
_ => {
return Err("Wrong options format".to_string());
}
}
}
TokenTree::Literal(lit) if options_opened && expect_option_value => {
expect_option_value = false;
match current_option_name.take() {
Some(option_name) => {
options.insert(option_name, unquote_literal(&lit)?);
}
_ => {
return Err("Wrong options format".to_string());
}
}
}
TokenTree::Punct(punct) if options_opened && punct == '=' => {
expect_option_value = true;
}
TokenTree::Punct(punct) if options_opened && punct == ',' => {
expect_option_value = false;
}
TokenTree::Punct(punct) if options_opened && punct == ';' => {
expect_option_value = false;
}
others => {
return Err(format!(
"Unexpected input for struct path parameters: {:?}",
others
));
}
}
}
if !current_field_tokens.is_empty() {
current_struct_fields.push(std::mem::take(&mut current_field_tokens));
}
if current_struct_name_tokens.is_empty() {
return Err("Unexpected comma with empty definitions!".to_string());
}
if current_struct_fields.is_empty() {
return Err(format!(
"Unexpected comma with empty fields for {}!",
render_text(¤t_struct_name_tokens)
));
}
found_structs.push((current_struct_name_tokens, current_struct_fields));
let all_check_functions = generate_checks_code_for(&found_structs);
let mut all_final_fields: Vec<String> = Vec::with_capacity(16);
for (_, fields) in &found_structs {
for field_tokens in fields {
let mut final_field_path = render_text(field_tokens).replace('~', ".");
if !options.is_empty() {
final_field_path = apply_options(&options, final_field_path)?;
}
all_final_fields.push(format!("\"{}\"", final_field_path))
}
}
let fields_str = format!("[{}]", all_final_fields.join(","));
let fields_tokens: TokenStream = fields_str
.parse()
.map_err(|_| format!("Generated code failed to parse: {}", fields_str))?;
let mut block_contents = all_check_functions;
block_contents.extend(fields_tokens);
Ok(TokenStream::from(TokenTree::Group(Group::new(
Delimiter::Brace,
block_contents,
))))
}
fn fold_field_into_struct_name(
field_tokens: Vec<TokenTree>,
struct_name_tokens: &mut Vec<TokenTree>,
) -> bool {
let is_single_ident = matches!(field_tokens.as_slice(), [TokenTree::Ident(_)]);
if is_single_ident {
struct_name_tokens.extend("::".parse::<TokenStream>().expect("`::` always parses"));
struct_name_tokens.extend(field_tokens);
}
is_single_ident
}
fn render_text(tokens: &[TokenTree]) -> String {
tokens.iter().map(|token| token.to_string()).collect()
}
fn parse_multiple_fields(
group_stream: TokenStream,
found_struct_fields: &mut Vec<Vec<TokenTree>>,
) -> Result<(), String> {
let mut current_field_tokens: Vec<TokenTree> = Vec::new();
for token_tree in group_stream.into_iter() {
match token_tree {
TokenTree::Ident(id) => current_field_tokens.push(TokenTree::Ident(id)),
TokenTree::Punct(punct) if punct == ',' => {
if current_field_tokens.is_empty() {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
found_struct_fields.push(std::mem::take(&mut current_field_tokens));
}
TokenTree::Punct(punct) if punct == '.' => {
if current_field_tokens.is_empty() {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
current_field_tokens.push(TokenTree::Punct(punct));
}
others => {
return Err(format!(
"Unexpected input for struct path group parameters: {:?}",
others
));
}
}
}
if !current_field_tokens.is_empty() {
found_struct_fields.push(current_field_tokens);
}
Ok(())
}
fn try_parse_all_fields(struct_path_stream: &TokenStream) -> Option<Result<TokenStream, String>> {
let tokens: Vec<TokenTree> = struct_path_stream.clone().into_iter().collect();
if !tokens
.iter()
.any(|t| matches!(t, TokenTree::Punct(p) if *p == '*'))
{
return None;
}
Some(parse_all_fields_at(&tokens))
}
const ALL_FIELDS_OPTION_KEYS: &[&str] = &["visibility", "case"];
const CANNOT_COMBINE_STAR_MESSAGE: &str = "`Type::*` cannot be combined with another struct or field group: its length is unknown to the macro";
const EXPECTED_TYPE_PATH_MESSAGE: &str =
"expected a type path such as `Type::*` or `crate::module::Type::*`";
const NOTHING_AFTER_NESTED_STAR_MESSAGE: &str = "nothing may follow a nested `Type::*` inside the parentheses; put options after the closing `)`";
const CANNOT_COMBINE_NESTED_STAR_MESSAGE: &str = "a nested `Type::*` cannot be combined with another struct or field group: its length is unknown to the macro";
const NESTED_STAR_MISSING_FIELD_MESSAGE: &str = "expected a field name before `.(` / `~(`";
const NESTED_STAR_INNER_EXPECTED_MESSAGE: &str = "inside the parens, expected `Type::*`";
fn try_parse_nested_star(tokens: &[TokenTree]) -> Option<Result<TokenStream, String>> {
let paren_idx = tokens.iter().enumerate().find_map(|(i, t)| {
let group = match t {
TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => g,
_ => return None,
};
let follows_connector = i
.checked_sub(1)
.is_some_and(|j| matches!(&tokens[j], TokenTree::Punct(p) if *p == '.' || *p == '~'));
let contains_star = group
.stream()
.into_iter()
.any(|t| matches!(t, TokenTree::Punct(p) if p == '*'));
(follows_connector || contains_star).then_some(i)
})?;
Some(parse_nested_star_at(tokens, paren_idx))
}
fn parse_nested_star_at(tokens: &[TokenTree], paren_idx: usize) -> Result<TokenStream, String> {
let prefix_region = &tokens[..paren_idx];
if contains_top_level_comma(prefix_region) {
return Err(CANNOT_COMBINE_NESTED_STAR_MESSAGE.to_string());
}
if prefix_region
.iter()
.any(|t| matches!(t, TokenTree::Group(g) if g.delimiter() == Delimiter::Brace))
{
return Err(
"a `{ ... }` field group cannot appear before a nested `Type::*`; use a plain field path such as `Parent::child.(Child::*)`".to_string(),
);
}
let connector = match paren_idx.checked_sub(1).map(|i| &tokens[i]) {
Some(TokenTree::Punct(p)) if *p == '.' || *p == '~' => p.clone(),
_ => return Err(NESTED_STAR_MISSING_FIELD_MESSAGE.to_string()),
};
let head = &prefix_region[..prefix_region.len() - 1];
let (parent_tokens, field_tokens) = parse_type_and_field_path(head)?;
if field_tokens.is_empty() {
return Err(
"expected a field before the nested `Type::*`, e.g. `Parent::child.(Child::*)`"
.to_string(),
);
}
if !matches!(field_tokens.last(), Some(TokenTree::Ident(_))) {
return Err(
"expected a field name immediately before the nested `Type::*`'s connector, e.g. `Parent::child.(Child::*)`".to_string(),
);
}
let paren_group = match &tokens[paren_idx] {
TokenTree::Group(g) => g,
_ => unreachable!(),
};
let inner_tokens: Vec<TokenTree> = paren_group.stream().into_iter().collect();
if inner_tokens
.iter()
.any(|t| matches!(t, TokenTree::Group(_)))
{
return Err("nesting a parenthesized group inside `Type::*` is not supported".to_string());
}
let (inner_type_tokens, star_span) = parse_bare_star_type(&inner_tokens)?;
let rest = &tokens[paren_idx + 1..];
let options = match rest {
[] => Vec::new(),
[TokenTree::Punct(p), remainder @ ..] if *p == ';' => parse_all_fields_options(remainder)?,
[TokenTree::Punct(p)] if *p == ',' => return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string()),
_ => return Err(CANNOT_COMBINE_NESTED_STAR_MESSAGE.to_string()),
};
const NESTED_STAR_OPTION_KEYS: &[&str] = &["visibility", "case", "delim"];
if let Some((unknown, _)) = options
.iter()
.find(|(key, _)| !NESTED_STAR_OPTION_KEYS.contains(&key.as_str()))
{
return Err(format!("Unknown option is specified: {}", unknown));
}
let (visibility_suffix, case_suffix) = names_const_suffixes(&options)?;
let options_map: HashMap<String, String> = options.into_iter().collect();
let raw_prefix = render_text(&field_tokens).replace('~', ".");
let prefix_string = apply_options(&options_map, raw_prefix)?;
let delim_string = options_map
.get("delim")
.cloned()
.unwrap_or_else(|| ".".to_string());
let inner_type_stream: TokenStream = inner_type_tokens.iter().cloned().collect();
let parent_stream: TokenStream = parent_tokens.iter().cloned().collect();
let check_template = if connector == '~' {
"let _: &INNER = SELF.FIELD.iter().next().unwrap();"
} else {
"let _: &INNER = &SELF.FIELD;"
};
let self_param_tokens: TokenStream =
TokenStream::from(TokenTree::Ident(Ident::new("t", star_span)));
let check = fill(
check_template,
Span::call_site(),
&[
("SELF", &self_param_tokens),
("INNER", &inner_type_stream),
("FIELD", &field_check_tokens(&field_tokens)),
],
);
let names_path = format!(
"INNER::__STRUCT_PATH_{}_FIELDS{}",
visibility_suffix, case_suffix
);
let names_tokens = fill(&names_path, star_span, &[("INNER", &inner_type_stream)]);
let prefix_tokens = quoted_string_tokens(&prefix_string, star_span)?;
let delim_tokens = quoted_string_tokens(&delim_string, star_span)?;
Ok(fill(
"{
const _: fn(&PARENT) = |t: &PARENT| { CHECK };
const __STRUCT_PATH_NAMES: [&str; NAMES.len()] = NAMES;
const __STRUCT_PATH_LEN: usize =
INNER::__struct_path_joined_len(PREFIX, DELIM, &__STRUCT_PATH_NAMES);
const __STRUCT_PATH_BYTES: [u8; __STRUCT_PATH_LEN] =
INNER::__struct_path_joined_bytes::<__STRUCT_PATH_LEN>(PREFIX, DELIM, &__STRUCT_PATH_NAMES);
const __STRUCT_PATH_OUT: [&str; __STRUCT_PATH_NAMES.len()] =
INNER::__struct_path_joined(&__STRUCT_PATH_BYTES, PREFIX, DELIM, &__STRUCT_PATH_NAMES);
__STRUCT_PATH_OUT
}",
star_span,
&[
("PARENT", &parent_stream),
("CHECK", &check),
("INNER", &inner_type_stream),
("PREFIX", &prefix_tokens),
("DELIM", &delim_tokens),
("NAMES", &names_tokens),
],
))
}
fn parse_type_and_field_path(
tokens: &[TokenTree],
) -> Result<(Vec<TokenTree>, Vec<TokenTree>), String> {
let mut struct_name_tokens: Vec<TokenTree> = Vec::new();
let mut field_tokens: Vec<TokenTree> = Vec::new();
let mut opened_struct = false;
let mut colons_counter = 0;
for token_tree in tokens {
match token_tree.clone() {
TokenTree::Ident(id) if struct_name_tokens.is_empty() => {
struct_name_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !struct_name_tokens.is_empty()
&& !opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
if colons_counter > 1 {
opened_struct = true;
}
}
TokenTree::Ident(id) if opened_struct => {
colons_counter = 0;
field_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !struct_name_tokens.is_empty()
&& opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
opened_struct = false;
let taken = std::mem::take(&mut field_tokens);
if !fold_field_into_struct_name(taken, &mut struct_name_tokens) {
return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string());
}
}
TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
if field_tokens.is_empty() {
return Err(NESTED_STAR_MISSING_FIELD_MESSAGE.to_string());
}
field_tokens.push(TokenTree::Punct(punct));
}
_ => return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string()),
}
}
if struct_name_tokens.is_empty() || !opened_struct {
return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string());
}
Ok((struct_name_tokens, field_tokens))
}
fn split_star_type(tokens: &[TokenTree]) -> Result<(&[TokenTree], Span, &[TokenTree]), String> {
let star_pos = tokens
.iter()
.position(|t| matches!(t, TokenTree::Punct(p) if *p == '*'))
.ok_or_else(|| EXPECTED_TYPE_PATH_MESSAGE.to_string())?;
if star_pos < 2
|| !matches!(&tokens[star_pos - 1], TokenTree::Punct(p) if *p == ':')
|| !matches!(&tokens[star_pos - 2], TokenTree::Punct(p) if *p == ':')
{
return Err("`*` must directly follow `Type::`".to_string());
}
let star_span = match &tokens[star_pos] {
TokenTree::Punct(p) => p.span(),
_ => unreachable!(),
};
Ok((&tokens[..star_pos - 2], star_span, &tokens[star_pos + 1..]))
}
fn is_self_type_path(tokens: &[TokenTree]) -> bool {
matches!(tokens.first(), Some(TokenTree::Ident(id)) if id.to_string() == "Self")
}
fn parse_bare_star_type(tokens: &[TokenTree]) -> Result<(Vec<TokenTree>, Span), String> {
if !tokens
.iter()
.any(|t| matches!(t, TokenTree::Punct(p) if *p == '*'))
{
return Err(NESTED_STAR_INNER_EXPECTED_MESSAGE.to_string());
}
let (type_tokens, star_span, rest) = split_star_type(tokens)?;
if type_tokens.is_empty() || !is_bare_type_path(type_tokens) {
return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string());
}
if is_self_type_path(type_tokens) {
return Err(
"`Self` cannot be named inside a nested `Type::*`; name the type directly, e.g. `Node::next~(Node::*)`".to_string(),
);
}
if !rest.is_empty() {
return Err(NOTHING_AFTER_NESTED_STAR_MESSAGE.to_string());
}
Ok((type_tokens.to_vec(), star_span))
}
fn quoted_string_tokens(value: &str, span: Span) -> Result<TokenStream, String> {
let text = format!("\"{}\"", value);
let tokens: TokenStream = text
.parse()
.map_err(|_| format!("Generated code failed to parse: {}", text))?;
Ok(tokens
.into_iter()
.map(|mut token| {
token.set_span(span);
token
})
.collect())
}
fn parse_all_fields_at(tokens: &[TokenTree]) -> Result<TokenStream, String> {
let (type_tokens, star_span, rest) = split_star_type(tokens)?;
if type_tokens.is_empty() {
return Err("Unexpected `*` with an empty type!".to_string());
}
if !is_bare_type_path(type_tokens) {
return Err(if contains_top_level_comma(type_tokens) {
CANNOT_COMBINE_STAR_MESSAGE.to_string()
} else {
EXPECTED_TYPE_PATH_MESSAGE.to_string()
});
}
let options = match rest {
[] => Vec::new(),
[TokenTree::Punct(p), remainder @ ..] if *p == ';' => parse_all_fields_options(remainder)?,
[TokenTree::Punct(p), more @ ..] if *p == ',' && !more.is_empty() => {
return Err(CANNOT_COMBINE_STAR_MESSAGE.to_string())
}
_ => return Err(EXPECTED_TYPE_PATH_MESSAGE.to_string()),
};
if let Some((unknown, _)) = options
.iter()
.find(|(key, _)| !ALL_FIELDS_OPTION_KEYS.contains(&key.as_str()))
{
return Err(format!("Unknown option is specified: {}", unknown));
}
let (visibility_suffix, case_suffix) = names_const_suffixes(&options)?;
let const_path = format!(
"TYPE::__STRUCT_PATH_{}_FIELDS{}",
visibility_suffix, case_suffix
);
let type_stream: TokenStream = type_tokens.iter().cloned().collect();
Ok(fill(&const_path, star_span, &[("TYPE", &type_stream)]))
}
fn names_const_suffixes(
options: &[(String, String)],
) -> Result<(&'static str, &'static str), String> {
let option_value = |name: &str| {
options
.iter()
.find(|(key, _)| key == name)
.map(|(_, value)| value.as_str())
};
let visibility_suffix = match option_value("visibility") {
None | Some("pub") => "PUB",
Some("all") => "ALL",
Some(other) => return Err(format!("Unknown visibility is specified: {}", other)),
};
let case_suffix = match option_value("case") {
None => "",
Some("camel") => "_CAMEL",
Some("pascal") => "_PASCAL",
Some(other) => return Err(format!("Unknown case is specified: {}", other)),
};
Ok((visibility_suffix, case_suffix))
}
fn contains_top_level_comma(tokens: &[TokenTree]) -> bool {
split_top_level_commas(tokens).len() > 1
}
fn is_bare_type_path(tokens: &[TokenTree]) -> bool {
let mut i = 0;
if matches!(tokens.first(), Some(TokenTree::Punct(p)) if *p == ':')
&& matches!(tokens.get(1), Some(TokenTree::Punct(p)) if *p == ':')
{
i = 2;
}
loop {
if !matches!(tokens.get(i), Some(TokenTree::Ident(_))) {
return false;
}
i += 1;
if i == tokens.len() {
return true;
}
let is_double_colon = matches!(tokens.get(i), Some(TokenTree::Punct(p)) if *p == ':')
&& matches!(tokens.get(i + 1), Some(TokenTree::Punct(p)) if *p == ':');
if !is_double_colon {
return false;
}
i += 2;
}
}
enum OptionParseState {
GroupStart,
Key,
Equals,
Value,
CommaOrEnd,
}
fn parse_all_fields_options(tokens: &[TokenTree]) -> Result<Vec<(String, String)>, String> {
let mut options: Vec<(String, String)> = Vec::new();
let mut pending_key: Option<String> = None;
let mut state = OptionParseState::GroupStart;
for token_tree in tokens {
state = match (state, token_tree) {
(OptionParseState::GroupStart | OptionParseState::Key, TokenTree::Ident(id)) => {
pending_key = Some(id.to_string());
OptionParseState::Equals
}
(OptionParseState::Equals, TokenTree::Punct(p)) if *p == '=' => OptionParseState::Value,
(OptionParseState::Value, TokenTree::Literal(lit)) => {
let key = pending_key.take().expect("set on entering Equals/Value");
if options.iter().any(|(existing, _)| existing == &key) {
return Err(format!("Duplicate option is specified: {}", key));
}
options.push((key, unquote_literal(lit)?));
OptionParseState::CommaOrEnd
}
(OptionParseState::CommaOrEnd, TokenTree::Punct(p)) if *p == ',' => {
OptionParseState::Key
}
(OptionParseState::CommaOrEnd | OptionParseState::GroupStart, TokenTree::Punct(p))
if *p == ';' =>
{
OptionParseState::GroupStart
}
(state, other) => {
let expected = match state {
OptionParseState::GroupStart | OptionParseState::Key => "an option name",
OptionParseState::Equals => "`=`",
OptionParseState::Value => "a quoted string (\"...\") or char ('.') literal",
OptionParseState::CommaOrEnd => "`,` or `;`",
};
return Err(format!(
"Expected {} in `Type::*` options, found `{}`",
expected, other
));
}
};
}
match state {
OptionParseState::GroupStart | OptionParseState::Key | OptionParseState::CommaOrEnd => {
Ok(options)
}
OptionParseState::Equals | OptionParseState::Value => Err(format!(
"Missing a value for option `{}`",
pending_key.expect("set on entering Equals/Value")
)),
}
}
#[proc_macro]
pub fn path(struct_path_stream: TokenStream) -> TokenStream {
match path_impl(struct_path_stream) {
Ok(stream) => stream,
Err(message) => compile_error_for(message),
}
}
fn contains_star(stream: TokenStream) -> bool {
stream.into_iter().any(|token| match token {
TokenTree::Punct(p) if p == '*' => true,
TokenTree::Group(g) => contains_star(g.stream()),
_ => false,
})
}
fn path_impl(struct_path_stream: TokenStream) -> Result<TokenStream, String> {
if contains_star(struct_path_stream.clone()) {
return Err(
"`*` for all declared fields is only supported by paths!, not path!".to_string(),
);
}
let mut current_struct_name_tokens: Vec<TokenTree> = Vec::new();
let mut opened_struct = false;
let mut colons_counter = 0;
let mut options_opened = false;
let mut current_field_tokens: Vec<TokenTree> = Vec::new();
let mut current_option_name: Option<String> = None;
let mut expect_option_value: bool = false;
let mut options: HashMap<String, String> = HashMap::new();
let mut found_structs: FoundStructs = Vec::new();
for token_tree in struct_path_stream.into_iter() {
match token_tree {
TokenTree::Ident(id) if current_struct_name_tokens.is_empty() => {
current_struct_name_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !current_struct_name_tokens.is_empty()
&& !opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
if colons_counter > 1 {
opened_struct = true;
}
}
TokenTree::Ident(id) if opened_struct => {
colons_counter = 0;
current_field_tokens.push(TokenTree::Ident(id));
}
TokenTree::Punct(punct)
if !current_struct_name_tokens.is_empty()
&& opened_struct
&& punct == ':'
&& colons_counter < 2 =>
{
colons_counter += 1;
opened_struct = false;
let field_tokens = std::mem::take(&mut current_field_tokens);
if !fold_field_into_struct_name(field_tokens, &mut current_struct_name_tokens) {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
}
TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
if current_field_tokens.is_empty() {
return Err(format!(
"Unexpected punctuation input for struct path group parameters: {:?}",
punct
));
}
current_field_tokens.push(TokenTree::Punct(punct));
}
TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
opened_struct = false;
colons_counter = 0;
if current_struct_name_tokens.is_empty() {
return Err("Unexpected comma with empty definitions!".to_string());
}
if current_field_tokens.is_empty() {
return Err(format!(
"Unexpected comma with empty fields for {}!",
render_text(¤t_struct_name_tokens)
));
}
found_structs.push((
std::mem::take(&mut current_struct_name_tokens),
vec![std::mem::take(&mut current_field_tokens)],
));
}
TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
options_opened = true;
opened_struct = false;
}
TokenTree::Ident(id) if options_opened && !expect_option_value => {
current_option_name = Some(id.to_string())
}
TokenTree::Ident(id) if options_opened && expect_option_value => {
expect_option_value = false;
match current_option_name.take() {
Some(option_name) => {
options.insert(option_name, id.to_string());
}
_ => {
return Err("Wrong options format".to_string());
}
}
}
TokenTree::Literal(lit) if options_opened && expect_option_value => {
expect_option_value = false;
match current_option_name.take() {
Some(option_name) => {
options.insert(option_name, unquote_literal(&lit)?);
}
_ => {
return Err("Wrong options format".to_string());
}
}
}
TokenTree::Punct(punct) if options_opened && punct == '=' => {
expect_option_value = true;
}
TokenTree::Punct(punct) if options_opened && punct == ',' => {
expect_option_value = false;
}
TokenTree::Punct(punct) if options_opened && punct == ';' => {
expect_option_value = false;
}
others => {
return Err(format!(
"Unexpected input for struct path parameters: {:?}",
others
));
}
}
}
if !current_struct_name_tokens.is_empty() && !current_field_tokens.is_empty() {
found_structs.push((current_struct_name_tokens, vec![current_field_tokens]));
}
if found_structs.is_empty() {
return Err("Unexpected empty path definition!".to_string());
}
let all_check_functions = generate_checks_code_for(&found_structs);
let full_field_path = found_structs
.iter()
.flat_map(|(_, fields)| fields.iter())
.map(|tokens| render_text(tokens))
.collect::<Vec<_>>()
.join(".")
.replace('~', ".");
let final_field_path = apply_options(&options, full_field_path)?;
let fields_str = format!("\"{}\"", final_field_path);
let fields_tokens: TokenStream = fields_str
.parse()
.map_err(|_| format!("Generated code failed to parse: {}", fields_str))?;
let mut block_contents = all_check_functions;
block_contents.extend(fields_tokens);
Ok(TokenStream::from(TokenTree::Group(Group::new(
Delimiter::Brace,
block_contents,
))))
}
fn generate_checks_code_for(found_structs: &FoundStructs) -> TokenStream {
let mut all_check_functions = TokenStream::new();
for (struct_name_tokens, struct_fields) in found_structs {
let struct_name: TokenStream = struct_name_tokens.iter().cloned().collect();
for field_tokens in struct_fields {
all_check_functions.extend(fill(
"const _: fn(&STRUCT) = |t: &STRUCT| { let _ = &t.FIELD; };",
Span::call_site(),
&[
("STRUCT", &struct_name),
("FIELD", &field_check_tokens(field_tokens)),
],
));
}
}
all_check_functions
}
fn field_check_tokens(field_tokens: &[TokenTree]) -> TokenStream {
field_tokens
.iter()
.map(|token| match token {
TokenTree::Punct(punct) if *punct == '~' => {
fill(".iter().next().unwrap().", punct.span(), &[])
}
token => token.clone().into(),
})
.collect()
}
fn fill(template: &str, span: Span, substitutions: &[(&str, &TokenStream)]) -> TokenStream {
fn walk(stream: TokenStream, span: Span, subs: &[(&str, &TokenStream)]) -> TokenStream {
stream
.into_iter()
.map(|mut token| {
match &token {
TokenTree::Ident(ident) => {
let name = ident.to_string();
if let Some((_, tokens)) = subs.iter().find(|(n, _)| *n == name) {
return (*tokens).clone();
}
}
TokenTree::Group(group) => {
let stream = walk(group.stream(), span, subs);
token = TokenTree::Group(Group::new(group.delimiter(), stream));
}
_ => {}
}
token.set_span(span);
token.into()
})
.collect()
}
walk(
template.parse().expect("templates are valid tokens"),
span,
substitutions,
)
}
fn unquote_literal(lit: &proc_macro::Literal) -> Result<String, String> {
let text = lit.to_string();
let bytes = text.as_bytes();
let is_quoted = bytes.len() >= 2
&& ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
|| (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''));
if is_quoted {
Ok(text[1..text.len() - 1].to_string())
} else {
Err(format!(
"Unsupported option value {}: expected a string (\"...\") or char ('.') literal",
text
))
}
}
fn convert_case_by_name(field_name: &str, case_name: &str) -> Result<String, String> {
match case_name {
"camel" => Ok(field_name.from_case(Case::Snake).to_case(Case::Camel)),
"pascal" => Ok(field_name.from_case(Case::Snake).to_case(Case::Pascal)),
another => Err(format!("Unknown case is specified: {}", another)),
}
}
fn convert_all(names: &[String], case_name: &str) -> Vec<String> {
names
.iter()
.map(|n| {
convert_case_by_name(n, case_name)
.unwrap_or_else(|_| panic!("{:?} is always a known case", case_name))
})
.collect()
}
fn apply_options(options: &HashMap<String, String>, field_path: String) -> Result<String, String> {
let delim = options
.get("delim")
.as_ref()
.map(|s| s.as_str())
.unwrap_or(".");
let case = options.get("case");
let segments = field_path
.split('.')
.map(|field_name| match case {
Some(case_value) => convert_case_by_name(field_name, case_value),
None => Ok(field_name.to_string()),
})
.collect::<Result<Vec<String>, String>>()?;
Ok(segments.join(delim))
}
#[proc_macro_derive(StructPath)]
pub fn derive_struct_path(input: TokenStream) -> TokenStream {
match derive_struct_path_impl(input) {
Ok(stream) => stream,
Err(error_stream) => error_stream,
}
}
struct DerivedField {
name: String,
is_plain_pub: bool,
}
fn derive_struct_path_impl(input: TokenStream) -> Result<TokenStream, TokenStream> {
let tokens: Vec<TokenTree> = input.into_iter().collect();
let mut pos = 0;
skip_attributes(&tokens, &mut pos);
let visibility_start = pos;
let (vis_len, item_is_plain_pub) = parse_visibility_prefix(&tokens[pos..]);
pos += vis_len;
let visibility_tokens: TokenStream = tokens[visibility_start..pos].iter().cloned().collect();
let (keyword, keyword_span) = match tokens.get(pos) {
Some(TokenTree::Ident(id)) => (id.to_string(), id.span()),
_ => {
return Err(compile_error_at(
"StructPath: unable to parse the derived item",
Span::call_site(),
))
}
};
if keyword != "struct" {
return Err(compile_error_at(
&format!(
"StructPath supports structs with named fields only, not {}s",
keyword
),
keyword_span,
));
}
pos += 1;
let name_ident = match tokens.get(pos) {
Some(TokenTree::Ident(id)) => id.clone(),
_ => {
return Err(compile_error_at(
"StructPath: expected a struct name",
keyword_span,
))
}
};
let name_span = name_ident.span();
pos += 1;
let (generics_impl, generics_type) = parse_lifetime_generics(&tokens, &mut pos)?;
let (where_clause, fields_group) = find_fields_group(&tokens[pos..])?;
let fields = parse_derived_fields(fields_group.stream())?;
let pub_names: Vec<String> = fields
.iter()
.filter(|f| f.is_plain_pub)
.map(|f| f.name.clone())
.collect();
let all_names: Vec<String> = fields.iter().map(|f| f.name.clone()).collect();
let name_tokens = TokenStream::from(TokenTree::Ident(name_ident));
let pub_array = array_literal_tokens(&pub_names);
let pub_camel_array = array_literal_tokens(&convert_all(&pub_names, "camel"));
let pub_pascal_array = array_literal_tokens(&convert_all(&pub_names, "pascal"));
let all_array = array_literal_tokens(&all_names);
let all_camel_array = array_literal_tokens(&convert_all(&all_names, "camel"));
let all_pascal_array = array_literal_tokens(&convert_all(&all_names, "pascal"));
let n_pub: TokenStream = pub_names
.len()
.to_string()
.parse()
.expect("a field count always parses as a literal");
let n_all: TokenStream = all_names
.len()
.to_string()
.parse()
.expect("a field count always parses as a literal");
let all_fields_visibility: TokenStream = if item_is_plain_pub {
"pub(crate)".parse().expect("`pub(crate)` always parses")
} else {
visibility_tokens.clone()
};
let generated = fill(
"impl<GENERICS_IMPL> NAME<GENERICS_TYPE> WHERE_CLAUSE {
#[doc(hidden)]
VIS const __STRUCT_PATH_PUB_FIELDS: [&'static str; N_PUB] = PUB_FIELDS;
#[doc(hidden)]
VIS const __STRUCT_PATH_PUB_FIELDS_CAMEL: [&'static str; N_PUB] = PUB_FIELDS_CAMEL;
#[doc(hidden)]
VIS const __STRUCT_PATH_PUB_FIELDS_PASCAL: [&'static str; N_PUB] = PUB_FIELDS_PASCAL;
#[doc(hidden)]
ALL_VIS const __STRUCT_PATH_ALL_FIELDS: [&'static str; N_ALL] = ALL_FIELDS;
#[doc(hidden)]
ALL_VIS const __STRUCT_PATH_ALL_FIELDS_CAMEL: [&'static str; N_ALL] = ALL_FIELDS_CAMEL;
#[doc(hidden)]
ALL_VIS const __STRUCT_PATH_ALL_FIELDS_PASCAL: [&'static str; N_ALL] = ALL_FIELDS_PASCAL;
#[doc(hidden)]
#[must_use]
VIS const fn __struct_path_joined_len(prefix: &str, delim: &str, names: &[&str]) -> usize {
let mut total = 0;
let mut i = 0;
while i < names.len() {
total += prefix.len() + delim.len() + names[i].len();
i += 1;
}
total
}
#[doc(hidden)]
#[must_use]
VIS const fn __struct_path_joined_bytes<const TOTAL: usize>(
prefix: &str,
delim: &str,
names: &[&str],
) -> [u8; TOTAL] {
let mut buf = [0u8; TOTAL];
let mut pos = 0;
let mut i = 0;
while i < names.len() {
let parts = [prefix.as_bytes(), delim.as_bytes(), names[i].as_bytes()];
let mut p = 0;
while p < parts.len() {
let mut b = 0;
while b < parts[p].len() {
buf[pos] = parts[p][b];
pos += 1;
b += 1;
}
p += 1;
}
i += 1;
}
buf
}
#[doc(hidden)]
#[must_use]
VIS const fn __struct_path_joined<const N: usize, const TOTAL: usize>(
bytes: &'static [u8; TOTAL],
prefix: &str,
delim: &str,
names: &[&str; N],
) -> [&'static str; N] {
let mut out = [\"\"; N];
let mut pos = 0;
let mut i = 0;
while i < N {
let len = prefix.len() + delim.len() + names[i].len();
let (_, rest) = bytes.split_at(pos);
let (piece, _) = rest.split_at(len);
out[i] = match core::str::from_utf8(piece) {
Ok(s) => s,
Err(_) => panic!(\"struct-path: joined path is not valid UTF-8\"),
};
pos += len;
i += 1;
}
out
}
}",
name_span,
&[
("GENERICS_IMPL", &generics_impl),
("GENERICS_TYPE", &generics_type),
("NAME", &name_tokens),
("WHERE_CLAUSE", &where_clause),
("VIS", &visibility_tokens),
("ALL_VIS", &all_fields_visibility),
("N_PUB", &n_pub),
("N_ALL", &n_all),
("PUB_FIELDS", &pub_array),
("PUB_FIELDS_CAMEL", &pub_camel_array),
("PUB_FIELDS_PASCAL", &pub_pascal_array),
("ALL_FIELDS", &all_array),
("ALL_FIELDS_CAMEL", &all_camel_array),
("ALL_FIELDS_PASCAL", &all_pascal_array),
],
);
Ok(generated)
}
fn skip_attributes(tokens: &[TokenTree], pos: &mut usize) {
while matches!(tokens.get(*pos), Some(TokenTree::Punct(p)) if *p == '#') {
*pos += 1;
if matches!(tokens.get(*pos), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket)
{
*pos += 1;
}
}
}
fn parse_visibility_prefix(tokens: &[TokenTree]) -> (usize, bool) {
if let Some(TokenTree::Group(g)) = tokens.first() {
if g.delimiter() == Delimiter::None {
let inner: Vec<TokenTree> = g.stream().into_iter().collect();
let (_, is_plain_pub) = parse_visibility_prefix(&inner);
return (1, is_plain_pub);
}
}
if matches!(tokens.first(), Some(TokenTree::Ident(id)) if id.to_string() == "pub") {
if matches!(tokens.get(1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
{
return (2, false);
}
return (1, true);
}
(0, false)
}
fn parse_field_visibility(tokens: &[TokenTree], pos: &mut usize) -> bool {
let (len, is_plain_pub) = parse_visibility_prefix(&tokens[*pos..]);
*pos += len;
is_plain_pub
}
fn split_top_level_commas(tokens: &[TokenTree]) -> Vec<Vec<TokenTree>> {
let mut segments = Vec::new();
let mut current = Vec::new();
let mut depth: i32 = 0;
let mut prev_was_arrow_dash = false;
for token in tokens {
match token {
TokenTree::Punct(p) if *p == '<' => {
depth += 1;
current.push(token.clone());
}
TokenTree::Punct(p) if *p == '>' => {
if !prev_was_arrow_dash {
depth = (depth - 1).max(0);
}
current.push(token.clone());
}
TokenTree::Punct(p) if *p == ',' && depth == 0 => {
segments.push(std::mem::take(&mut current));
}
other => current.push(other.clone()),
}
prev_was_arrow_dash =
matches!(token, TokenTree::Punct(p) if *p == '-' && p.spacing() == Spacing::Joint);
}
if !current.is_empty() {
segments.push(current);
}
segments
}
fn lifetime_param_name(segment: &[TokenTree]) -> Option<Vec<TokenTree>> {
if segment.len() < 2 {
return None;
}
if !matches!(&segment[0], TokenTree::Punct(p) if *p == '\'') {
return None;
}
if !matches!(&segment[1], TokenTree::Ident(_)) {
return None;
}
let bare = segment[0..2].to_vec();
if segment.len() == 2 {
return Some(bare);
}
if !matches!(&segment[2], TokenTree::Punct(p) if *p == ':') {
return None;
}
if segment.len() == 3 {
return Some(bare);
}
let mut i = 3;
loop {
if i + 1 >= segment.len() {
return None;
}
let is_lifetime = matches!(&segment[i], TokenTree::Punct(p) if *p == '\'')
&& matches!(&segment[i + 1], TokenTree::Ident(_));
if !is_lifetime {
return None;
}
i += 2;
if i == segment.len() {
break;
}
if !matches!(&segment[i], TokenTree::Punct(p) if *p == '+') {
return None;
}
i += 1;
}
Some(bare)
}
fn parse_lifetime_generics(
tokens: &[TokenTree],
pos: &mut usize,
) -> Result<(TokenStream, TokenStream), TokenStream> {
if !matches!(tokens.get(*pos), Some(TokenTree::Punct(p)) if *p == '<') {
return Ok((TokenStream::new(), TokenStream::new()));
}
let open_span = match &tokens[*pos] {
TokenTree::Punct(p) => p.span(),
_ => unreachable!(),
};
*pos += 1;
let start = *pos;
let mut depth = 1;
while depth > 0 {
match tokens.get(*pos) {
Some(TokenTree::Punct(p)) if *p == '<' => depth += 1,
Some(TokenTree::Punct(p)) if *p == '>' => depth -= 1,
Some(_) => {}
None => {
return Err(compile_error_at(
"StructPath: unterminated generic parameter list",
open_span,
))
}
}
*pos += 1;
}
let generics_tokens = &tokens[start..*pos - 1];
let mut bare_lifetimes: Vec<TokenTree> = Vec::new();
for (i, segment) in split_top_level_commas(generics_tokens)
.into_iter()
.enumerate()
{
if i > 0 {
bare_lifetimes.extend(fill(",", open_span, &[]));
}
match lifetime_param_name(&segment) {
Some(bare) => bare_lifetimes.extend(bare),
None => {
let span = segment.first().map(|t| t.span()).unwrap_or(open_span);
return Err(compile_error_at(
"StructPath only supports lifetime generic parameters",
span,
));
}
}
}
let generics_impl: TokenStream = generics_tokens.iter().cloned().collect();
let generics_type: TokenStream = bare_lifetimes.into_iter().collect();
Ok((generics_impl, generics_type))
}
fn find_fields_group(remaining: &[TokenTree]) -> Result<(TokenStream, Group), TokenStream> {
match remaining.last() {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Brace => {
let where_clause: TokenStream =
remaining[..remaining.len() - 1].iter().cloned().collect();
Ok((where_clause, g.clone()))
}
Some(TokenTree::Punct(p)) if *p == ';' => match remaining.first() {
Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
Err(compile_error_at(
"StructPath supports structs with named fields only, not tuple structs",
g.span(),
))
}
_ => Err(compile_error_at(
"StructPath supports structs with named fields only, not unit structs",
p.span(),
)),
},
_ => Err(compile_error_at(
"StructPath: expected a struct body with named fields",
Span::call_site(),
)),
}
}
fn parse_derived_fields(fields_stream: TokenStream) -> Result<Vec<DerivedField>, TokenStream> {
let tokens: Vec<TokenTree> = fields_stream.into_iter().collect();
let mut fields = Vec::new();
for segment in split_top_level_commas(&tokens) {
let mut pos = 0;
skip_attributes(&segment, &mut pos);
let is_plain_pub = parse_field_visibility(&segment, &mut pos);
let name_ident = match segment.get(pos) {
Some(TokenTree::Ident(id)) => id,
_ => {
return Err(compile_error_at(
"StructPath: expected a field name",
segment
.first()
.map(|t| t.span())
.unwrap_or_else(Span::call_site),
))
}
};
let name = name_ident.to_string();
let name = name.strip_prefix("r#").unwrap_or(&name).to_string();
fields.push(DerivedField { name, is_plain_pub });
}
Ok(fields)
}
fn array_literal_tokens(values: &[String]) -> TokenStream {
let joined = values
.iter()
.map(|v| format!("\"{}\"", v))
.collect::<Vec<_>>()
.join(",");
format!("[{}]", joined)
.parse()
.expect("field names always produce a valid array literal")
}