use std::path::{Path, PathBuf};
use std::sync::{Mutex, MutexGuard};
pub(crate) static CWD_LOCK: Mutex<()> = Mutex::new(());
pub(crate) struct CwdGuard {
_lock: MutexGuard<'static, ()>,
original: PathBuf,
}
impl CwdGuard {
pub(crate) fn enter(dir: &Path) -> Self {
let lock = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
let original = std::env::current_dir().expect("read current directory");
std::env::set_current_dir(dir).expect("enter directory");
Self { _lock: lock, original }
}
}
impl Drop for CwdGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original);
}
}
pub(crate) mod cargo_sort_order {
pub(crate) const DEF_TABLE_ORDER: &[&str] = &[
"package",
"workspace",
"lib",
"bin",
"features",
"dependencies",
"build-dependencies",
"dev-dependencies",
];
fn header_segments(inner: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
for character in inner.chars() {
match quote {
Some(open) if character == open => quote = None,
Some(_) => current.push(character),
None if character == '\'' || character == '"' => quote = Some(character),
None if character == '.' => segments.push(std::mem::take(&mut current)),
None => current.push(character),
}
}
segments.push(current);
segments
}
fn rank_of(name: &str) -> usize {
DEF_TABLE_ORDER
.iter()
.position(|table| *table == name)
.unwrap_or(DEF_TABLE_ORDER.len())
}
fn table_sort_key(inner: &str) -> (usize, u8) {
let segments = header_segments(inner);
let first = segments.first().map(String::as_str).unwrap_or_default();
if first == "target" {
let kind = segments.last().map(String::as_str).unwrap_or_default();
return (rank_of(kind), 1);
}
(rank_of(first), 0)
}
const DEPENDENCY_TABLES: &[&str] = &["dependencies", "dev-dependencies", "build-dependencies"];
pub(crate) fn assert_dependency_keys_sorted(label: &str, manifest: &str) -> usize {
let document = manifest
.parse::<toml_edit::DocumentMut>()
.unwrap_or_else(|error| panic!("{label}: generated manifest must be valid TOML: {error}\n{manifest}"));
let mut compared = 0usize;
assert_table_keys_sorted(label, "", document.as_table(), manifest, &mut compared);
compared
}
fn assert_table_keys_sorted(
label: &str,
path: &str,
table: &toml_edit::Table,
manifest: &str,
compared: &mut usize,
) {
for (name, item) in table.iter() {
let Some(child) = item.as_table() else { continue };
if child.is_dotted() {
continue;
}
let child_path = if path.is_empty() {
name.to_owned()
} else {
format!("{path}.{name}")
};
if DEPENDENCY_TABLES.contains(&name) {
let emitted: Vec<&str> = child.iter().map(|(key, _)| key).collect();
let mut expected = emitted.clone();
expected.sort_unstable();
assert_eq!(
emitted, expected,
"{label}: `[{child_path}]` keys are not in cargo-sort order -- it compares the \
bare dependency NAME (a dotted key like `foo.workspace` is the single key \
`foo`), so `cargo sort --check` would reorder this manifest and fail it:\n{manifest}"
);
*compared += emitted.len();
}
assert_table_keys_sorted(label, &child_path, child, manifest, compared);
}
}
pub(crate) fn assert_canonical_table_order(label: &str, manifest: &str) {
let mut previous: Option<((usize, u8), &str)> = None;
let mut header_count = 0usize;
for line in manifest.lines() {
let trimmed = line.trim();
let Some(inner) = trimmed.strip_prefix('[').and_then(|rest| rest.strip_suffix(']')) else {
continue;
};
let inner = inner
.strip_prefix('[')
.and_then(|rest| rest.strip_suffix(']'))
.unwrap_or(inner);
header_count += 1;
let key = table_sort_key(inner);
if let Some((previous_key, previous_header)) = previous {
assert!(
key >= previous_key,
"{label}: table `{trimmed}` must not follow `{previous_header}` -- cargo-sort \
orders tables {DEF_TABLE_ORDER:?} first and every other table after them, so \
`cargo sort --check` would reorder this manifest and fail it:\n{manifest}"
);
}
previous = Some((key, trimmed));
}
assert!(
header_count > 0,
"{label}: no table headers found, so this check examined nothing:\n{manifest}"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_reject_lints_table_placed_before_dependencies() {
let manifest = "[package]\nname = \"demo\"\n\n[lints.clippy]\ndbg_macro = \"deny\"\n\n\
[dependencies]\nserde = \"1\"\n";
let result = std::panic::catch_unwind(|| assert_canonical_table_order("demo", manifest));
assert!(result.is_err(), "checker must reject lints emitted before dependencies");
}
#[test]
fn should_accept_lints_table_placed_last() {
let manifest = "[package]\nname = \"demo\"\n\n[dependencies]\nserde = \"1\"\n\n\
[lints.clippy]\ndbg_macro = \"deny\"\n";
assert_canonical_table_order("demo", manifest);
}
#[test]
fn should_accept_subtables_and_target_dependency_blocks() {
let manifest = "[package]\nname = \"demo\"\n\n[package.metadata.cargo-machete]\n\
ignored = []\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[features]\n\
default = []\n\n[dependencies]\nserde = \"1\"\n\n\
[target.'cfg(unix)'.dependencies]\nlibc = \"0.2\"\n\n\
[build-dependencies]\ncc = \"1\"\n\n[dev-dependencies]\n\
tempfile = \"3\"\n\n[lints.clippy]\ndbg_macro = \"deny\"\n";
assert_canonical_table_order("demo", manifest);
}
#[test]
fn should_reject_dotted_key_ordered_by_raw_line_text() {
let manifest = "[package]\nname = \"demo\"\n\n[dependencies]\n\
alpha-parser = { version = \"1\", path = \"../core\" }\nalpha.workspace = true\n";
let result = std::panic::catch_unwind(|| assert_dependency_keys_sorted("demo", manifest));
assert!(
result.is_err(),
"checker must reject `alpha-parser` emitted before `alpha.workspace`"
);
}
#[test]
fn should_accept_dotted_key_ordered_by_dependency_name() {
let manifest = "[package]\nname = \"demo\"\n\n[dependencies]\n\
alpha.workspace = true\nalpha-parser = { version = \"1\", path = \"../core\" }\n";
assert_eq!(
assert_dependency_keys_sorted("demo", manifest),
2,
"both dependency keys must have been compared"
);
}
#[test]
fn should_check_target_and_workspace_dependency_tables() {
let manifest = "[workspace]\nmembers = []\n\n[workspace.dependencies]\n\
serde = \"1\"\n\n[target.'cfg(unix)'.dependencies]\nlibc = \"0.2\"\n";
assert_eq!(
assert_dependency_keys_sorted("demo", manifest),
2,
"the workspace and target dependency tables must both have been visited"
);
let broken = "[target.'cfg(unix)'.dependencies]\nlibc-extra = \"1\"\nlibc.workspace = true\n";
let result = std::panic::catch_unwind(|| assert_dependency_keys_sorted("demo", broken));
assert!(result.is_err(), "checker must reach into target dependency tables");
}
#[test]
fn should_reject_features_table_placed_after_dependencies() {
let manifest = "[package]\nname = \"demo\"\n\n[dependencies]\nserde = \"1\"\n\n\
[features]\ndefault = []\n";
let result = std::panic::catch_unwind(|| assert_canonical_table_order("demo", manifest));
assert!(
result.is_err(),
"checker must reject features emitted after dependencies"
);
}
}
}
pub(crate) fn git_init(root: &Path) {
let status = std::process::Command::new("git")
.args(["init", "-q"])
.current_dir(root)
.status()
.expect("git init");
assert!(status.success(), "git init must succeed for a tracked-ness fixture");
}
pub(crate) fn git_add(root: &Path, relative: &[&str]) {
let status = std::process::Command::new("git")
.arg("add")
.arg("--")
.args(relative)
.current_dir(root)
.status()
.expect("git add");
assert!(status.success(), "git add must succeed for a tracked-ness fixture");
}
pub(crate) fn write_file(root: &Path, relative: &str, content: &str) -> PathBuf {
let path = root.join(relative);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("create parent directory");
}
std::fs::write(&path, content).expect("write fixture file");
path
}