use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
const LAYERS: &[(&str, &[&str])] = &[
(
"foundations",
&[
"csrf",
"db",
"declaration",
"error",
"lens",
"naming",
"protocol",
"query_term",
"toasty_compat",
"topcoat_compat",
],
),
(
"the declaration model",
&["form", "navigation", "policy", "schema", "table", "tenancy"],
),
("resources", &["resource"]),
(
"serving",
&["auth", "notification", "page", "panel", "upload"],
),
];
#[test]
fn every_module_reaches_only_its_own_layer_and_the_ones_below() {
let src = Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let lib = strip(&fs::read_to_string(src.join("lib.rs")).expect("read lib.rs"));
for module in declared_modules(&lib) {
assert!(
layer(&module).is_some(),
"`{module}` is in no layer: add it to `LAYERS`"
);
}
let exports = root_exports(&lib);
let mut reached_up = Vec::new();
for file in sources(&src) {
let relative = file.strip_prefix(&src).expect("under src");
let segments = module_path(relative);
let from = &segments[0];
let from_layer = layer(from).expect("every module is in a layer");
let text = strip(&fs::read_to_string(&file).expect("read a source"));
for target in reached_modules(&text, &segments) {
let target = exports.get(&target).cloned().unwrap_or(target);
if layer(&target).is_some_and(|to| to > from_layer) {
reached_up.push(format!(
"{} reaches `{target}` in {}",
relative.display(),
LAYERS[layer(&target).expect("checked")].0
));
}
}
}
reached_up.sort();
reached_up.dedup();
assert!(
reached_up.is_empty(),
"a module reaches a layer above its own:\n{}",
reached_up.join("\n")
);
}
fn layer(module: &str) -> Option<usize> {
LAYERS
.iter()
.position(|(_, modules)| modules.contains(&module))
}
fn sources(dir: &Path) -> Vec<PathBuf> {
let root = dir.ends_with("src");
let mut files = Vec::new();
for entry in fs::read_dir(dir).expect("read a source directory") {
let path = entry.expect("a directory entry").path();
let name = path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
if path.is_dir() {
if name != "tests" {
files.extend(sources(&path));
}
} else if name.ends_with(".rs")
&& !matches!(name, "tests.rs" | "test_support.rs")
&& !(root && name == "lib.rs")
{
files.push(path);
}
}
files
}
fn module_path(relative: &Path) -> Vec<String> {
let mut segments: Vec<String> = relative
.iter()
.map(|segment| {
segment
.to_string_lossy()
.trim_end_matches(".rs")
.to_string()
})
.collect();
if segments.last().is_some_and(|last| last == "mod") {
segments.pop();
}
segments
}
fn reached_modules(text: &str, segments: &[String]) -> Vec<String> {
let mut reached = Vec::new();
let mut rest = text;
while let Some(at) = rest.find("crate::") {
let preceded = rest[..at]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == ':');
rest = &rest[at + "crate::".len()..];
if !preceded {
reached.extend(first_segments(rest));
}
}
let mut rest = text;
while let Some(at) = rest.find("super::") {
let preceded = rest[..at]
.chars()
.next_back()
.is_some_and(|c| c.is_alphanumeric() || c == '_' || c == ':');
rest = &rest[at..];
let mut climbs = 0;
while let Some(after) = rest.strip_prefix("super::") {
climbs += 1;
rest = after;
}
if !preceded && climbs >= segments.len() {
reached.extend(first_segments(rest));
}
}
reached
}
fn first_segments(rest: &str) -> Vec<String> {
match rest.strip_prefix('{') {
Some(group) => top_level_items(&group[..group_end(group)])
.iter()
.filter_map(|item| identifier(item))
.collect(),
None => identifier(rest).into_iter().collect(),
}
}
fn identifier(text: &str) -> Option<String> {
let text = text.trim_start();
let end = text
.find(|c: char| !(c.is_alphanumeric() || c == '_'))
.unwrap_or(text.len());
(end > 0).then(|| text[..end].to_string())
}
fn group_end(group: &str) -> usize {
let mut depth = 1;
for (index, c) in group.char_indices() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return index;
}
}
_ => {}
}
}
group.len()
}
fn top_level_items(group: &str) -> Vec<&str> {
let mut items = Vec::new();
let (mut depth, mut start) = (0, 0);
for (index, c) in group.char_indices() {
match c {
'{' => depth += 1,
'}' => depth -= 1,
',' if depth == 0 => {
items.push(group[start..index].trim());
start = index + 1;
}
_ => {}
}
}
items.push(group[start..].trim());
items.retain(|item| !item.is_empty());
items
}
fn declared_modules(lib: &str) -> Vec<String> {
let mut modules = Vec::new();
let mut test_only = false;
for line in lib.lines().map(str::trim) {
if line == "#[cfg(test)]" {
test_only = true;
continue;
}
let declared = line
.strip_prefix("pub mod ")
.or_else(|| line.strip_prefix("mod "))
.and_then(|rest| rest.strip_suffix(';'));
if let Some(module) = declared
&& !test_only
{
modules.push(module.to_string());
}
test_only = false;
}
modules
}
fn root_exports(lib: &str) -> HashMap<String, String> {
let mut exports = HashMap::new();
let mut rest = lib;
while let Some(at) = rest.find("\npub use ") {
rest = &rest[at + "\npub use ".len()..];
let statement = &rest[..rest.find(';').unwrap_or(rest.len())];
let Some(module) = identifier(statement) else {
continue;
};
for leaf in leaves(statement) {
exports.insert(leaf, module.clone());
}
}
exports
}
fn leaves(tree: &str) -> Vec<String> {
let mut leaves = Vec::new();
let mut word = String::new();
let chars: Vec<char> = tree.chars().collect();
for (index, &c) in chars.iter().enumerate() {
if c.is_alphanumeric() || c == '_' {
word.push(c);
continue;
}
if !word.is_empty() && !(c == ':' && chars.get(index + 1) == Some(&':')) {
leaves.push(std::mem::take(&mut word));
}
word.clear();
}
if !word.is_empty() {
leaves.push(word);
}
leaves
}
fn strip(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let mut chars = source.chars().peekable();
while let Some(c) = chars.next() {
match c {
'/' if chars.peek() == Some(&'/') => {
for c in chars.by_ref() {
if c == '\n' {
out.push('\n');
break;
}
}
}
'/' if chars.peek() == Some(&'*') => {
chars.next();
let mut previous = ' ';
for c in chars.by_ref() {
if previous == '*' && c == '/' {
break;
}
previous = c;
}
}
'\'' => {
let mut ahead = chars.clone();
match (ahead.next(), ahead.next()) {
(Some('\\'), _) => {
for c in chars.by_ref() {
if c == '\'' {
break;
}
}
}
(Some(_), Some('\'')) => {
chars.next();
chars.next();
}
_ => out.push(c),
}
}
'"' => {
out.push('"');
while let Some(c) = chars.next() {
match c {
'\\' => {
chars.next();
}
'"' => break,
_ => {}
}
}
out.push('"');
}
_ => out.push(c),
}
}
out
}