use std::collections::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()
}
const SNAKE_CASE: &str = "rename_all = \"snake_case\"";
pub fn serialized_tags(path: &str, name: &str) -> BTreeSet<String> {
let file = parsed(path);
let declared = declared_items(&file.items)
.into_iter()
.find_map(|item| match item {
syn::Item::Enum(found) if found.ident == name => Some(found),
_ => None,
})
.unwrap_or_else(|| panic!("{path} declares no {name} enum"));
let stated: Vec<String> = declared.attrs.iter().map(scan::token_text).collect();
assert!(
stated
.iter()
.any(|attribute| attribute.contains(SNAKE_CASE)),
"{name} must state {SNAKE_CASE}, stating {stated:?}"
);
let renamed: Vec<String> = declared
.variants
.iter()
.filter(|variant| {
variant
.attrs
.iter()
.any(|attribute| attribute.path().is_ident("serde"))
})
.map(|variant| variant.ident.to_string())
.collect();
assert!(
renamed.is_empty(),
"{name} renames a variant of its own, which this reader does not apply: {renamed:?}"
);
declared
.variants
.iter()
.map(|variant| snake_case(&variant.ident.to_string()))
.collect()
}
fn snake_case(name: &str) -> String {
let mut written = String::new();
for character in name.chars() {
if character.is_uppercase() && !written.is_empty() {
written.push('_');
}
written.extend(character.to_lowercase());
}
written
}
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(_))
}