use std::path::Path;
use anyhow::{Context, Result};
use syn::visit::Visit;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SourceRef {
pub path: String,
pub ignore: Vec<String>,
}
pub struct ScanResult {
pub install_sources: Vec<SourceRef>,
pub uninstall_sources: Vec<SourceRef>,
pub has_install_fn: bool,
pub has_uninstall_fn: bool,
}
pub fn scan_source_dir(src_dir: &Path) -> Result<ScanResult> {
let mut result = ScanResult {
install_sources: Vec::new(),
uninstall_sources: Vec::new(),
has_install_fn: false,
has_uninstall_fn: false,
};
for entry in walkdir::WalkDir::new(src_dir)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map(|x| x == "rs").unwrap_or(false))
{
let path = entry.path();
log::trace!("Scanning source file: {}", path.display());
let source = std::fs::read_to_string(path)
.with_context(|| format!("failed to read source file: {}", path.display()))?;
let file = match syn::parse_file(&source) {
Ok(f) => f,
Err(e) => {
log::warn!("Failed to parse {}: {e}", path.display());
continue;
}
};
let mut visitor = SourceVisitor {
install_sources: &mut result.install_sources,
uninstall_sources: &mut result.uninstall_sources,
has_install_fn: &mut result.has_install_fn,
has_uninstall_fn: &mut result.has_uninstall_fn,
current_fn: None,
};
visitor.visit_file(&file);
}
Ok(result)
}
struct SourceVisitor<'a> {
install_sources: &'a mut Vec<SourceRef>,
uninstall_sources: &'a mut Vec<SourceRef>,
has_install_fn: &'a mut bool,
has_uninstall_fn: &'a mut bool,
current_fn: Option<String>,
}
impl SourceVisitor<'_> {
fn push(&mut self, s: SourceRef) {
match self.current_fn.as_deref() {
Some("install") => merge_or_push(self.install_sources, s),
Some("uninstall") => merge_or_push(self.uninstall_sources, s),
_ => {
merge_or_push(self.install_sources, s.clone());
merge_or_push(self.uninstall_sources, s);
}
}
}
}
fn merge_or_push(list: &mut Vec<SourceRef>, new: SourceRef) {
if let Some(existing) = list.iter_mut().find(|r| r.path == new.path) {
for pat in new.ignore {
if !existing.ignore.contains(&pat) {
existing.ignore.push(pat);
}
}
} else {
list.push(new);
}
}
impl<'ast> Visit<'ast> for SourceVisitor<'_> {
fn visit_item_fn(&mut self, node: &'ast syn::ItemFn) {
let name = node.sig.ident.to_string();
if name == "install" {
*self.has_install_fn = true;
} else if name == "uninstall" {
*self.has_uninstall_fn = true;
}
let prev = self.current_fn.take();
self.current_fn = Some(name);
syn::visit::visit_item_fn(self, node);
self.current_fn = prev;
}
fn visit_macro(&mut self, node: &'ast syn::Macro) {
let name = node
.path
.segments
.last()
.map(|s| s.ident.to_string())
.unwrap_or_default();
if name == "source" {
if let Some(s) = parse_source_macro(node) {
self.push(s);
}
}
syn::visit::visit_macro(self, node);
}
}
fn parse_source_macro(mac: &syn::Macro) -> Option<SourceRef> {
let args = mac
.parse_body_with(syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated)
.ok()?;
let mut iter = args.iter();
let path = match iter.next()? {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) => s.value(),
_ => return None,
};
let mut out = SourceRef {
path,
ignore: Vec::new(),
};
for arg in iter {
if let syn::Expr::Assign(syn::ExprAssign { left, right, .. }) = arg {
let key = match left.as_ref() {
syn::Expr::Path(p) if p.path.segments.len() == 1 => {
p.path.segments[0].ident.to_string()
}
_ => {
log::warn!("source!: ignoring unrecognized option form");
continue;
}
};
match key.as_str() {
"ignore" => {
if let Some(items) = extract_str_array(right) {
out.ignore = items;
} else {
log::warn!(
"source!({:?}, ignore = ...): value must be an array of string literals",
out.path
);
}
}
_ => log::warn!(
"source!({:?}, {key} = ...): unknown option, ignoring",
out.path
),
}
}
}
Some(out)
}
fn extract_str_array(expr: &syn::Expr) -> Option<Vec<String>> {
if let syn::Expr::Array(arr) = expr {
let mut out = Vec::with_capacity(arr.elems.len());
for el in &arr.elems {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = el
{
out.push(s.value());
} else {
return None;
}
}
Some(out)
} else {
None
}
}