use cargo_metadata::{Package, Target};
use std::fs::File;
use std::io::Read;
use std::{collections::HashMap, path::PathBuf};
use syn::parse_file;
use syn::visit::Visit;
use syn::ItemFn;
pub struct FncmdSubcmds {
map: HashMap<String, PathBuf>,
}
impl FncmdSubcmds {
pub fn iter(&self) -> impl Iterator<Item = (&String, &PathBuf)> { self.map.iter() }
}
fn is_subcommand(it: &str, of: &str) -> bool { it.len() > of.len() && it.starts_with(of) }
impl From<(&Target, &Package)> for FncmdSubcmds {
fn from((target, package): (&Target, &Package)) -> Self {
let name_toplevel = &target.name;
let targets = package.targets.iter().filter_map(|target| {
target.kind.iter().any(|k| k == "bin" || k == "example").then(|| target)
});
let mut map: HashMap<String, PathBuf> = targets
.filter_map(|target| {
let content = {
let mut file = File::open(&target.src_path).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
content
};
parse_file(&content).ok().and_then(|ast| {
Visitor::from(&ast).get_main_fncmd().is_some().then(|| {
(target.name.clone(), target.src_path.clone().into_std_path_buf())
})
})
})
.collect::<HashMap<_, _>>();
{
map.retain(|name, _| is_subcommand(name, name_toplevel));
let table = map.clone();
map.retain(|name, _| {
!table.iter().any(|(name_other, _)| is_subcommand(name, name_other))
});
}
FncmdSubcmds { map }
}
}
struct Visitor<'ast> {
functions: Vec<&'ast ItemFn>,
}
impl<'ast> From<&'ast syn::File> for Visitor<'ast> {
fn from(ast: &'ast syn::File) -> Self {
let mut visitor = Visitor { functions: Vec::new() };
visitor.visit_file(ast);
visitor
}
}
impl<'ast> Visit<'ast> for Visitor<'ast> {
fn visit_item_fn(&mut self, node: &'ast ItemFn) {
self.functions.push(node);
}
}
impl<'ast> Visitor<'ast> {
fn get_main_fncmd(&self) -> Option<&'ast ItemFn> {
self.functions
.iter()
.find(|&&function| {
function.sig.ident == "main"
&& matches!(function.vis, syn::Visibility::Public(_))
&& function
.attrs
.iter()
.any(|attr| {
attr.path.segments.len() <= 2
&& attr.path.segments.iter().all(|segment| segment.ident == "fncmd")
})
})
.copied()
}
}