use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
use pedant_core::capabilities::detect_capabilities;
use pedant_core::ir::extract;
use pedant_types::Capability;
use syn::punctuated::Punctuated;
use syn::visit::Visit;
pub(crate) const CHECKS_FEATURE: &str = "checks";
pub(crate) const SEMANTIC_EXCLUSION: &str = "ir/semantic";
pub(crate) fn excluded_root() -> PathBuf {
crate_path("src").join(SEMANTIC_EXCLUSION)
}
pub(crate) fn process_evidence(path: &Path) -> Option<Box<str>> {
let syntax = parse_rust_file(path);
let ir = extract(&path.to_string_lossy(), &syntax, None);
detect_capabilities(&ir, None)
.findings
.iter()
.find(|finding| finding.capability == Capability::ProcessExec)
.map(|finding| format!("{}: {}", path.display(), finding.evidence).into_boxed_str())
}
pub(crate) fn assert_semantic_exclusion_is_not_vacuous() {
assert!(
module_files("ir")
.iter()
.any(|path| path.ends_with("ir/semantic/context.rs")),
"the exclusion is not vacuous: context.rs is in the unfiltered expansion"
);
}
pub(crate) const SUBSTRATE_ROOTS: &[&str] = &["hash.rs", "pattern.rs", "substrate.rs"];
pub(crate) fn crate_path(relative: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(relative)
}
fn read_file(path: &Path) -> String {
fs::read_to_string(path)
.unwrap_or_else(|error| panic!("{} should be readable: {error}", path.display()))
}
pub(crate) fn parse_rust_file(path: &Path) -> syn::File {
syn::parse_file(&read_file(path))
.unwrap_or_else(|error| panic!("{} should parse as Rust: {error}", path.display()))
}
pub(crate) fn manifest_table() -> toml::Table {
let text = read_file(&crate_path("Cargo.toml"));
toml::from_str(&text).expect("pedant-core/Cargo.toml should parse as TOML")
}
pub(crate) fn file_name(path: &Path) -> Box<str> {
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or_else(|| panic!("{} should have a UTF-8 file name", path.display()))
.into()
}
fn is_rust_source(path: &Path) -> bool {
path.extension().is_some_and(|extension| extension == "rs")
}
fn entry_path(entry: std::io::Result<fs::DirEntry>) -> PathBuf {
entry.expect("directory entry should be readable").path()
}
pub(crate) fn test_root_paths() -> Box<[PathBuf]> {
let entries = fs::read_dir(crate_path("tests")).expect("pedant-core/tests should be readable");
let mut roots: Vec<PathBuf> = entries
.map(entry_path)
.filter(|path| path.is_file() && is_rust_source(path))
.collect();
roots.sort();
roots.into_boxed_slice()
}
#[cfg(feature = "resolution-test-support")]
pub(crate) fn crate_sources() -> Box<[PathBuf]> {
let mut files = Vec::new();
collect_rust_files(&crate_path("src"), &mut files);
files.sort();
files.into_boxed_slice()
}
pub(crate) fn module_files(module: &str) -> Box<[PathBuf]> {
let source_root = crate_path("src");
let mut files = Vec::new();
let flat = source_root.join(format!("{module}.rs"));
if flat.is_file() {
files.push(flat);
}
let directory = source_root.join(module);
if directory.is_dir() {
collect_rust_files(&directory, &mut files);
}
files.into_boxed_slice()
}
fn collect_rust_files(directory: &Path, files: &mut Vec<PathBuf>) {
let entries = fs::read_dir(directory)
.unwrap_or_else(|error| panic!("{} should be readable: {error}", directory.display()));
for entry in entries {
let path = entry_path(entry);
match (path.is_dir(), is_rust_source(&path)) {
(true, _) => collect_rust_files(&path, files),
(false, true) => files.push(path),
(false, false) => {}
}
}
}
pub(crate) fn has_checks_gate(attrs: &[syn::Attribute]) -> bool {
attrs.iter().any(is_checks_gate)
}
fn is_checks_gate(attr: &syn::Attribute) -> bool {
match &attr.meta {
syn::Meta::List(list) if list.path.is_ident("cfg") => list
.parse_args::<syn::Meta>()
.is_ok_and(|predicate| is_checks_predicate(&predicate)),
_ => false,
}
}
fn is_checks_predicate(predicate: &syn::Meta) -> bool {
match predicate {
syn::Meta::NameValue(pair) if pair.path.is_ident("feature") => {
literal_is(&pair.value, CHECKS_FEATURE)
}
_ => false,
}
}
fn literal_is(expr: &syn::Expr, expected: &str) -> bool {
match expr {
syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(text),
..
}) => text.value() == expected,
_ => false,
}
}
fn use_tree_names(tree: &syn::UseTree, names: &mut BTreeSet<Box<str>>) {
match tree {
syn::UseTree::Path(path) => use_tree_names(&path.tree, names),
syn::UseTree::Name(name) => {
names.insert(name.ident.to_string().into_boxed_str());
}
syn::UseTree::Rename(rename) => {
names.insert(rename.rename.to_string().into_boxed_str());
}
syn::UseTree::Group(group) => group.items.iter().for_each(|it| use_tree_names(it, names)),
syn::UseTree::Glob(_) => {}
}
}
#[derive(Default)]
pub(crate) struct PathIdents {
idents: BTreeSet<Box<str>>,
}
impl PathIdents {
pub(crate) fn scan(file: &syn::File) -> Self {
let mut scan = Self::default();
scan.visit_file(file);
scan
}
#[cfg(feature = "resolution-test-support")]
pub(crate) fn names_any(&self, candidates: &[&str]) -> bool {
candidates
.iter()
.any(|candidate| self.idents.contains(*candidate))
}
fn visit_macro_body(&mut self, node: &syn::Macro) {
if let Ok(arguments) =
node.parse_body_with(Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated)
{
arguments.iter().for_each(|it| self.visit_expr(it));
return;
}
if let Ok(statements) = node.parse_body_with(syn::Block::parse_within) {
statements.iter().for_each(|it| self.visit_stmt(it));
}
}
}
impl<'ast> Visit<'ast> for PathIdents {
fn visit_path(&mut self, node: &'ast syn::Path) {
let segments = node.segments.iter();
self.idents
.extend(segments.map(|it| it.ident.to_string().into_boxed_str()));
syn::visit::visit_path(self, node);
}
fn visit_use_path(&mut self, node: &'ast syn::UsePath) {
self.idents.insert(node.ident.to_string().into_boxed_str());
syn::visit::visit_use_path(self, node);
}
fn visit_use_name(&mut self, node: &'ast syn::UseName) {
self.idents.insert(node.ident.to_string().into_boxed_str());
}
fn visit_use_rename(&mut self, node: &'ast syn::UseRename) {
self.idents.insert(node.ident.to_string().into_boxed_str());
}
fn visit_macro(&mut self, node: &'ast syn::Macro) {
self.visit_macro_body(node);
syn::visit::visit_macro(self, node);
}
}
pub(crate) struct LibSurface {
pub(crate) ungated_modules: Box<[Box<str>]>,
pub(crate) judgment_names: BTreeSet<Box<str>>,
}
impl LibSurface {
pub(crate) fn classify() -> Self {
let lib = parse_rust_file(&crate_path("src").join("lib.rs"));
let mut ungated_modules = Vec::new();
let mut judgment_names = BTreeSet::new();
for item in &lib.items {
match item {
syn::Item::Mod(declaration) => {
classify_module(declaration, &mut ungated_modules, &mut judgment_names);
}
syn::Item::Use(reexport) => classify_reexport(reexport, &mut judgment_names),
_ => {}
}
}
Self {
ungated_modules: ungated_modules.into_boxed_slice(),
judgment_names,
}
}
pub(crate) fn judgment_references(&self, scan: &PathIdents) -> Box<[Box<str>]> {
self.judgment_names
.intersection(&scan.idents)
.cloned()
.collect()
}
}
fn classify_module(
declaration: &syn::ItemMod,
ungated_modules: &mut Vec<Box<str>>,
judgment_names: &mut BTreeSet<Box<str>>,
) {
let name = declaration.ident.to_string().into_boxed_str();
match has_checks_gate(&declaration.attrs) {
true => {
judgment_names.insert(name);
}
false => ungated_modules.push(name),
}
}
fn classify_reexport(reexport: &syn::ItemUse, judgment_names: &mut BTreeSet<Box<str>>) {
if has_checks_gate(&reexport.attrs) {
use_tree_names(&reexport.tree, judgment_names);
}
}