use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use super::inventory::SOURCES;
pub fn crate_path(relative: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join(relative)
}
pub fn discovered_sources() -> BTreeSet<String> {
discovered_beneath("src")
}
pub fn discovered_test_sources() -> BTreeSet<String> {
discovered_beneath("tests")
}
fn discovered_beneath(directory: &str) -> BTreeSet<String> {
let root = crate_path(directory);
let mut found = BTreeSet::new();
collect(&root, &crate_path("."), &mut found);
found
}
fn collect(directory: &Path, base: &Path, found: &mut BTreeSet<String>) {
let listing = std::fs::read_dir(directory)
.unwrap_or_else(|error| panic!("{}: {error}", directory.display()));
for entry in listing {
let listed = entry.unwrap_or_else(|error| panic!("{}: {error}", directory.display()));
let path = listed.path();
let kind = listed
.file_type()
.unwrap_or_else(|error| panic!("{}: {error}", path.display()));
let is_rust = path.extension().is_some_and(|extension| extension == "rs");
match (kind.is_dir(), is_rust) {
(true, _) => collect(&path, base, found),
(false, true) => {
found.insert(relative_text(base, &path));
}
(false, false) => (),
}
}
}
fn relative_text(base: &Path, path: &Path) -> String {
path.strip_prefix(base)
.unwrap_or_else(|error| panic!("{} is outside {}: {error}", path.display(), base.display()))
.to_str()
.unwrap_or_else(|| panic!("{} is not valid UTF-8", path.display()))
.replace('\\', "/")
}
pub fn code_only(text: &str) -> String {
let kept: Vec<&str> = text
.lines()
.filter(|line| {
let trimmed = line.trim_start();
!trimmed.starts_with("//") && !trimmed.starts_with("# ")
})
.collect();
kept.join("\n")
}
pub fn compact(text: &str) -> String {
code_only(text).split_whitespace().collect()
}
pub fn source(path: &str) -> &'static str {
SOURCES
.iter()
.find(|source| source.path == path)
.map(|source| source.text)
.unwrap_or_else(|| panic!("{path} is not a modelled production source"))
}
pub fn declaring_sources(spelling: &str) -> Vec<&'static str> {
SOURCES
.iter()
.filter(|entry| code_only(entry.text).contains(spelling))
.map(|entry| entry.path)
.collect()
}
pub fn naming(sources: &[&str], spellings: &[&str], vocabulary: &str) -> Vec<String> {
let mut offenders: Vec<String> = Vec::new();
for path in sources {
let code = compact(source(path));
offenders.extend(
spellings
.iter()
.filter(|spelling| code.contains(**spelling))
.map(|spelling| format!("{path} names {vocabulary} {spelling}")),
);
}
offenders
}
pub fn parsed(path: &str) -> syn::File {
syn::parse_file(source(path)).unwrap_or_else(|error| panic!("{path}: {error}"))
}
pub fn function_body(path: &str, name: &str) -> String {
let file = parsed(path);
let found = file
.items
.iter()
.find_map(|item| match item {
syn::Item::Fn(function) if function.sig.ident == name => Some(function),
_ => None,
})
.unwrap_or_else(|| panic!("{path} declares no fn {name}"));
token_text(&found.block)
}
pub struct DeclaredMethod {
pub subject: Arc<str>,
pub name: String,
pub visibility: String,
pub body: String,
}
pub fn declared_methods(path: &str) -> Vec<DeclaredMethod> {
let file = parsed(path);
let mut found = Vec::new();
for item in &file.items {
let syn::Item::Impl(block) = item else {
continue;
};
let subject: Arc<str> = Arc::from(token_text(&block.self_ty));
for member in &block.items {
if let syn::ImplItem::Fn(function) = member {
found.push(DeclaredMethod {
subject: Arc::clone(&subject),
name: function.sig.ident.to_string(),
visibility: token_text(&function.vis),
body: token_text(&function.block),
});
}
}
}
found
}
pub fn subject_methods(path: &str, subject: &str) -> Vec<DeclaredMethod> {
let found: Vec<DeclaredMethod> = declared_methods(path)
.into_iter()
.filter(|method| method.subject.as_ref() == subject)
.collect();
assert!(
!found.is_empty(),
"{path} declares no impl block for {subject}"
);
found
}
pub fn method_body(path: &str, name: &str) -> String {
let found: Vec<DeclaredMethod> = declared_methods(path)
.into_iter()
.filter(|method| method.name == name)
.collect();
match found.len() {
1 => single_body(found, path, name),
0 => panic!("{path} declares no method {name}"),
_ => {
let subjects: Vec<Arc<str>> = found.into_iter().map(|method| method.subject).collect();
panic!("{path} declares {name} on {subjects:?}: name the subject that owns it")
}
}
}
pub fn method_body_of(path: &str, subject: &str, name: &str) -> String {
let found: Vec<DeclaredMethod> = subject_methods(path, subject)
.into_iter()
.filter(|method| method.name == name)
.collect();
assert_eq!(
found.len(),
1,
"{path} declares {subject}::{name} exactly once"
);
single_body(found, path, name)
}
fn single_body(found: Vec<DeclaredMethod>, path: &str, name: &str) -> String {
found
.into_iter()
.next()
.unwrap_or_else(|| panic!("{path} declares no method {name}"))
.body
}
pub fn token_text<T: quote::ToTokens>(value: &T) -> String {
value.to_token_stream().to_string()
}
pub fn position_of(haystack: &str, needle: &str, subject: &str) -> usize {
haystack
.find(needle)
.unwrap_or_else(|| panic!("{subject} does not contain {needle:?}"))
}