use std::collections::{BTreeMap, BTreeSet};
use super::scan::{self, parsed};
pub fn derived_paths(attributes: &[syn::Attribute]) -> BTreeSet<String> {
attributes
.iter()
.filter(|attribute| attribute.path().is_ident("derive"))
.flat_map(|attribute| {
attribute
.parse_args_with(
syn::punctuated::Punctuated::<syn::Path, syn::Token![,]>::parse_terminated,
)
.unwrap_or_else(|error| panic!("a derive attribute parses: {error}"))
})
.map(|path| {
path.segments
.last()
.map(|segment| segment.ident.to_string())
.unwrap_or_else(|| panic!("a derive attribute names a trait"))
})
.collect()
}
pub fn declared_items(items: &[syn::Item]) -> Vec<&syn::Item> {
let mut found: Vec<&syn::Item> = Vec::new();
for item in items {
found.push(item);
let inner = match item {
syn::Item::Mod(module) => module.content.as_ref().map(|(_, declared)| declared),
_ => None,
};
found.extend(
inner
.into_iter()
.flat_map(|declared| declared_items(declared)),
);
}
found
}
pub fn declares_only(item: &syn::Item) -> bool {
match item {
syn::Item::Mod(module) => module.content.is_none(),
syn::Item::Use(_) => true,
_ => false,
}
}
pub fn item_label(item: &syn::Item) -> String {
match item {
syn::Item::Const(entry) => format!("const {}", entry.ident),
syn::Item::Enum(entry) => format!("enum {}", entry.ident),
syn::Item::ExternCrate(entry) => format!("extern crate {}", entry.ident),
syn::Item::Fn(entry) => format!("fn {}", entry.sig.ident),
syn::Item::ForeignMod(entry) => format!("extern block {}", scan::token_text(&entry.abi)),
syn::Item::Impl(entry) => format!("impl {}", scan::token_text(&entry.self_ty)),
syn::Item::Macro(entry) => format!("macro {}", scan::token_text(&entry.mac.path)),
syn::Item::Mod(entry) => format!("inline mod {}", entry.ident),
syn::Item::Static(entry) => format!("static {}", entry.ident),
syn::Item::Struct(entry) => format!("struct {}", entry.ident),
syn::Item::Trait(entry) => format!("trait {}", entry.ident),
syn::Item::TraitAlias(entry) => format!("trait alias {}", entry.ident),
syn::Item::Type(entry) => format!("type {}", entry.ident),
syn::Item::Union(entry) => format!("union {}", entry.ident),
syn::Item::Use(entry) => format!("use {}", scan::token_text(&entry.tree)),
other => scan::token_text(other),
}
}
pub fn declared_error_variants() -> BTreeSet<String> {
parsed("src/error.rs")
.items
.iter()
.find_map(|item| match item {
syn::Item::Enum(declared) if declared.ident == "GraphBuildError" => Some(declared),
_ => None,
})
.unwrap_or_else(|| panic!("src/error.rs declares no GraphBuildError enum"))
.variants
.iter()
.map(|variant| variant.ident.to_string())
.collect()
}
pub fn public_use_leaves(path: &str, root: Option<&str>) -> BTreeSet<String> {
parsed(path)
.items
.iter()
.filter_map(|item| match item {
syn::Item::Use(declared) if is_public(&declared.vis) => Some(&declared.tree),
_ => None,
})
.flat_map(|tree| use_leaves(tree, root))
.collect()
}
fn use_leaves(tree: &syn::UseTree, root: Option<&str>) -> Vec<String> {
match tree {
syn::UseTree::Path(path) => entered(path, root),
syn::UseTree::Group(group) => group
.items
.iter()
.flat_map(|inner| use_leaves(inner, root))
.collect(),
syn::UseTree::Name(name) => named_leaf(name.ident.to_string(), root),
syn::UseTree::Rename(rename) => named_leaf(rename.rename.to_string(), root),
syn::UseTree::Glob(_) => vec!["*".to_owned()],
}
}
fn entered(path: &syn::UsePath, root: Option<&str>) -> Vec<String> {
match root {
Some(name) if path.ident != name => Vec::new(),
_ => use_leaves(&path.tree, None),
}
}
fn named_leaf(name: String, root: Option<&str>) -> Vec<String> {
match root {
Some(_) => Vec::new(),
None => vec![name],
}
}
pub fn public_signatures(path: &str) -> Vec<String> {
let file = parsed(path);
declared_items(&file.items)
.into_iter()
.flat_map(declared_signatures)
.collect()
}
fn declared_signatures(item: &syn::Item) -> Vec<String> {
match item {
syn::Item::Impl(block) => impl_signatures(block),
syn::Item::Fn(function) if is_public(&function.vis) => {
vec![normalized(&scan::token_text(&function.sig))]
}
_ => Vec::new(),
}
}
fn impl_signatures(block: &syn::ItemImpl) -> Vec<String> {
let subject = subject_name(&block.self_ty);
block
.items
.iter()
.filter_map(|item| match item {
syn::ImplItem::Fn(function) if is_public(&function.vis) => Some(format!(
"{subject}::{}",
normalized(&scan::token_text(&function.sig)).replacen("fn ", "", 1)
)),
_ => None,
})
.collect()
}
fn normalized(text: &str) -> String {
let held = text.replace(" -> ", "\u{1}");
held.replace(" :: ", "::")
.replace(" :", ":")
.replace(" < ", "<")
.replace(" <", "<")
.replace(" > ", ">")
.replace(" >", ">")
.replace(" ,", ",")
.replace(" (", "(")
.replace("( ", "(")
.replace(" )", ")")
.replace("& ", "&")
.replace(",)", ")")
.replace('\u{1}', " -> ")
}
pub fn declared_fields(items: &[&syn::Item], name: &str) -> Vec<String> {
items
.iter()
.filter_map(|item| match item {
syn::Item::Struct(declared) if declared.ident == name => Some(declared),
_ => None,
})
.flat_map(|declared| declared.fields.iter())
.filter_map(|field| field.ident.as_ref().map(ToString::to_string))
.collect()
}
pub fn public_fields(items: &[&syn::Item], path: &str) -> Vec<String> {
items
.iter()
.filter_map(|item| match item {
syn::Item::Struct(declared) if is_public(&declared.vis) => Some(declared),
_ => None,
})
.flat_map(|declared| {
declared
.fields
.iter()
.filter(|field| is_public(&field.vis))
.map(move |field| {
let name = field
.ident
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "0".to_owned());
format!("{path}: {}.{name} is public", declared.ident)
})
})
.collect()
}
pub fn public_constructors(items: &[&syn::Item], path: &str) -> Vec<String> {
items
.iter()
.filter_map(|item| match item {
syn::Item::Impl(block) => Some(block),
_ => None,
})
.filter(|block| is_closed_type(block))
.flat_map(|block| block_constructors(block, path))
.collect()
}
fn block_constructors(block: &syn::ItemImpl, path: &str) -> Vec<String> {
let subject = subject_name(&block.self_ty);
block
.items
.iter()
.filter_map(|item| match item {
syn::ImplItem::Fn(function) if is_public(&function.vis) => Some(function),
_ => None,
})
.filter(|function| manufactures(&function.sig, &subject))
.map(|function| {
format!(
"{path}: {subject}::{} is a public constructor",
function.sig.ident
)
})
.collect()
}
fn manufactures(signature: &syn::Signature, subject: &str) -> bool {
let returned = match &signature.output {
syn::ReturnType::Default => return false,
syn::ReturnType::Type(_, declared) => scan::token_text(declared),
};
signature.receiver().is_none() && names_type(&returned, subject)
}
fn names_type(text: &str, subject: &str) -> bool {
text.split(|character: char| !character.is_alphanumeric() && character != '_')
.any(|token| token == subject || token == "Self")
}
fn subject_name(subject: &syn::Type) -> String {
let text = scan::token_text(subject);
text.split_whitespace()
.next()
.unwrap_or_else(|| panic!("an impl block states no subject type: {text:?}"))
.to_owned()
}
const CALLER_CONSTRUCTED: &[&str] = &[
"GraphAnalysis",
"GraphAnalysisLimits",
"GraphCache",
"GraphCacheLimits",
"GraphEdgeSelection",
"GraphLimits",
"GraphRecords",
];
fn is_closed_type(block: &syn::ItemImpl) -> bool {
let subject = subject_name(&block.self_ty);
!CALLER_CONSTRUCTED.iter().any(|allowed| subject == *allowed)
}
fn is_public(visibility: &syn::Visibility) -> bool {
matches!(visibility, syn::Visibility::Public(_))
}
pub fn declared_call_graph(path: &str) -> BTreeMap<String, BTreeSet<String>> {
let file = parsed(path);
let bodies = declared_bodies(&declared_items(&file.items));
let declared: BTreeSet<&str> = bodies.keys().map(String::as_str).collect();
bodies
.iter()
.map(|(name, body)| (name.clone(), calls(body, &declared)))
.collect()
}
fn declared_bodies(items: &[&syn::Item]) -> BTreeMap<String, String> {
let mut found: BTreeMap<String, String> = BTreeMap::new();
for item in items {
for (name, body) in item_bodies(item) {
found.entry(name).or_default().push_str(&body);
}
}
found
}
fn item_bodies(item: &syn::Item) -> Vec<(String, String)> {
match item {
syn::Item::Fn(function) => vec![compacted(&function.sig, &function.block)],
syn::Item::Impl(block) => block.items.iter().filter_map(impl_body).collect(),
_ => Vec::new(),
}
}
fn impl_body(item: &syn::ImplItem) -> Option<(String, String)> {
match item {
syn::ImplItem::Fn(method) => Some(compacted(&method.sig, &method.block)),
_ => None,
}
}
fn compacted(signature: &syn::Signature, body: &syn::Block) -> (String, String) {
(
signature.ident.to_string(),
scan::token_text(body).split_whitespace().collect(),
)
}
fn calls(body: &str, declared: &BTreeSet<&str>) -> BTreeSet<String> {
declared
.iter()
.filter(|name| is_called(body, name))
.map(|name| (*name).to_owned())
.collect()
}
fn is_called(body: &str, name: &str) -> bool {
let needle = format!("{name}(");
body.match_indices(&needle)
.any(|(at, _)| at == 0 || !is_name_char(body.as_bytes()[at - 1]))
}
fn is_name_char(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || byte == b'_'
}
pub fn recursive_functions(calls: &BTreeMap<String, BTreeSet<String>>) -> Vec<String> {
let mut remaining: BTreeSet<&str> = calls.keys().map(String::as_str).collect();
loop {
let settled: Vec<&str> = remaining
.iter()
.copied()
.filter(|name| reaches_none(calls, name, &remaining))
.collect();
match settled.is_empty() {
true => return remaining.iter().map(|name| (*name).to_owned()).collect(),
false => remaining.retain(|name| !settled.contains(name)),
}
}
}
fn reaches_none(
calls: &BTreeMap<String, BTreeSet<String>>,
name: &str,
remaining: &BTreeSet<&str>,
) -> bool {
calls
.get(name)
.into_iter()
.flatten()
.all(|called| !remaining.contains(called.as_str()))
}