use proc_macro2::{TokenStream, TokenTree};
use syn::spanned::Spanned;
use syn::visit::Visit;
use super::super::{
FaceSyntaxError, SyntaxLocation, location, path_to_string, split_top_level, syntax_error,
};
pub fn application_entries(source: &str) -> Result<Vec<(String, SyntaxLocation)>, FaceSyntaxError> {
let file = super::super::nesting::parse_file(source)?;
let mut entries = Vec::new();
let mut visitor = ApplicationVisitor {
entries: &mut entries,
error: None,
};
visitor.visit_file(&file);
if let Some(error) = visitor.error {
Err(error)
} else {
Ok(entries)
}
}
struct ApplicationVisitor<'a> {
entries: &'a mut Vec<(String, SyntaxLocation)>,
error: Option<FaceSyntaxError>,
}
impl<'ast> Visit<'ast> for ApplicationVisitor<'_> {
fn visit_item_macro(&mut self, item: &'ast syn::ItemMacro) {
if self.error.is_some() {
return;
}
let Some(segment) = item.mac.path.segments.last() else {
return;
};
if segment.ident != "application" {
syn::visit::visit_item_macro(self, item);
return;
}
let tokens = split_top_level(item.mac.tokens.clone());
if tokens.len() != 1 {
self.error = Some(syntax_error(
item.mac.span(),
"application! expects exactly `entry = <path>`",
));
return;
}
let mut tokens = tokens[0].clone().into_iter();
let Some(TokenTree::Ident(name)) = tokens.next() else {
self.error = Some(syntax_error(
item.mac.span(),
"application! entry must start with `entry`",
));
return;
};
if name != "entry"
|| !matches!(tokens.next(), Some(TokenTree::Punct(punct)) if punct.as_char() == '=')
{
self.error = Some(syntax_error(
name.span(),
"application! expects `entry = <path>`",
));
return;
}
let path = tokens.collect::<TokenStream>();
let Ok(path) = syn::parse2::<syn::Path>(path) else {
self.error = Some(syntax_error(
name.span(),
"application! entry must be a Rust path",
));
return;
};
self.entries
.push((path_to_string(&path), location(item.mac.span())));
}
}
#[cfg(test)]
mod tests {
use super::application_entries;
#[test]
fn application_entry_parser_ignores_comments_and_strings() {
let source = r#"
// application!(entry = crate::wrong)
const TEXT: &str = "application!(entry = crate::also_wrong)";
nichlink::application!(entry = crate::app::run);
"#;
let entries = application_entries(source).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].0, "crate::app::run");
assert_eq!(entries[0].1.line, 4);
}
#[test]
fn application_entry_parser_rejects_malformed_declarations() {
let error = application_entries("application!(crate::main)").unwrap_err();
assert!(!error.message.is_empty());
}
#[test]
fn application_entry_parser_keeps_duplicates_visible_to_the_build_policy() {
let source = "application!(entry = crate::main); application!(entry = crate::run);";
let entries = application_entries(source).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].0, "crate::main");
assert_eq!(entries[1].0, "crate::run");
}
}