pub(crate) struct Declaration {
pub(crate) name: String,
pub(crate) path_attribute: Option<String>,
pub(crate) test_only: bool,
}
fn split_top_level(text: &str) -> Vec<&str> {
let mut pieces: Vec<&str> = Vec::new();
let mut depth: usize = 0;
let mut start: usize = 0;
for (offset, character) in text.char_indices() {
if character == '(' {
depth += 1;
} else if character == ')' {
depth = depth.saturating_sub(1);
} else if character == ',' && depth == 0 {
pieces.push(text[start..offset].trim());
start = offset + 1;
}
}
pieces.push(text[start..].trim());
return pieces;
}
fn is_test_gate(predicate: &str) -> bool {
let trimmed: &str = predicate.trim();
if trimmed == "test" {
return true;
}
if let Some(inner) = trimmed.strip_prefix("all(").and_then(|rest| return rest.strip_suffix(')')) {
for condition in split_top_level(inner) {
if is_test_gate(condition) {
return true;
}
}
}
return false;
}
pub(crate) fn module_name(line: &str) -> Option<&str> {
let mut rest: &str = line.trim();
for prefix in ["pub(crate) ", "pub(super) ", "pub "] {
if let Some(after) = rest.strip_prefix(prefix) {
rest = after;
break;
}
}
let after_keyword: &str = rest.strip_prefix("mod ")?;
let name: &str = after_keyword.strip_suffix(';')?;
if name.is_empty() || !name.chars().all(|character| return character.is_alphanumeric() || character == '_') {
return None;
}
return Some(name);
}
pub(crate) fn declarations(text: &str) -> Result<Vec<Declaration>, String> {
let mut found: Vec<Declaration> = Vec::new();
let mut attributes: Vec<&str> = Vec::new();
for line in text.lines() {
let trimmed: &str = line.trim();
if trimmed.is_empty() || trimmed.starts_with("//") {
continue;
}
if trimmed.starts_with("#[") {
attributes.push(trimmed);
continue;
}
let Some(name) = module_name(trimmed) else {
attributes.clear();
continue;
};
if line.starts_with(char::is_whitespace) {
return Err(format!("`mod {name};` is indented; a module declared inside an inline module is not resolved"));
}
let mut path_attribute: Option<String> = None;
let mut test_only: bool = false;
for attribute in attributes.drain(..) {
if attribute.starts_with("#[path") {
let value: Option<&str> =
attribute.strip_prefix("#[path = \"").and_then(|rest| return rest.strip_suffix("\"]"));
let Some(written) = value else {
return Err(format!("`{attribute}` above `mod {name};` is not in the `#[path = \"...\"]` form"));
};
path_attribute = Some(written.to_owned());
} else if attribute.starts_with("#[cfg_attr(") && attribute.contains("path") {
return Err(format!("`{attribute}` above `mod {name};` chooses a file by condition; not resolved"));
} else if let Some(predicate) = attribute.strip_prefix("#[cfg(").and_then(|rest| return rest.strip_suffix(")]")) {
test_only = test_only || is_test_gate(predicate);
}
}
found.push(Declaration { name: name.to_owned(), path_attribute, test_only });
}
return Ok(found);
}