use aethershell::builtins::BUILTIN_LOOKUP;
use aethershell::safety::{effect_of, Effect};
use std::collections::{HashMap, HashSet};
fn source_files() -> Vec<String> {
fn walk(dir: &std::path::Path, out: &mut Vec<String>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for e in entries.filter_map(|e| e.ok()) {
let p = e.path();
if p.is_dir() {
walk(&p, out);
} else if p.extension().and_then(|s| s.to_str()) == Some("rs") {
if let Ok(text) = std::fs::read_to_string(&p) {
out.push(text);
}
}
}
}
let mut out = Vec::new();
walk(
&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"),
&mut out,
);
assert!(
out.len() > 20,
"expected to read the crate's modules, got {}",
out.len()
);
out
}
const EVIDENCE: &[(&str, &str)] = &[
("Command::new", "constructs an OS process"),
("fs::write", "writes a file"),
("fs::remove_file", "deletes a file"),
("fs::remove_dir", "deletes a directory"),
("fs::create_dir", "creates a directory"),
("fs::copy", "copies a file"),
("fs::rename", "renames a path"),
("File::create", "creates a file"),
("TcpStream::connect", "opens a socket"),
("reqwest::", "makes an HTTP request"),
(".append(true)", "opens a file to append"),
(".create(true)", "opens a file to create"),
(".write(true)", "opens a file to write"),
("fs::set_permissions", "changes file permissions"),
("fs::hard_link", "creates a hard link"),
("symlink_file(", "creates a file symlink"),
("symlink_dir(", "creates a directory symlink"),
("fs::symlink(", "creates a symbolic link"),
("TcpListener::bind", "binds a listening socket"),
("UdpSocket::bind", "binds a datagram socket"),
];
const BASELINE: &str = include_str!("effect_ratchet_baseline.txt");
fn baseline() -> std::collections::HashSet<&'static str> {
BASELINE
.lines()
.map(str::trim)
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect()
}
const FOLLOW_DEPTH: usize = 4;
fn strip_comments(src: &str) -> String {
let chars: Vec<char> = src.chars().collect();
let mut out = String::with_capacity(src.len());
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
let next = chars.get(i + 1).copied();
if c == '/' && next == Some('/') {
while i < chars.len() && chars[i] != '\n' {
out.push(' ');
i += 1;
}
continue;
}
if c == '/' && next == Some('*') {
let mut depth = 1usize;
out.push(' ');
out.push(' ');
i += 2;
while i < chars.len() && depth > 0 {
if chars[i] == '/' && chars.get(i + 1) == Some(&'*') {
depth += 1;
out.push(' ');
out.push(' ');
i += 2;
} else if chars[i] == '*' && chars.get(i + 1) == Some(&'/') {
depth -= 1;
out.push(' ');
out.push(' ');
i += 2;
} else {
out.push(if chars[i] == '\n' { '\n' } else { ' ' });
i += 1;
}
}
continue;
}
if c == '\'' {
let close = if next == Some('\\') {
chars
.iter()
.skip(i + 2)
.position(|&x| x == '\'')
.map(|p| i + 2 + p)
} else if chars.get(i + 2) == Some(&'\'') {
Some(i + 2)
} else {
None
};
if let Some(close) = close {
for _ in i..=close {
out.push(' ');
}
i = close + 1;
continue;
}
out.push(c);
i += 1;
continue;
}
if c == '"' {
out.push('"');
i += 1;
let mut esc = false;
while i < chars.len() {
let ch = chars[i];
if ch == '\\' && !esc {
esc = true;
out.push(' ');
i += 1;
continue;
}
if ch == '"' && !esc {
out.push('"');
i += 1;
break;
}
esc = false;
out.push(if ch == '\n' { '\n' } else { ' ' });
i += 1;
}
continue;
}
out.push(c);
i += 1;
}
out
}
fn all_fn_bodies() -> Vec<(String, String)> {
let mut out = Vec::new();
for file in source_files() {
collect_fn_bodies(&strip_comments(&file), &mut out);
}
out
}
fn collect_fn_bodies(source: &str, out: &mut Vec<(String, String)>) {
let bytes = source.as_bytes();
let mut search = 0usize;
while let Some(rel) = source[search..].find("fn ") {
let start = search + rel;
search = start + 3;
let rest = &source[start + 3..];
let name_end = match rest.find(|c: char| !(c.is_alphanumeric() || c == '_')) {
Some(i) => i,
None => continue,
};
let fn_name = rest[..name_end].to_string();
if fn_name.is_empty() {
continue;
}
let brace = match source[start..].find('{') {
Some(i) => start + i,
None => continue,
};
let mut depth = 0i32;
let mut i = brace;
let mut in_str = false;
let mut prev_escape = false;
while i < bytes.len() {
let c = bytes[i] as char;
if in_str {
if c == '\\' && !prev_escape {
prev_escape = true;
} else {
if c == '"' && !prev_escape {
in_str = false;
}
prev_escape = false;
}
} else if c == '"' {
in_str = true;
} else if c == '{' {
depth += 1;
} else if c == '}' {
depth -= 1;
if depth == 0 {
break;
}
}
i += 1;
}
if depth == 0 && i > brace {
out.push((fn_name, source[brace..=i.min(bytes.len() - 1)].to_string()));
}
}
}
fn bodies_by_name() -> HashMap<String, String> {
let mut counts: HashMap<String, usize> = HashMap::new();
let bodies = all_fn_bodies();
for (name, _) in &bodies {
*counts.entry(name.clone()).or_insert(0) += 1;
}
bodies
.into_iter()
.filter(|(name, _)| counts.get(name) == Some(&1))
.collect()
}
fn ambiguous_name_count() -> usize {
let mut counts: HashMap<String, usize> = HashMap::new();
for (name, _) in all_fn_bodies() {
*counts.entry(name).or_insert(0) += 1;
}
counts.values().filter(|c| **c > 1).count()
}
#[test]
fn the_parser_finds_a_plausible_number_of_builtin_bodies() {
let all = bodies_by_name();
let builtins = all.keys().filter(|n| n.starts_with("bi_")).count();
assert!(
builtins > 800,
"expected to parse most builtin bodies, got {builtins}"
);
assert!(
all.len() > builtins,
"expected helper functions to be parsed too, got {} total vs {builtins} builtins",
all.len()
);
let body = all
.get("bi_aecon_decode")
.expect("a known builtin should be parsed");
assert!(
body.starts_with('{') && body.ends_with('}'),
"brace matched"
);
}
#[test]
fn the_lint_does_not_read_comments_as_code() {
let src = "\
// fn json_to_value(json: serde_json::Value) -> Value;\n\
fn deletes_things() { std::fs::remove_file(p); }\n";
let stripped = strip_comments(src);
assert!(
!stripped.contains("fn json_to_value"),
"a commented-out signature must not survive stripping: {stripped}"
);
let mut bodies = Vec::new();
collect_fn_bodies(&stripped, &mut bodies);
assert_eq!(
bodies.len(),
1,
"only the real function should be parsed, got {bodies:?}"
);
assert_eq!(bodies[0].0, "deletes_things");
let flagged: Vec<String> = current_violations()
.into_iter()
.map(|(n, _, _)| n)
.filter(|n| n == "json_parse" || n == "jq_query")
.collect();
assert!(
flagged.is_empty(),
"pure JSON builtins must not be reported as acting: {flagged:?}"
);
}
fn direct_evidence(body: &str) -> Option<(&'static str, &'static str)> {
EVIDENCE
.iter()
.find(|(m, _)| body.contains(m))
.map(|(m, why)| (*m, *why))
}
fn delegated_evidence(
body: &str,
all: &HashMap<String, String>,
depth: usize,
seen: &mut HashSet<String>,
) -> Option<(&'static str, String)> {
if depth == 0 {
return None;
}
for callee in called_names(body) {
if !seen.insert(callee.clone()) {
continue;
}
let Some(cb) = all.get(&callee) else { continue };
if let Some((marker, why)) = direct_evidence(cb) {
return Some((marker, format!("{callee}() {why}")));
}
if let Some((marker, chain)) = delegated_evidence(cb, all, depth - 1, seen) {
return Some((marker, format!("{callee}() → {chain}")));
}
}
None
}
fn called_names(body: &str) -> Vec<String> {
let mut out = Vec::new();
let mut ident_start: Option<usize> = None;
let mut prev: Option<char> = None;
let mut prev_prev: Option<char> = None;
for (i, c) in body.char_indices() {
let is_ident = c.is_alphanumeric() || c == '_';
match (ident_start, is_ident) {
(None, true) if c.is_alphabetic() || c == '_' => {
let qualified = prev == Some('.') || (prev == Some(':') && prev_prev == Some(':'));
if !qualified {
ident_start = Some(i);
}
}
(Some(start), false) => {
if c == '(' {
out.push(body[start..i].to_string());
}
ident_start = None;
}
_ => {}
}
if !c.is_whitespace() {
prev_prev = prev;
prev = Some(c);
}
}
out
}
fn current_violations() -> Vec<(String, &'static str, String)> {
let all = bodies_by_name();
let mut out = Vec::new();
for (fn_name, body) in all.iter() {
let Some(name) = fn_name.strip_prefix("bi_") else {
continue;
};
if name.is_empty() || !BUILTIN_LOOKUP.contains_key(name) {
continue;
}
if effect_of(name) != Effect::Pure {
continue;
}
if let Some((marker, why)) = direct_evidence(body) {
out.push((name.to_string(), marker, why.to_string()));
continue;
}
let mut seen = HashSet::new();
seen.insert(fn_name.clone());
if let Some((marker, chain)) = delegated_evidence(body, &all, FOLLOW_DEPTH, &mut seen) {
out.push((name.to_string(), marker, format!("delegates: {chain}")));
}
}
out.sort();
out
}
#[test]
fn no_new_builtin_acts_while_classified_pure() {
let base = baseline();
let fresh: Vec<String> = current_violations()
.into_iter()
.filter(|(name, _, _)| !base.contains(name.as_str()))
.map(|(name, marker, why)| format!(" {name}: {why} (`{marker}`) but effect_of = Pure"))
.collect();
assert!(
fresh.is_empty(),
"{} builtin(s) added since the baseline act but are classified Pure.\n\
Classify them in `safety::effect_of` — do not add them to \
tests/effect_ratchet_baseline.txt, which may only shrink:\n{}",
fresh.len(),
fresh.join("\n")
);
}
#[test]
fn the_baseline_only_shrinks() {
let current: std::collections::HashSet<String> = current_violations()
.into_iter()
.map(|(n, _, _)| n)
.collect();
let fixed: Vec<&str> = baseline()
.into_iter()
.filter(|n| !current.contains(*n))
.collect();
assert!(
fixed.is_empty(),
"{} baseline entr(ies) no longer violate — delete them from \
tests/effect_ratchet_baseline.txt:\n {}",
fixed.len(),
fixed.join("\n ")
);
}
#[test]
fn report_the_outstanding_debt() {
let n = current_violations().len();
println!("effect ratchet: {n} builtin(s) act while classified Pure (baseline 0)");
println!(
"unresolvable call names (defined more than once, so not followed): {}",
ambiguous_name_count()
);
assert!(n <= baseline().len(), "the debt must never grow");
}
#[test]
fn the_scanner_still_sees_a_process_being_constructed() {
let all = bodies_by_name();
let body = all
.get("bi_platform_machine_id")
.expect("bi_platform_machine_id should be parsed");
assert!(
direct_evidence(body).is_some(),
"this builtin constructs a process; failing to see it means the scanner \
is blind, not that the code changed"
);
}
#[test]
fn the_scanner_still_sees_a_file_being_appended_to() {
let all = bodies_by_name();
let body = all
.get("bi_git_ignore")
.expect("bi_git_ignore should be parsed");
assert!(
direct_evidence(body).is_some(),
"git_ignore opens a file with .append(true)"
);
}
#[test]
fn a_char_literal_containing_a_quote_does_not_blind_the_scanner() {
let src = r#"
fn probe() {
let quote = '"';
let _ = quote;
std::process::Command::new("ls");
}
"#;
let stripped = strip_comments(src);
assert!(
stripped.contains("Command::new"),
"code after a quote character literal was blanked: {stripped}"
);
}
#[test]
fn a_string_literal_is_not_read_as_code() {
let src = r#"
fn helptext() -> &'static str {
"usage: xs | join(\"-\")"
}
"#;
let stripped = strip_comments(src);
assert!(
!stripped.contains("join("),
"documentation inside a string was read as a call: {stripped}"
);
}