use std::collections::VecDeque;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Hit {
pub file: PathBuf,
pub line: usize,
pub token: String,
pub text: String,
}
impl std::fmt::Display for Hit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}: `{}` — {}", self.file.display(), self.line, self.token, self.text)
}
}
fn rayon_call_tokens() -> Vec<String> {
[
concat!("use ", "rayon"),
concat!("extern crate ", "rayon"),
concat!("rayon", "::"),
concat!(".", "par_iter("),
concat!(".", "into_par_iter("),
concat!(".", "par_iter_mut("),
concat!(".", "par_bridge("),
concat!(".", "par_chunks("),
concat!(".", "par_chunks_mut("),
concat!(".", "par_sort("),
concat!(".", "par_sort_by("),
concat!(".", "par_sort_unstable("),
concat!(".", "par_extend("),
concat!("Parallel", "Iterator"),
concat!("IntoParallel", "Iterator"),
concat!("ThreadPool", "Builder"),
]
.iter()
.map(|s| (*s).to_string())
.collect()
}
fn rayon_dep_key() -> String {
concat!("ray", "on").to_string()
}
fn strip_line_comment(line: &str) -> &str {
match line.find("//") {
Some(i) => &line[..i],
None => line,
}
}
pub fn workspace_root(start: &Path) -> PathBuf {
let mut found = start.to_path_buf();
let mut cursor = Some(start);
while let Some(dir) = cursor {
let manifest = dir.join("Cargo.toml");
if std::fs::read_to_string(&manifest)
.map(|text| text.lines().any(|l| l.trim() == "[workspace]"))
.unwrap_or(false)
{
found = dir.to_path_buf();
}
cursor = dir.parent();
}
found
}
pub fn workspace_manifests(root: &Path) -> Vec<PathBuf> {
let mut out = vec![root.join("Cargo.toml")];
for member in first_party_dirs(root) {
let m = root.join(&member).join("Cargo.toml");
if m.exists() {
out.push(m);
}
}
out
}
pub fn workspace_members(root: &Path) -> Vec<String> {
manifest_string_array(root, "members")
}
pub fn workspace_excludes(root: &Path) -> Vec<String> {
manifest_string_array(root, "exclude")
}
pub fn first_party_dirs(root: &Path) -> Vec<String> {
let mut out = workspace_members(root);
for e in workspace_excludes(root) {
if !out.contains(&e) {
out.push(e);
}
}
out
}
fn manifest_string_array(root: &Path, key: &str) -> Vec<String> {
let Ok(text) = std::fs::read_to_string(root.join("Cargo.toml")) else {
return Vec::new();
};
let mut cursor = 0usize;
let mut start = None;
let mut in_workspace = false;
for line in text.split_inclusive('\n') {
let trimmed = line.trim_start();
if trimmed.starts_with('[') {
in_workspace = trimmed.starts_with("[workspace]");
} else if in_workspace {
if let Some(rest) = trimmed.strip_prefix(key) {
if rest.trim_start().starts_with('=') {
start = Some(cursor + (line.len() - trimmed.len()));
break;
}
}
}
cursor += line.len();
}
let Some(start) = start else { return Vec::new() };
let rest: String = text[start..]
.lines()
.map(|l| l.split('#').next().unwrap_or(""))
.collect::<Vec<_>>()
.join("\n");
let Some(open) = rest.find('[') else { return Vec::new() };
let Some(close) = rest[open..].find(']') else { return Vec::new() };
rest[open + 1..open + close]
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect()
}
pub fn source_roots(root: &Path) -> Vec<PathBuf> {
const SUBDIRS: &[&str] = &["src", "tests", "benches", "examples"];
let mut dirs: Vec<PathBuf> = Vec::new();
let push_for = |base: PathBuf, dirs: &mut Vec<PathBuf>| {
for sub in SUBDIRS {
let p = base.join(sub);
if p.is_dir() {
dirs.push(p);
}
}
};
push_for(root.to_path_buf(), &mut dirs);
for member in first_party_dirs(root) {
push_for(root.join(member), &mut dirs);
}
dirs
}
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(rd) = std::fs::read_dir(dir) else { return };
for entry in rd.flatten() {
let path = entry.path();
if path.is_dir() {
if path.file_name().map(|n| n == "target").unwrap_or(false) {
continue;
}
rs_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Scan {
pub hits: Vec<Hit>,
pub scanned: usize,
}
pub fn scan_source(root: &Path) -> Scan {
let tokens = rayon_call_tokens();
let mut files = Vec::new();
for dir in source_roots(root) {
rs_files(&dir, &mut files);
}
let mut hits = Vec::new();
for file in &files {
let Ok(text) = std::fs::read_to_string(file) else { continue };
for (i, raw) in text.lines().enumerate() {
let code = strip_line_comment(raw);
for tok in &tokens {
if code.contains(tok.as_str()) {
hits.push(Hit {
file: file.clone(),
line: i + 1,
token: tok.clone(),
text: code.trim().to_string(),
});
}
}
}
}
Scan { hits, scanned: files.len() }
}
pub fn scan_manifests(root: &Path) -> Scan {
let key = rayon_dep_key();
let manifests = workspace_manifests(root);
let mut hits = Vec::new();
for manifest in &manifests {
let Ok(text) = std::fs::read_to_string(manifest) else { continue };
let mut in_deps = false;
for (i, raw) in text.lines().enumerate() {
let line = raw.trim();
if line.starts_with('[') {
in_deps = line.contains("dependencies]");
continue;
}
if !in_deps {
continue;
}
let code = strip_line_comment(line);
let name = code.split('=').next().unwrap_or("").trim().trim_matches('"');
if name == key {
hits.push(Hit {
file: manifest.clone(),
line: i + 1,
token: key.clone(),
text: code.trim().to_string(),
});
}
}
}
Scan { hits, scanned: manifests.len() }
}
pub fn cargo_metadata(root: &Path) -> Result<serde_json::Value, String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let out = Command::new(cargo)
.args(["metadata", "--format-version", "1", "--offline", "--locked"])
.current_dir(root)
.output()
.map_err(|e| format!("could not run `cargo metadata` in {}: {e}", root.display()))?;
if !out.status.success() {
let stderr = String::from_utf8_lossy(&out.stderr);
return Err(format!(
"`cargo metadata --offline --locked` failed in {} ({}):\n{}",
root.display(),
out.status,
stderr.lines().take(8).collect::<Vec<_>>().join("\n"),
));
}
serde_json::from_slice(&out.stdout).map_err(|e| format!("cargo metadata is not JSON: {e}"))
}
fn id_to_name(md: &serde_json::Value) -> std::collections::HashMap<String, String> {
md["packages"]
.as_array()
.map(|ps| {
ps.iter()
.filter_map(|p| {
Some((p["id"].as_str()?.to_string(), p["name"].as_str()?.to_string()))
})
.collect()
})
.unwrap_or_default()
}
fn edge_kinds(dep: &serde_json::Value) -> Vec<String> {
dep["dep_kinds"]
.as_array()
.map(|ks| {
ks.iter()
.map(|k| k["kind"].as_str().unwrap_or("normal").to_string())
.collect()
})
.unwrap_or_default()
}
fn chain_to(
md: &serde_json::Value,
target_name: &str,
require_non_normal: bool,
) -> Option<Vec<String>> {
let names = id_to_name(md);
let nodes: std::collections::HashMap<&str, &serde_json::Value> = md["resolve"]["nodes"]
.as_array()?
.iter()
.filter_map(|n| Some((n["id"].as_str()?, n)))
.collect();
let members: Vec<&str> =
md["workspace_members"].as_array()?.iter().filter_map(|m| m.as_str()).collect();
let mut seen: std::collections::HashSet<(&str, bool)> = std::collections::HashSet::new();
let mut queue: VecDeque<(&str, bool, Vec<String>)> = VecDeque::new();
for m in members {
if seen.insert((m, false)) {
let label = names.get(m).cloned().unwrap_or_else(|| m.to_string());
queue.push_back((m, false, vec![label]));
}
}
while let Some((id, crossed, path)) = queue.pop_front() {
if names.get(id).map(|n| n == target_name).unwrap_or(false)
&& path.len() > 1
&& (crossed || !require_non_normal)
{
return Some(path);
}
let Some(node) = nodes.get(id) else { continue };
let Some(deps) = node["deps"].as_array() else { continue };
for dep in deps {
let kinds = edge_kinds(dep);
let has_normal = kinds.iter().any(|k| k == "normal");
let has_other = kinds.iter().any(|k| k == "dev" || k == "build");
let Some(pkg) = dep["pkg"].as_str() else { continue };
let mut onward: Vec<bool> = Vec::new();
if has_normal {
onward.push(crossed);
}
if require_non_normal && has_other {
onward.push(true);
}
for next_crossed in onward {
if !seen.insert((pkg, next_crossed)) {
continue;
}
let mut next = path.clone();
next.push(names.get(pkg).cloned().unwrap_or_else(|| pkg.to_string()));
queue.push_back((pkg, next_crossed, next));
}
}
}
None
}
pub fn overapproximated_normal_chain(md: &serde_json::Value) -> Option<Vec<String>> {
chain_to(md, &rayon_dep_key(), false)
}
pub fn unshipped_rayon_chain(md: &serde_json::Value) -> Option<Vec<String>> {
chain_to(md, &rayon_dep_key(), true)
}
pub fn rayon_tree(root: &Path, edges: &str, extra: &[&str]) -> Result<String, String> {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".into());
let mut cmd = Command::new(cargo);
cmd.args(["tree", "-i", &rayon_dep_key(), "--target", "all", "-e", edges])
.args(["--offline", "--locked"])
.args(extra)
.current_dir(root);
let out = cmd
.output()
.map_err(|e| format!("could not run `cargo tree` in {}: {e}", root.display()))?;
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
let stderr = String::from_utf8_lossy(&out.stderr).to_string();
if !out.status.success() {
return Err(format!(
"`cargo tree -e {edges} -i rayon --target all {}` failed in {} ({}):\n{}",
extra.join(" "),
root.display(),
out.status,
stderr.lines().take(8).collect::<Vec<_>>().join("\n"),
));
}
Ok(if stdout.trim().is_empty() { stderr } else { stdout })
}
pub fn tree_found_nothing(out: &str) -> bool {
out.trim().is_empty() || out.contains("nothing to print")
}
pub fn tree_chain_to_first_party(out: &str, root: &Path) -> Option<Vec<String>> {
let here = format!("({}", root.display());
let mut stack: Vec<String> = Vec::new();
for line in out.lines() {
let prefix: String =
line.chars().take_while(|c| matches!(c, '│' | ' ' | '├' | '└' | '─')).collect();
let body = &line[prefix.len()..];
if body.is_empty() || body.starts_with('[') {
continue;
}
let depth = prefix.chars().count() / 4;
let Some(name) = body.split_whitespace().next().map(str::to_string) else { continue };
stack.truncate(depth);
stack.push(name);
if body.contains(&here) {
let mut chain = stack.clone();
chain.reverse();
return Some(chain);
}
}
None
}
pub fn shipped_rayon_tree(root: &Path) -> Result<Option<Vec<String>>, String> {
shipped_rayon_tree_with(root, &[])
}
pub fn shipped_rayon_tree_with(
root: &Path,
features: &[&str],
) -> Result<Option<Vec<String>>, String> {
let out = rayon_tree(root, "normal", features)?;
if tree_found_nothing(&out) {
return Ok(None);
}
Ok(Some(tree_chain_to_first_party(&out, root).unwrap_or_else(|| {
out.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect()
})))
}
pub fn only_feature(name: &str) -> Vec<String> {
vec!["--no-default-features".into(), "--features".into(), name.into()]
}
pub fn rayon_tree_liveness_chain(root: &Path) -> Result<Option<Vec<String>>, String> {
let out = rayon_tree(root, "normal,dev,build", &[])?;
if tree_found_nothing(&out) {
return Ok(None);
}
Ok(tree_chain_to_first_party(&out, root))
}
pub fn rayon_tree_all_features_chain(root: &Path) -> Result<Option<Vec<String>>, String> {
let out = rayon_tree(root, "normal", &["--all-features"])?;
if tree_found_nothing(&out) {
return Ok(None);
}
Ok(tree_chain_to_first_party(&out, root))
}
pub fn workspace_features(md: &serde_json::Value, pkg: &str) -> Option<Vec<String>> {
let names = id_to_name(md);
let mut out: Vec<String> = Vec::new();
let mut found = false;
for node in md["resolve"]["nodes"].as_array()? {
let id = node["id"].as_str()?;
if names.get(id).map(|n| n != pkg).unwrap_or(true) {
continue;
}
found = true;
if let Some(fs) = node["features"].as_array() {
out.extend(fs.iter().filter_map(|f| f.as_str().map(str::to_string)));
}
}
if !found {
return None;
}
out.sort();
out.dedup();
Some(out)
}
#[derive(Debug, Clone)]
pub struct GatedTestFile {
pub file: PathBuf,
pub feature: String,
pub tests: usize,
}
pub fn gated_test_files(crate_dir: &Path) -> Vec<GatedTestFile> {
let mut out = Vec::new();
let Ok(rd) = std::fs::read_dir(crate_dir.join("tests")) else { return out };
for entry in rd.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("rs") {
continue;
}
let Ok(text) = std::fs::read_to_string(&path) else { continue };
let Some(feature) = text.lines().find_map(crate_level_cfg_feature) else { continue };
let tests = text
.lines()
.filter(|l| {
let t = l.trim();
t.starts_with("#[test]") || t.starts_with("#[tokio::test")
})
.count();
out.push(GatedTestFile { file: path, feature, tests });
}
out.sort_by(|a, b| a.file.cmp(&b.file));
out
}
fn crate_level_cfg_feature(line: &str) -> Option<String> {
let t = line.trim();
let rest = t.strip_prefix("#![cfg(feature")?;
let rest = rest.trim_start().strip_prefix('=')?.trim_start();
let rest = rest.strip_prefix('"')?;
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comment_stripping_keeps_code_and_drops_prose() {
assert_eq!(strip_line_comment("let x = 1; // rayon::scope is banned").trim(), "let x = 1;");
assert_eq!(strip_line_comment("//! never rayon").trim(), "");
assert_eq!(strip_line_comment("let y = 2;").trim(), "let y = 2;");
}
#[test]
fn the_forbidden_tokens_are_not_spelled_literally_in_this_file() {
let me = include_str!("law.rs");
for tok in rayon_call_tokens() {
let literal_uses = me
.lines()
.filter(|l| !strip_line_comment(l).trim().is_empty())
.filter(|l| strip_line_comment(l).contains(&tok))
.count();
assert_eq!(literal_uses, 0, "`{tok}` appears literally in live code in law.rs");
}
}
#[test]
fn a_crate_level_gate_is_read_and_a_per_fn_one_is_not() {
assert_eq!(crate_level_cfg_feature("#![cfg(feature = \"wgpu\")]"), Some("wgpu".into()));
assert_eq!(crate_level_cfg_feature(" #![cfg(feature=\"gpu\")] "), Some("gpu".into()));
assert_eq!(crate_level_cfg_feature("#[cfg(feature = \"wgpu\")]"), None);
assert_eq!(crate_level_cfg_feature("fn main() {}"), None);
}
#[test]
fn the_word_members_in_a_header_comment_is_not_the_members_key() {
let dir = scratch("header-comment");
std::fs::write(
dir.join("Cargo.toml"),
"# korp is the workspace ROOT. The `crates/*` leaves are NOT pulled in\n\
# as members — they are self-rooting.\n\
[workspace]\n\
resolver = \"3\"\n\
members = [\"xtask\"]\n\
exclude = [\"crates/korp-ontology\", \"crates/korp-demo\"]\n\
\n\
[package]\n\
name = \"korp\"\n\
exclude = [\"docs/*\"]\n",
)
.unwrap();
assert_eq!(workspace_members(&dir), vec!["xtask".to_string()]);
assert_eq!(
workspace_excludes(&dir),
vec!["crates/korp-ontology".to_string(), "crates/korp-demo".to_string()]
);
assert_eq!(
first_party_dirs(&dir),
vec![
"xtask".to_string(),
"crates/korp-ontology".to_string(),
"crates/korp-demo".to_string()
]
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn a_trailing_toml_comment_inside_the_array_is_not_a_member() {
let dir = scratch("array-comment");
std::fs::write(
dir.join("Cargo.toml"),
"[workspace]\nmembers = [\n \"a\", # the first one\n \"b\",\n]\n",
)
.unwrap();
assert_eq!(workspace_members(&dir), vec!["a".to_string(), "b".to_string()]);
std::fs::remove_dir_all(&dir).ok();
}
fn scratch(tag: &str) -> PathBuf {
let dir = std::env::temp_dir()
.join(format!("facett-law-{tag}-{}-{:?}", std::process::id(), std::thread::current().id()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
const KORP_ALL_FEATURES_TREE: &str = "\
rayon v1.12.0
├── av-scenechange v0.14.1
│ └── rav1e v0.8.1
│ └── ravif v0.13.0
│ └── image v0.25.10
│ ├── arboard v3.6.1
│ │ └── egui-winit v0.35.0
│ │ └── eframe v0.35.0
│ │ ├── korp v0.1.0 (/home/rickard/scratch/lawfix/korp)
│ │ └── nornir-robotui v0.3.0
│ │ └── korp v0.1.0 (/home/rickard/scratch/lawfix/korp)
│ ├── dify v0.8.0
│ │ └── egui_kittest v0.35.0
│ │ └── nornir-robotui v0.3.0 (*)
│ ├── eframe v0.35.0 (*)
│ ├── egui_kittest v0.35.0 (*)
│ ├── facett-about v0.1.13 (/home/rickard/scratch/lawfix/facett/facett-about)
│ │ └── korp v0.1.0 (/home/rickard/scratch/lawfix/korp)
│ ├── korp v0.1.0 (/home/rickard/scratch/lawfix/korp)
│ ├── nornir-robotui v0.3.0 (*)
│ └── webp v0.3.1
│ └── nornir-robotui v0.3.0 (*)
├── dify v0.8.0 (*)
├── image v0.25.10 (*)
├── maybe-rayon v0.1.1
│ └── rav1e v0.8.1 (*)
└── ravif v0.13.0 (*)
";
#[test]
fn the_tree_parser_reads_a_real_violation_chain_all_the_way_to_us() {
assert!(!tree_found_nothing(KORP_ALL_FEATURES_TREE));
let korp = Path::new("/home/rickard/scratch/lawfix/korp");
let chain = tree_chain_to_first_party(KORP_ALL_FEATURES_TREE, korp)
.expect("the chain reaches korp and must be readable");
assert_eq!(
chain,
vec![
"korp",
"eframe",
"egui-winit",
"arboard",
"image",
"ravif",
"rav1e",
"av-scenechange",
"rayon"
]
);
let facett = Path::new("/home/rickard/scratch/lawfix/facett");
let via_facett = tree_chain_to_first_party(KORP_ALL_FEATURES_TREE, facett)
.expect("facett-about is first-party to the facett workspace");
assert_eq!(
via_facett,
vec!["facett-about", "image", "ravif", "rav1e", "av-scenechange", "rayon"]
);
assert_eq!(tree_chain_to_first_party(KORP_ALL_FEATURES_TREE, Path::new("/nowhere")), None);
}
#[test]
fn nothing_to_print_is_the_clean_answer_and_empty_is_not_silently_the_same() {
let clean = "warning: nothing to print.\n\nTo find dependencies that require \
specific target platforms, try to use option `--target all` first.\n";
assert!(tree_found_nothing(clean));
assert!(tree_found_nothing(" \n"));
assert!(!tree_found_nothing(KORP_ALL_FEATURES_TREE));
assert_eq!(tree_chain_to_first_party(clean, Path::new("/home/rickard/scratch/lawfix/korp")), None);
}
#[test]
fn members_parse_from_a_multi_line_array() {
let dir = scratch("multiline");
std::fs::write(
dir.join("Cargo.toml"),
"[workspace]\nmembers = [\n \"a\",\n \"b\",\n]\n",
)
.unwrap();
assert_eq!(workspace_members(&dir), vec!["a".to_string(), "b".to_string()]);
std::fs::remove_dir_all(&dir).ok();
}
}