use std::collections::BTreeMap;
use std::path::Path;
use super::declaration::{Declaration, declarations};
use super::walk::discover;
#[derive(Debug)]
pub(crate) struct CrateRoot {
pub(crate) path: String,
pub(crate) test_only: bool,
}
#[derive(Debug)]
pub(crate) struct SourceFile {
pub(crate) path: String,
pub(crate) test_only: bool,
}
fn join_names(directory: &str, part: &str) -> Result<String, String> {
let mut names: Vec<&str> = Vec::new();
for piece in directory.split('/').chain(part.split('/')) {
if piece.is_empty() || piece == "." {
continue;
}
if piece == ".." {
if names.pop().is_none() {
return Err(format!("`{part}` in `{directory}` leaves the source root"));
}
continue;
}
names.push(piece);
}
return Ok(names.join("/"));
}
fn parent_of(file: &str) -> &str {
return file.rsplit_once('/').map_or("", |(parent, _)| return parent);
}
fn stem_of(file: &str) -> &str {
let name: &str = file.rsplit_once('/').map_or(file, |(_, name)| return name);
return name.strip_suffix(".rs").unwrap_or(name);
}
fn locate(root: &Path, file: &str, owns_directory: bool, declaration: &Declaration) -> Result<(String, bool), String> {
let directory: &str = parent_of(file);
if let Some(written) = &declaration.path_attribute {
let target: String = join_names(directory, written.as_str())?;
if !root.join(target.as_str()).is_file() {
return Err(format!("{file}: `#[path = \"{written}\"]` names {target}, which is not a file"));
}
return Ok((target, true));
}
let module_directory: String = if owns_directory {
directory.to_owned()
} else {
join_names(directory, stem_of(file))?
};
let plain: String = join_names(module_directory.as_str(), format!("{}.rs", declaration.name).as_str())?;
let nested: String = join_names(module_directory.as_str(), format!("{}/mod.rs", declaration.name).as_str())?;
let plain_exists: bool = root.join(plain.as_str()).is_file();
let nested_exists: bool = root.join(nested.as_str()).is_file();
if plain_exists && nested_exists {
return Err(format!("{file}: `mod {};` matches both {plain} and {nested}", declaration.name));
}
if plain_exists {
return Ok((plain, false));
}
if nested_exists {
return Ok((nested, true));
}
return Err(format!("{file}: `mod {};` has neither {plain} nor {nested}", declaration.name));
}
pub(crate) fn module_tree(root: &Path, roots: &[CrateRoot]) -> Result<Vec<SourceFile>, String> {
let mut known: BTreeMap<String, bool> = BTreeMap::new();
let mut pending: Vec<(String, bool, bool)> = Vec::new();
for crate_root in roots {
pending.push((crate_root.path.clone(), crate_root.test_only, true));
}
while let Some((file, test_only, owns_directory)) = pending.pop() {
if known.get(file.as_str()).is_some_and(|earlier| return !*earlier || test_only) {
continue;
}
known.insert(file.clone(), test_only);
let text: String = std::fs::read_to_string(root.join(file.as_str()))
.map_err(|error| return format!("cannot read {file}: {error}"))?;
let found: Vec<Declaration> = declarations(text.as_str()).map_err(|error| return format!("{file}: {error}"))?;
for declaration in found {
let (child, child_owns): (String, bool) = locate(root, file.as_str(), owns_directory, &declaration)?;
pending.push((child, test_only || declaration.test_only, child_owns));
}
}
let mut tree: Vec<SourceFile> = Vec::new();
for (path, test_only) in known {
tree.push(SourceFile { path, test_only });
}
return Ok(tree);
}
pub(crate) fn classified_sources(root: &Path, roots: &[CrateRoot]) -> Result<Vec<SourceFile>, String> {
let walked: Vec<String> = discover(root)?;
let tree: Vec<SourceFile> = module_tree(root, roots)?;
let mut unreached: Vec<String> = Vec::new();
for path in walked {
if !tree.iter().any(|file| return file.path == path) {
unreached.push(path);
}
}
if !unreached.is_empty() {
return Err(format!("no `mod` declaration reaches: {}", unreached.join(", ")));
}
return Ok(tree);
}
pub(crate) fn manifest_roots(manifest: &str, src_prefix: &str) -> Result<Vec<CrateRoot>, String> {
let mut roots: Vec<CrateRoot> = Vec::new();
let mut section: &str = "";
for line in manifest.lines() {
let trimmed: &str = line.trim();
if trimmed.starts_with('[') {
section = trimmed;
continue;
}
let Some(value) = trimmed.strip_prefix("path = \"").and_then(|rest| return rest.strip_suffix('"')) else {
continue;
};
let test_only: bool = matches!(section, "[[test]]" | "[[bench]]" | "[[example]]");
let release: bool = matches!(section, "[lib]" | "[[bin]]");
let Some(relative) = value.strip_prefix(src_prefix) else {
if test_only || release {
return Err(format!("{section} path `{value}` is not under {src_prefix}"));
}
continue;
};
if test_only || release {
roots.push(CrateRoot { path: relative.to_owned(), test_only });
}
}
if roots.is_empty() {
return Err(String::from("the manifest names no target under the source prefix"));
}
return Ok(roots);
}