pub fn ecosystem_from_path(path: &str) -> Option<&'static str> {
let name = path.rsplit(['/', '\\']).next().unwrap_or(path);
let eco = match name {
"package.json" | "package-lock.json" | "npm-shrinkwrap.json" | "yarn.lock"
| "pnpm-lock.yaml" => "npm",
"requirements.txt" | "Pipfile" | "Pipfile.lock" | "pyproject.toml" | "poetry.lock"
| "setup.py" => "pypi",
"Cargo.toml" | "Cargo.lock" => "crates",
"go.mod" | "go.sum" => "go",
"pom.xml" | "build.gradle" | "build.gradle.kts" => "maven",
"composer.json" | "composer.lock" => "composer",
"Gemfile" | "Gemfile.lock" => "rubygems",
"packages.config" => "nuget",
_ => "",
};
if !eco.is_empty() {
return Some(eco);
}
if name.starts_with("requirements-") && name.ends_with(".txt") {
return Some("pypi");
}
if name.ends_with(".csproj") {
return Some("nuget");
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_manifests_map_to_their_ecosystem() {
let cases = [
("package.json", "npm"),
("package-lock.json", "npm"),
("yarn.lock", "npm"),
("pnpm-lock.yaml", "npm"),
("requirements.txt", "pypi"),
("pyproject.toml", "pypi"),
("Pipfile", "pypi"),
("poetry.lock", "pypi"),
("Cargo.toml", "crates"),
("Cargo.lock", "crates"),
("go.mod", "go"),
("go.sum", "go"),
("pom.xml", "maven"),
("build.gradle", "maven"),
("build.gradle.kts", "maven"),
("composer.json", "composer"),
("composer.lock", "composer"),
("Gemfile", "rubygems"),
("Gemfile.lock", "rubygems"),
("packages.config", "nuget"),
];
for (file, eco) in cases {
assert_eq!(ecosystem_from_path(file), Some(eco), "{file}");
}
}
#[test]
fn cleanlib_864_pub_is_no_longer_detected_no_backend_exists() {
assert_eq!(ecosystem_from_path("pubspec.yaml"), None);
assert_eq!(ecosystem_from_path("pubspec.lock"), None);
}
#[test]
fn inspects_only_the_basename_of_a_full_path() {
assert_eq!(ecosystem_from_path("/home/dev/proj/package.json"), Some("npm"));
assert_eq!(ecosystem_from_path("./sub/dir/Cargo.toml"), Some("crates"));
assert_eq!(ecosystem_from_path(r"C:\proj\go.mod"), Some("go"));
}
#[test]
fn patterned_names_are_detected() {
assert_eq!(ecosystem_from_path("requirements-dev.txt"), Some("pypi"));
assert_eq!(ecosystem_from_path("requirements-test.txt"), Some("pypi"));
assert_eq!(ecosystem_from_path("MyApp.csproj"), Some("nuget"));
}
#[test]
fn unrecognised_or_ambiguous_returns_none() {
assert_eq!(ecosystem_from_path("deps.txt"), None);
assert_eq!(ecosystem_from_path("README.md"), None);
assert_eq!(ecosystem_from_path("manifest"), None);
assert_eq!(ecosystem_from_path(""), None);
}
#[test]
fn every_detected_ecosystem_is_wire_accepted() {
const WIRE_ACCEPTED: [&str; 8] =
["npm", "pypi", "go", "maven", "crates", "nuget", "rubygems", "composer"];
let manifests = [
"package.json",
"package-lock.json",
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
"requirements.txt",
"Pipfile",
"Pipfile.lock",
"pyproject.toml",
"poetry.lock",
"setup.py",
"Cargo.toml",
"Cargo.lock",
"go.mod",
"go.sum",
"pom.xml",
"build.gradle",
"build.gradle.kts",
"composer.json",
"composer.lock",
"Gemfile",
"Gemfile.lock",
"packages.config",
"pubspec.yaml",
"pubspec.lock",
"requirements-dev.txt",
"MyApp.csproj",
];
for m in manifests {
let Some(eco) = ecosystem_from_path(m) else {
continue;
};
assert!(
WIRE_ACCEPTED.contains(&eco),
"{m} detects as `{eco}`, which cleanlib-app's SUPPORTED_ECOSYSTEMS rejects"
);
}
}
}