use dashmap::DashMap;
use std::sync::Arc;
use tower_lsp_server::ls_types::Uri;
use crate::Ecosystem;
pub struct EcosystemRegistry {
ecosystems: DashMap<&'static str, Arc<dyn Ecosystem>>,
filename_map: DashMap<&'static str, &'static str>,
extension_map: DashMap<&'static str, &'static str>,
patterns: DashMap<&'static str, (&'static str, &'static str, &'static str)>,
}
impl EcosystemRegistry {
pub fn new() -> Self {
Self {
ecosystems: DashMap::new(),
filename_map: DashMap::new(),
extension_map: DashMap::new(),
patterns: DashMap::new(),
}
}
pub fn register(&self, ecosystem: Arc<dyn Ecosystem>) {
let id = ecosystem.id();
for filename in ecosystem.manifest_filenames() {
self.filename_map.insert(*filename, id);
}
for extension in ecosystem.manifest_extensions() {
debug_assert!(
self.extension_map
.get(extension)
.is_none_or(|owner| *owner == id),
"extension {extension:?} already claimed by ecosystem {:?}, cannot also assign it to {id:?}",
self.extension_map.get(extension).map(|o| *o),
);
self.extension_map.insert(*extension, id);
}
for &pattern in ecosystem.manifest_patterns() {
let Some((prefix, suffix)) = pattern.split_once('*') else {
debug_assert!(
false,
"manifest pattern {pattern:?} must contain exactly one '*'"
);
continue;
};
debug_assert!(
!prefix.is_empty() || !suffix.is_empty(),
"manifest pattern {pattern:?} must not be a bare '*', which would claim every file"
);
debug_assert!(
!suffix.contains('*'),
"manifest pattern {pattern:?} must contain exactly one '*'"
);
debug_assert!(
self.patterns
.iter()
.all(|e| e.value().2 == id || *e.key() == pattern),
"pattern {pattern:?} would introduce a second ecosystem ({id:?}) into the \
single-owner pattern set; the most-specific-wins selection in \
get_for_filename assumes all patterns belong to one ecosystem"
);
self.patterns.insert(pattern, (prefix, suffix, id));
}
self.ecosystems.insert(id, ecosystem);
}
pub fn get(&self, id: &str) -> Option<Arc<dyn Ecosystem>> {
self.ecosystems.get(id).map(|e| Arc::clone(&e))
}
pub fn get_for_filename(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
if let Some(id) = self.filename_map.get(filename) {
return self.get(*id);
}
if let Some(id) = self.match_pattern(filename) {
return self.get(id);
}
if self.extension_map.is_empty() {
return None;
}
let (_, extension) = filename.rsplit_once('.')?;
let lowercased = format!(".{}", extension.to_lowercase());
let id = self.extension_map.get(lowercased.as_str())?;
self.get(*id)
}
fn match_pattern(&self, filename: &str) -> Option<&'static str> {
if self.patterns.is_empty() {
return None;
}
let mut best: Option<(usize, &'static str, &'static str)> = None; for e in self.patterns.iter() {
let (prefix, suffix, id) = *e.value();
if prefix_suffix_matches(filename, prefix, suffix) {
let score = prefix.len() + suffix.len();
let pattern = *e.key();
if best.is_none_or(|(s, p, _)| (score, pattern) > (s, p)) {
best = Some((score, pattern, id));
}
}
}
best.map(|(_, _, id)| id)
}
pub fn get_for_uri(&self, uri: &Uri) -> Option<Arc<dyn Ecosystem>> {
let path = uri.path().as_str();
let filename = path.rsplit('/').next()?;
if let Some(ecosystem) = self.get_for_filename(filename) {
return Some(ecosystem);
}
self.get_for_directory_pattern(path, filename)
}
fn get_for_directory_pattern(&self, path: &str, filename: &str) -> Option<Arc<dyn Ecosystem>> {
for entry in self.ecosystems.iter() {
let ecosystem = entry.value();
let matches =
ecosystem
.manifest_directory_patterns()
.iter()
.any(|(dir_pattern, suffix)| {
directory_pattern_matches(path, filename, dir_pattern, suffix)
});
if matches {
return Some(Arc::clone(ecosystem));
}
}
None
}
pub fn ecosystem_ids(&self) -> Vec<&'static str> {
self.ecosystems.iter().map(|e| *e.key()).collect()
}
pub fn get_for_lockfile(&self, filename: &str) -> Option<Arc<dyn Ecosystem>> {
for entry in self.ecosystems.iter() {
let ecosystem = entry.value();
let matches = ecosystem
.lockfile_filenames()
.iter()
.any(|pattern| lockfile_pattern_matches(pattern, filename));
if matches {
return Some(Arc::clone(ecosystem));
}
}
None
}
pub fn all_lockfile_patterns(&self) -> Vec<String> {
let mut patterns = Vec::new();
for entry in self.ecosystems.iter() {
let ecosystem = entry.value();
for filename in ecosystem.lockfile_filenames() {
patterns.push(format!("**/{}", filename));
}
}
patterns
}
}
impl Default for EcosystemRegistry {
fn default() -> Self {
Self::new()
}
}
fn lockfile_pattern_matches(pattern: &str, filename: &str) -> bool {
match pattern.split_once('*') {
Some((prefix, suffix)) => prefix_suffix_matches(filename, prefix, suffix),
None => pattern == filename,
}
}
fn prefix_suffix_matches(filename: &str, prefix: &str, suffix: &str) -> bool {
filename.len() >= prefix.len() + suffix.len()
&& filename.starts_with(prefix)
&& filename.ends_with(suffix)
}
fn directory_pattern_matches(path: &str, filename: &str, dir_pattern: &str, suffix: &str) -> bool {
if !filename.ends_with(suffix) {
return false;
}
let Some(dir_path) = path.strip_suffix(filename) else {
return false;
};
let dir_path = dir_path.trim_end_matches('/');
match dir_path.len().checked_sub(dir_pattern.len()) {
Some(0) => dir_path == dir_pattern,
Some(n) => dir_path.ends_with(dir_pattern) && dir_path.as_bytes()[n - 1] == b'/',
None => false,
}
}
#[must_use]
pub fn manifest_pattern_matches(filename: &str, pattern: &str) -> bool {
match pattern.split_once('*') {
Some((prefix, suffix)) if !suffix.contains('*') => {
prefix_suffix_matches(filename, prefix, suffix)
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::any::Any;
use tower_lsp_server::ls_types::Position;
use crate::{
ConcreteVersion, PackageName, ParseResult, Registry,
completion::Completions,
lsp_helpers::{
DiagnosticMessages, DiagnosticPolicy, EcosystemFormatter, OsvNaming, PackageNaming,
PackageRendering, RequirementResolution, SourcePolicy,
},
};
struct MockFormatter;
impl PackageNaming for MockFormatter {}
impl PackageRendering for MockFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for MockFormatter {}
impl DiagnosticMessages for MockFormatter {}
impl DiagnosticPolicy for MockFormatter {}
impl SourcePolicy for MockFormatter {}
impl OsvNaming for MockFormatter {}
struct MockEcosystem {
id: &'static str,
display_name: &'static str,
filenames: &'static [&'static str],
lockfiles: &'static [&'static str],
}
impl crate::ecosystem::private::Sealed for MockEcosystem {}
impl Ecosystem for MockEcosystem {
fn id(&self) -> &'static str {
self.id
}
fn display_name(&self) -> &'static str {
self.display_name
}
fn manifest_filenames(&self) -> &[&'static str] {
self.filenames
}
fn lockfile_filenames(&self) -> &[&'static str] {
self.lockfiles
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a Uri,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(async move { unimplemented!() })
}
fn registry(&self) -> Arc<dyn Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn EcosystemFormatter {
&MockFormatter
}
fn generate_completions<'a>(
&'a self,
_parse_result: &'a dyn ParseResult,
_position: Position,
_content: &'a str,
_freshness: crate::FreshnessSettings,
) -> crate::ecosystem::BoxFuture<'a, Completions> {
Box::pin(async move { Completions::default() })
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockExtEcosystem {
id: &'static str,
filenames: &'static [&'static str],
extensions: &'static [&'static str],
}
impl crate::ecosystem::private::Sealed for MockExtEcosystem {}
impl Ecosystem for MockExtEcosystem {
fn id(&self) -> &'static str {
self.id
}
fn display_name(&self) -> &'static str {
self.id
}
fn manifest_filenames(&self) -> &[&'static str] {
self.filenames
}
fn manifest_extensions(&self) -> &[&'static str] {
self.extensions
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a Uri,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(async move { unimplemented!() })
}
fn registry(&self) -> Arc<dyn Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn EcosystemFormatter {
&MockFormatter
}
fn generate_completions<'a>(
&'a self,
_parse_result: &'a dyn ParseResult,
_position: Position,
_content: &'a str,
_freshness: crate::FreshnessSettings,
) -> crate::ecosystem::BoxFuture<'a, Completions> {
Box::pin(async move { Completions::default() })
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockPatternEcosystem {
id: &'static str,
patterns: &'static [&'static str],
dir_patterns: &'static [(&'static str, &'static str)],
}
impl crate::ecosystem::private::Sealed for MockPatternEcosystem {}
impl Ecosystem for MockPatternEcosystem {
fn id(&self) -> &'static str {
self.id
}
fn display_name(&self) -> &'static str {
self.id
}
fn manifest_filenames(&self) -> &[&'static str] {
&[]
}
fn manifest_patterns(&self) -> &[&'static str] {
self.patterns
}
fn manifest_directory_patterns(&self) -> &[(&'static str, &'static str)] {
self.dir_patterns
}
fn parse_manifest<'a>(
&'a self,
_content: &'a str,
_uri: &'a Uri,
) -> crate::ecosystem::BoxFuture<'a, crate::error::Result<Box<dyn ParseResult>>> {
Box::pin(async move { unimplemented!() })
}
fn registry(&self) -> Arc<dyn Registry> {
unimplemented!()
}
fn formatter(&self) -> &dyn EcosystemFormatter {
&MockFormatter
}
fn generate_completions<'a>(
&'a self,
_parse_result: &'a dyn ParseResult,
_position: Position,
_content: &'a str,
_freshness: crate::FreshnessSettings,
) -> crate::ecosystem::BoxFuture<'a, Completions> {
Box::pin(async move { Completions::default() })
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn pypi_pattern_registry() -> EcosystemRegistry {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockPatternEcosystem {
id: "pypi",
patterns: &[
"requirements*.txt",
"*-requirements.txt",
"*.requirements.txt",
"constraints*.txt",
],
dir_patterns: &[("requirements", ".txt")],
}));
registry
}
fn gha_pattern_registry() -> EcosystemRegistry {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockPatternEcosystem {
id: "github-actions",
patterns: &[],
dir_patterns: &[
(".github/workflows", ".yml"),
(".github/workflows", ".yaml"),
],
}));
registry
}
#[test]
fn test_get_for_uri_multi_segment_directory_pattern_matches_yml_and_yaml() {
let registry = gha_pattern_registry();
for path in [
"/repo/.github/workflows/ci.yml",
"/repo/.github/workflows/release.yaml",
] {
let uri = crate::test_util::test_uri(path);
assert_eq!(
registry.get_for_uri(&uri).map(|e| e.id()),
Some("github-actions"),
"{path} should match the .github/workflows/*.y[a]ml directory pattern"
);
}
}
#[test]
fn test_get_for_uri_multi_segment_directory_pattern_matches_nested_repo() {
let registry = gha_pattern_registry();
let uri = crate::test_util::test_uri("/home/user/a/b/.github/workflows/x.yml");
assert_eq!(
registry.get_for_uri(&uri).map(|e| e.id()),
Some("github-actions"),
"a repo nested under arbitrary ancestor directories should still match"
);
}
#[test]
fn test_get_for_uri_multi_segment_directory_pattern_rejects_partial_paths() {
let registry = gha_pattern_registry();
for path in [
"/repo/workflows/x.yml",
"/repo/.github/x.yml",
"/repo/my.github/workflows/x.yml",
] {
let uri = crate::test_util::test_uri(path);
assert!(
registry.get_for_uri(&uri).is_none(),
"{path} should not match the .github/workflows/*.y[a]ml directory pattern"
);
}
}
#[test]
fn test_get_for_filename_pattern_matches_requirements_variants() {
let registry = pypi_pattern_registry();
for name in [
"requirements.txt",
"requirements-dev.txt",
"requirements.dev.txt",
"requirements_test.txt",
"dev-requirements.txt",
"test.requirements.txt",
"constraints.txt",
"constraints-prod.txt",
] {
assert_eq!(
registry.get_for_filename(name).map(|e| e.id()),
Some("pypi"),
"{name} should match a PyPI pattern"
);
}
}
#[test]
fn test_get_for_filename_pattern_does_not_match_unrelated_files() {
let registry = pypi_pattern_registry();
for name in [
"notes.txt",
"LICENSE.txt",
"myrequirements.txt",
"requirements.txt.bak",
"Requirements.txt",
"requirements",
] {
assert!(
registry.get_for_filename(name).is_none(),
"{name} should not match any PyPI pattern"
);
}
}
#[test]
fn test_get_for_filename_exact_name_wins_over_pattern() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockEcosystem {
id: "exact",
display_name: "Exact",
filenames: &["requirements.txt"],
lockfiles: &[],
}));
registry.register(Arc::new(MockPatternEcosystem {
id: "pattern",
patterns: &["requirements*.txt"],
dir_patterns: &[],
}));
assert_eq!(
registry.get_for_filename("requirements.txt").unwrap().id(),
"exact"
);
}
#[test]
fn test_get_for_filename_pattern_wins_over_extension() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockExtEcosystem {
id: "ext",
filenames: &[],
extensions: &[".txt"],
}));
registry.register(Arc::new(MockPatternEcosystem {
id: "pattern",
patterns: &["requirements*.txt"],
dir_patterns: &[],
}));
assert_eq!(
registry.get_for_filename("requirements.txt").unwrap().id(),
"pattern"
);
}
#[test]
fn test_get_for_filename_pattern_most_specific_wins_deterministically() {
let registry = pypi_pattern_registry();
assert_eq!(
registry
.get_for_filename("requirements.requirements.txt")
.unwrap()
.id(),
"pypi"
);
}
#[test]
fn test_new_registry_is_empty() {
let registry = EcosystemRegistry::new();
assert_eq!(registry.ecosystem_ids().len(), 0);
}
#[test]
fn test_register_ecosystem() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "test",
display_name: "Test Ecosystem",
filenames: &["test.toml"],
lockfiles: &[],
});
registry.register(ecosystem);
assert_eq!(registry.ecosystem_ids().len(), 1);
assert!(registry.get("test").is_some());
}
#[test]
fn test_get_by_id() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "test",
display_name: "Test Ecosystem",
filenames: &["test.toml"],
lockfiles: &[],
});
registry.register(ecosystem);
let retrieved = registry.get("test").unwrap();
assert_eq!(retrieved.id(), "test");
assert_eq!(retrieved.display_name(), "Test Ecosystem");
}
#[test]
fn test_get_by_filename() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "test",
display_name: "Test Ecosystem",
filenames: &["test.toml", "test.json"],
lockfiles: &[],
});
registry.register(ecosystem);
let retrieved1 = registry.get_for_filename("test.toml").unwrap();
assert_eq!(retrieved1.id(), "test");
let retrieved2 = registry.get_for_filename("test.json").unwrap();
assert_eq!(retrieved2.id(), "test");
assert!(registry.get_for_filename("unknown.toml").is_none());
}
#[test]
fn test_get_by_uri() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "test",
display_name: "Test Ecosystem",
filenames: &["test.toml"],
lockfiles: &[],
});
registry.register(ecosystem);
let uri = crate::test_util::test_uri("/home/user/project/test.toml");
let retrieved = registry.get_for_uri(&uri).unwrap();
assert_eq!(retrieved.id(), "test");
let unknown_uri = crate::test_util::test_uri("/home/user/project/unknown.toml");
assert!(registry.get_for_uri(&unknown_uri).is_none());
}
#[test]
fn test_get_for_uri_directory_pattern_matches_split_requirements_layout() {
let registry = pypi_pattern_registry();
for path in [
"/home/user/project/requirements/base.txt",
"/home/user/project/requirements/dev.txt",
"/home/user/project/sub/requirements/prod.txt",
] {
let uri = crate::test_util::test_uri(path);
assert_eq!(
registry.get_for_uri(&uri).map(|e| e.id()),
Some("pypi"),
"{path} should match the requirements/*.txt directory pattern"
);
}
}
#[test]
fn test_get_for_uri_directory_pattern_requires_matching_directory_and_suffix() {
let registry = pypi_pattern_registry();
for path in [
"/home/user/project/reqs/base.txt",
"/home/user/project/requirements/base.cfg",
"/home/user/project/requirements.txt",
] {
let uri = crate::test_util::test_uri(path);
if path.ends_with("requirements.txt") {
assert_eq!(registry.get_for_uri(&uri).map(|e| e.id()), Some("pypi"));
} else {
assert!(
registry.get_for_uri(&uri).is_none(),
"{path} should not match the requirements/*.txt directory pattern"
);
}
}
}
#[test]
fn test_get_for_uri_directory_pattern_requires_exact_directory_segment() {
for path in [
"/home/user/project/requirements.txt/base.txt",
"/home/user/project/myrequirements/base.txt",
] {
let uri = crate::test_util::test_uri(path);
assert!(
pypi_pattern_registry().get_for_uri(&uri).is_none(),
"{path} should not match the requirements/*.txt directory pattern"
);
}
}
#[test]
fn test_get_for_uri_basename_pattern_wins_over_directory_pattern() {
let registry = pypi_pattern_registry();
let uri =
crate::test_util::test_uri("/home/user/project/requirements/dev-requirements.txt");
assert_eq!(registry.get_for_uri(&uri).map(|e| e.id()), Some("pypi"));
}
#[test]
fn test_multiple_ecosystems() {
let registry = EcosystemRegistry::new();
let eco1 = Arc::new(MockEcosystem {
id: "cargo",
display_name: "Cargo",
filenames: &["Cargo.toml"],
lockfiles: &["Cargo.lock"],
});
let eco2 = Arc::new(MockEcosystem {
id: "npm",
display_name: "npm",
filenames: &["package.json"],
lockfiles: &["package-lock.json"],
});
registry.register(eco1);
registry.register(eco2);
assert_eq!(registry.ecosystem_ids().len(), 2);
assert_eq!(
registry.get_for_filename("Cargo.toml").unwrap().id(),
"cargo"
);
assert_eq!(
registry.get_for_filename("package.json").unwrap().id(),
"npm"
);
}
#[test]
fn test_get_for_lockfile() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "cargo",
display_name: "Cargo",
filenames: &["Cargo.toml"],
lockfiles: &["Cargo.lock"],
});
registry.register(ecosystem);
let retrieved = registry.get_for_lockfile("Cargo.lock").unwrap();
assert_eq!(retrieved.id(), "cargo");
assert_eq!(retrieved.display_name(), "Cargo");
assert!(registry.get_for_lockfile("unknown.lock").is_none());
}
#[test]
fn test_get_for_lockfile_multiple_lockfiles() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "pypi",
display_name: "PyPI",
filenames: &["pyproject.toml"],
lockfiles: &["poetry.lock", "uv.lock"],
});
registry.register(ecosystem);
let retrieved1 = registry.get_for_lockfile("poetry.lock").unwrap();
assert_eq!(retrieved1.id(), "pypi");
let retrieved2 = registry.get_for_lockfile("uv.lock").unwrap();
assert_eq!(retrieved2.id(), "pypi");
}
#[test]
fn test_get_for_lockfile_matches_wildcard_pattern() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "nuget",
display_name: "NuGet",
filenames: &["Directory.Packages.props"],
lockfiles: &["packages.lock.json", "packages.*.lock.json"],
});
registry.register(ecosystem);
assert_eq!(
registry
.get_for_lockfile("packages.App1.lock.json")
.map(|e| e.id()),
Some("nuget")
);
assert_eq!(
registry
.get_for_lockfile("packages.lock.json")
.map(|e| e.id()),
Some("nuget")
);
assert!(registry.get_for_lockfile("other.lock.json").is_none());
assert!(registry.get_for_lockfile("packages.lock").is_none());
}
#[test]
fn test_all_lockfile_patterns_empty() {
let registry = EcosystemRegistry::new();
assert!(registry.all_lockfile_patterns().is_empty());
}
#[test]
fn test_all_lockfile_patterns_single_ecosystem() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "cargo",
display_name: "Cargo",
filenames: &["Cargo.toml"],
lockfiles: &["Cargo.lock"],
});
registry.register(ecosystem);
let patterns = registry.all_lockfile_patterns();
assert_eq!(patterns.len(), 1);
assert_eq!(patterns[0], "**/Cargo.lock");
}
#[test]
fn test_all_lockfile_patterns_multiple_ecosystems() {
let registry = EcosystemRegistry::new();
let eco1 = Arc::new(MockEcosystem {
id: "cargo",
display_name: "Cargo",
filenames: &["Cargo.toml"],
lockfiles: &["Cargo.lock"],
});
let eco2 = Arc::new(MockEcosystem {
id: "npm",
display_name: "npm",
filenames: &["package.json"],
lockfiles: &["package-lock.json"],
});
let eco3 = Arc::new(MockEcosystem {
id: "pypi",
display_name: "PyPI",
filenames: &["pyproject.toml"],
lockfiles: &["poetry.lock", "uv.lock"],
});
registry.register(eco1);
registry.register(eco2);
registry.register(eco3);
let patterns = registry.all_lockfile_patterns();
assert_eq!(patterns.len(), 4);
assert!(patterns.contains(&"**/Cargo.lock".to_string()));
assert!(patterns.contains(&"**/package-lock.json".to_string()));
assert!(patterns.contains(&"**/poetry.lock".to_string()));
assert!(patterns.contains(&"**/uv.lock".to_string()));
}
#[test]
fn test_all_lockfile_patterns_no_lockfiles() {
let registry = EcosystemRegistry::new();
let ecosystem = Arc::new(MockEcosystem {
id: "test",
display_name: "Test",
filenames: &["test.toml"],
lockfiles: &[],
});
registry.register(ecosystem);
let patterns = registry.all_lockfile_patterns();
assert!(patterns.is_empty());
}
#[test]
fn test_get_for_filename_extension_fallback() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockExtEcosystem {
id: "nuget",
filenames: &["Directory.Packages.props"],
extensions: &[".csproj", ".fsproj"],
}));
assert_eq!(
registry.get_for_filename("MyApp.csproj").unwrap().id(),
"nuget"
);
assert_eq!(
registry
.get_for_filename("Directory.Packages.props")
.unwrap()
.id(),
"nuget"
);
assert!(registry.get_for_filename("unrelated.txt").is_none());
}
#[test]
fn test_get_for_filename_extension_fallback_case_insensitive() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockExtEcosystem {
id: "nuget",
filenames: &["Directory.Packages.props"],
extensions: &[".csproj"],
}));
assert_eq!(
registry.get_for_filename("MyApp.CSPROJ").unwrap().id(),
"nuget"
);
}
#[test]
fn test_get_for_filename_exact_match_case_sensitive_not_shadowed_by_extension() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockExtEcosystem {
id: "nuget",
filenames: &["packages.config"],
extensions: &[],
}));
assert!(registry.get_for_filename("packages.Config").is_none());
}
#[test]
fn test_get_for_filename_no_extension_returns_none() {
let registry = EcosystemRegistry::new();
registry.register(Arc::new(MockExtEcosystem {
id: "nuget",
filenames: &[],
extensions: &[".csproj"],
}));
assert!(registry.get_for_filename("README").is_none());
}
}