/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/tools/pm/outdated/mod.rs
934 строки
28 KB
Bartek Iwańczuk
fix(update): don't downgrade lockfile when npm cache is stale (#35904)
20 июл 2026, 15:05
Не верифицирован
20 июл 2026, 15:05
f2ac97c
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. mod interactive; use std::collections::HashSet; use std::sync::Arc; use deno_cache_dir::GlobalOrLocalHttpCache; use deno_cache_dir::file_fetcher::CacheSetting; use deno_core::anyhow::bail; use deno_core::error::AnyError; use deno_semver::StackString; use deno_semver::VersionReq; use deno_semver::package::PackageNv; use deno_semver::package::PackageReq; use deno_terminal::colors; use super::CacheTopLevelDepsOptions; use super::deps::Dep; use super::deps::DepId; use super::deps::DepKind; use super::deps::DepManager; use super::deps::DepManagerArgs; use super::deps::PackageLatestVersion; use crate::args::Flags; use crate::args::OutdatedFlags; use crate::factory::CliFactory; use crate::file_fetcher::CreateCliFileFetcherOptions; use crate::file_fetcher::create_cli_file_fetcher; use crate::jsr::JsrFetchResolver; use crate::npm::NpmFetchResolver; use crate::npm::PackageInfoLoadError; /// Packages whose metadata could not be fetched (e.g. unreachable or /// unauthorized private registries) are dropped from the update check. They are /// collected here, keyed and deduplicated by `(kind, name)`, so we can warn the /// user instead of skipping them silently. type SkippedPackages = std::collections::BTreeMap<(DepKind, StackString), Arc<PackageInfoLoadError>>; #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] struct OutdatedPackage { kind: DepKind, latest: String, semver_compatible: String, current: String, name: StackString, } #[allow(clippy::print_stdout, reason = "print method")] fn print_outdated_table(packages: &[OutdatedPackage]) { const HEADINGS: &[&str] = &["Package", "Current", "Update", "Latest"]; let mut longest_package = 0; let mut longest_current = 0; let mut longest_update = 0; let mut longest_latest = 0; for package in packages { let name_len = package.kind.scheme().len() + 1 + package.name.len(); longest_package = longest_package.max(name_len); longest_current = longest_current.max(package.current.len()); longest_update = longest_update.max(package.semver_compatible.len()); longest_latest = longest_latest.max(package.latest.len()); } let package_column_width = longest_package.max(HEADINGS[0].len()) + 2; let current_column_width = longest_current.max(HEADINGS[1].len()) + 2; let update_column_width = longest_update.max(HEADINGS[2].len()) + 2; let latest_column_width = longest_latest.max(HEADINGS[3].len()) + 2; let package_fill = "─".repeat(package_column_width); let current_fill = "─".repeat(current_column_width); let update_fill = "─".repeat(update_column_width); let latest_fill = "─".repeat(latest_column_width); println!("┌{package_fill}┬{current_fill}┬{update_fill}┬{latest_fill}┐"); println!( "│ {}{} │ {}{} │ {}{} │ {}{} │", colors::intense_blue(HEADINGS[0]), " ".repeat(package_column_width - 2 - HEADINGS[0].len()), colors::intense_blue(HEADINGS[1]), " ".repeat(current_column_width - 2 - HEADINGS[1].len()), colors::intense_blue(HEADINGS[2]), " ".repeat(update_column_width - 2 - HEADINGS[2].len()), colors::intense_blue(HEADINGS[3]), " ".repeat(latest_column_width - 2 - HEADINGS[3].len()) ); for package in packages { println!("├{package_fill}┼{current_fill}┼{update_fill}┼{latest_fill}┤",); print!( "│ {:<package_column_width$} ", format!("{}:{}", package.kind.scheme(), package.name), package_column_width = package_column_width - 2 ); print!( "│ {:<current_column_width$} ", package.current, current_column_width = current_column_width - 2 ); print!( "│ {:<update_column_width$} ", package.semver_compatible, update_column_width = update_column_width - 2 ); println!( "│ {:<latest_column_width$} │", package.latest, latest_column_width = latest_column_width - 2 ); } println!("└{package_fill}┴{current_fill}┴{update_fill}┴{latest_fill}┘",); } fn print_suggestion(compatible: bool) { log::info!(""); let (cmd, txt) = if compatible { ("", "compatible") } else { (" --latest", "available") }; log::info!( "{}", color_print::cformat!( "<p(245)>Run</> <u>deno update{}</> <p(245)>to update to the latest {} versions,</>\n<p(245)>or</> <u>deno outdated --help</> <p(245)>for more information.</>", cmd, txt, ) ); } fn print_outdated( deps: &mut DepManager, compatible: bool, ) -> Result<(), AnyError> { let mut outdated = Vec::new(); let mut seen = std::collections::BTreeSet::new(); let mut skipped = SkippedPackages::new(); for (dep_id, resolved, latest_versions) in deps.deps_with_resolved_latest_versions() { let dep = deps.get_dep(dep_id); collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions); let Some(resolved) = resolved else { continue }; let latest = { let preferred = if compatible { &latest_versions.semver_compatible } else { &latest_versions.latest }; if let Some(v) = preferred { v } else { continue; } }; if latest > &resolved && seen.insert((dep.kind, dep.req.name.clone(), resolved.version.clone())) { outdated.push(OutdatedPackage { kind: dep.kind, name: dep.req.name.clone(), current: resolved.version.to_string(), latest: latest_versions .latest .map(|l| l.version.to_string()) .unwrap_or_default(), semver_compatible: latest_versions .semver_compatible .map(|l| l.version.to_string()) .unwrap_or_default(), }) } } if !outdated.is_empty() { outdated.sort(); print_outdated_table(&outdated); } print_skipped_warning(&skipped); if !outdated.is_empty() { print_suggestion(compatible); } Ok(()) } /// Records a package whose metadata fetch failed, deduplicating by /// `(kind, name)` and keeping the first reason seen. fn collect_skipped( skipped: &mut SkippedPackages, kind: DepKind, name: &StackString, latest_versions: &PackageLatestVersion, ) { if let Some(error) = &latest_versions.fetch_error { skipped .entry((kind, name.clone())) .or_insert_with(|| error.clone()); } } fn print_skipped_warning(skipped: &SkippedPackages) { if skipped.is_empty() { return; } log::warn!(""); log::warn!( "{}", color_print::cformat!( "<yellow>Warning</>: Unable to check updates for {} package(s), their metadata could not be fetched (e.g. private registry auth or network issues).", skipped.len(), ) ); if log::log_enabled!(log::Level::Debug) { for ((kind, name), error) in skipped { log::warn!( "{}", color_print::cformat!( " <bold>{}:{}</> (registry: {})", kind.scheme(), name, error.registry_url, ) ); log::warn!(" Reason: {}", error.reason); } log::warn!( "{}", color_print::cformat!( "<p(245)>Verify your registry credentials (.npmrc) and network connectivity.</>" ) ); } else { log::warn!( "{}", color_print::cformat!( "<p(245)>Run with </><u>--log-level=debug</><p(245)> for details on which packages could not be checked.</>" ) ); } } pub async fn outdated( flags: Arc<Flags>, update_flags: OutdatedFlags, ) -> Result<(), AnyError> { let factory = CliFactory::from_flags(flags.clone()); let cli_options = factory.cli_options()?; let workspace = cli_options.workspace(); let http_client = factory.http_client_provider(); let deps_http_cache = factory.global_http_cache()?; let file_fetcher = create_cli_file_fetcher( Arc::new(deno_runtime::deno_web::BlobStore::default()) as Arc<dyn deno_runtime::deno_web::BlobStoreTrait>, GlobalOrLocalHttpCache::Global(deps_http_cache.clone()), http_client.clone(), factory.memory_files().clone(), factory.sys(), CreateCliFileFetcherOptions { allow_remote: true, cache_setting: CacheSetting::RespectHeaders, download_log_level: log::Level::Trace, progress_bar: None, }, ); let file_fetcher = Arc::new(file_fetcher); let npm_fetch_resolver = Arc::new(NpmFetchResolver::new( file_fetcher.clone(), factory.npmrc()?.clone(), factory.npm_version_resolver()?.clone(), )); let jsr_fetch_resolver = Arc::new(JsrFetchResolver::new( file_fetcher.clone(), factory.jsr_version_resolver()?.clone(), )); if !cli_options.start_dir.has_deno_or_pkg_json() { bail!( "No deno.json or package.json in \"{}\".", cli_options.initial_cwd().display(), ); } let args = dep_manager_args( &factory, npm_fetch_resolver.clone(), jsr_fetch_resolver.clone(), ) .await?; let filter_set = filter::FilterSet::from_filter_strings( update_flags.filters.iter().map(|s| s.as_str()), )?; let filter_fn = |alias: Option<&str>, req: &PackageReq, _: DepKind| { if filter_set.is_empty() { return true; } let name = alias.unwrap_or(&req.name); filter_set.matches(name) }; let mut deps = if update_flags.recursive { super::deps::DepManager::from_workspace(workspace, filter_fn, args)? } else { super::deps::DepManager::from_workspace_dir( &cli_options.start_dir, filter_fn, args, )? }; deps.resolve_versions().await?; match update_flags.kind { crate::args::OutdatedKind::Update { latest, interactive, lockfile_only, } => { update( deps, latest, &filter_set, interactive, flags, CacheTopLevelDepsOptions { lockfile_only, additional_roots: vec![], }, ) .await?; } crate::args::OutdatedKind::PrintOutdated { compatible } => { print_outdated(&mut deps, compatible)?; } } Ok(()) } enum ChosenVersionReq { Some(VersionReq), None { latest_available: bool }, } fn choose_new_version_req( dep: &Dep, resolved: Option<&PackageNv>, latest_versions: &PackageLatestVersion, update_to_latest: bool, filter_set: &filter::FilterSet, ) -> ChosenVersionReq { let explicit_version_req = filter_set .matching_filter(dep.alias.as_deref().unwrap_or(&dep.req.name)) .version_spec() .cloned(); if let Some(version_req) = explicit_version_req { if let Some(resolved) = resolved { // todo(nathanwhit): handle tag if version_req.tag().is_none() && version_req.matches(&resolved.version) { return ChosenVersionReq::None { latest_available: false, }; } } ChosenVersionReq::Some(version_req) } else { let Some(resolved) = resolved else { return ChosenVersionReq::None { latest_available: false, }; }; let Some(preferred) = (if update_to_latest { latest_versions.latest.as_ref() } else { latest_versions.semver_compatible.as_ref() }) else { return ChosenVersionReq::None { latest_available: false, }; }; // Detect the original operator to preserve it let version_req_str = dep.req.version_req.to_string(); let operator = if version_req_str.starts_with('~') { "~" } else if version_req_str.starts_with('^') { "^" } else { // Check if it's an exact version (no operator) let exact = if let Some(range) = dep.req.version_req.range() { range.0[0].start == range.0[0].end } else { false }; if exact { "" } else { "^" } }; let candidate_version_req = VersionReq::parse_from_specifier( format!("{}{}", operator, preferred.version).as_str(), ) .unwrap(); if preferred.version <= resolved.version && (candidate_version_req == dep.req.version_req || preferred.version < resolved.version) { return ChosenVersionReq::None { latest_available: !update_to_latest && latest_versions .latest .as_ref() .is_some_and(|nv| nv.version > resolved.version), }; } ChosenVersionReq::Some(candidate_version_req) } } struct ToUpdate { dep_id: DepId, package_name: String, current_version: Option<PackageNv>, current_version_req: VersionReq, new_version_req: VersionReq, // Whether the npm registry metadata for this package must be refetched // before the post-modification install re-resolves it. See the comment at // the blocklist construction below for why. force_reload_npm: bool, } async fn update( mut deps: DepManager, update_to_latest: bool, filter_set: &filter::FilterSet, interactive: bool, flags: Arc<Flags>, cache_options: CacheTopLevelDepsOptions, ) -> Result<(), AnyError> { let mut to_update = Vec::new(); let mut can_update_to_latest = false; let mut skipped = SkippedPackages::new(); for (dep_id, resolved, latest_versions) in deps .deps_with_resolved_latest_versions() .into_iter() .collect::<Vec<_>>() { let dep = deps.get_dep(dep_id); collect_skipped(&mut skipped, dep.kind, &dep.req.name, &latest_versions); let new_version_req = choose_new_version_req( dep, resolved.as_ref(), &latest_versions, update_to_latest, filter_set, ); let new_version_req = match new_version_req { ChosenVersionReq::Some(version_req) => version_req, ChosenVersionReq::None { latest_available } => { can_update_to_latest = can_update_to_latest || latest_available; continue; } }; // The post-modification install re-resolves each package through the npm // installer, which reads the npm registry cache with the default // `CacheSetting::Use`. That cache is *separate* from the one behind // `latest_versions` (the `NpmFetchResolver` fetched those fresh with // `RespectHeaders`), so it can be stale in ways the displayed "latest" // versions don't reveal. A stale installer cache re-resolves to whatever // version it happens to know about, which can move the lockfile up (hiding // a newer in-range version, #35348) or, worse, *down* -- silently // downgrading a lockfile entry that was pinned ahead of the stale cache // (#35822). Comparing `latest_versions` against the resolved version can't // detect this: both can read a fresh newest version while the installer // cache lags behind. So refetch the installer's metadata for every npm // package we might re-resolve. let force_reload_npm = dep.kind == DepKind::Npm; to_update.push(ToUpdate { dep_id, package_name: format!("{}:{}", dep.kind.scheme(), dep.req.name), current_version: resolved.clone(), current_version_req: dep.req.version_req.clone(), new_version_req: new_version_req.clone(), force_reload_npm, }); } if interactive && !to_update.is_empty() { let selected = interactive::select_interactive( to_update .iter() .map(|to_update: &ToUpdate| { let dep = deps.get_dep(to_update.dep_id); interactive::PackageInfo { id: to_update.dep_id, current_version_req: to_update.current_version_req.clone(), name: dep.alias_or_name().into(), kind: dep.kind, new_version: to_update.new_version_req.clone(), } }) .collect(), )?; if let Some(selected) = selected { to_update.retain(|to_update| selected.contains(&to_update.dep_id)); } else { log::info!("Cancelled, not updating"); return Ok(()); } } if !to_update.is_empty() { let lockfile_only = cache_options.lockfile_only; if lockfile_only { // `--lockfile-only`: don't touch deno.json/package.json. Instead, // remove the existing lockfile entries for the deps so the // following install re-resolves them to the latest version within // their existing ranges. deps.invalidate_lockfile_for_update( to_update.iter().map(|pkg| pkg.dep_id), )?; } else { for pkg in &to_update { deps.update_dep(pkg.dep_id, pkg.new_version_req.clone()); } deps.commit_changes()?; } // Force fresh npm registry metadata for exactly the packages being // updated. The installer's re-resolution reads the npm registry cache with // the default `CacheSetting::Use`, so a stale cached packument can hide a // newer in-range version and leave the lockfile pinned to the old one -- // even though `deno outdated` (which fetches packuments fresh) reports the // update. Adding each such npm package to the cache blocklist makes // `cache_setting()` return `ReloadSome`, refetching metadata for only those // packages. See #35348. let install_flags = { let mut install_flags = (*flags).clone(); install_flags.cache_blocklist.extend( to_update .iter() .filter(|pkg| pkg.force_reload_npm) .map(|pkg| pkg.package_name.clone()), ); Arc::new(install_flags) }; let factory = super::npm_install_after_modification( install_flags, Some(deps.jsr_fetch_resolver.clone()), cache_options, ) .await?; let mut updated_to_versions = HashSet::new(); let args = dep_manager_args( &factory, deps.npm_fetch_resolver.clone(), deps.jsr_fetch_resolver.clone(), ) .await?; let mut deps = deps.reloaded_after_modification(args); deps.resolve_current_versions().await?; for pkg in &to_update { if let Some(new_resolved) = deps.resolved_version(pkg.dep_id) { if lockfile_only { // For lockfile-only, only report deps whose resolved version // actually changed in the lockfile. if pkg.current_version.as_ref() == Some(new_resolved) { continue; } updated_to_versions.insert(( pkg.package_name.clone(), pkg .current_version .as_ref() .map(|nv| nv.version.to_string()) .unwrap_or_else(|| "(unresolved)".to_string()), new_resolved.version.to_string(), )); } else { updated_to_versions.insert(( pkg.package_name.clone(), pkg.current_version_req.to_string(), pkg.new_version_req.to_string(), )); } } else { log::warn!( "Failed to resolve version for new version requirement: {} -> {}", pkg.package_name, pkg.new_version_req ); } } log::info!( "Updated {} dependenc{}:", updated_to_versions.len(), if updated_to_versions.len() == 1 { "y" } else { "ies" } ); let mut updated_to_versions = updated_to_versions.into_iter().collect::<Vec<_>>(); updated_to_versions.sort_by(|(k, _, _), (k2, _, _)| k.cmp(k2)); let max_name = updated_to_versions .iter() .map(|(name, _, _)| name.len()) .max() .unwrap_or(0); let max_old = updated_to_versions .iter() .map(|(_, maybe_current, _)| maybe_current.len()) .max() .unwrap_or(0); let max_new = updated_to_versions .iter() .map(|(_, _, new_version)| new_version.len()) .max() .unwrap_or(0); for (package_name, current_version, new_version) in updated_to_versions { log::info!( " - {}{}{} {}{} -> {}{}", colors::gray(package_name[0..4].to_string()), &package_name[4..], " ".repeat(max_name - package_name.len()), " ".repeat(max_old - current_version.len()), colors::gray(¤t_version), " ".repeat(max_new - new_version.len()), colors::green(&new_version), ); } if lockfile_only && update_to_latest { let note = deno_terminal::colors::intense_blue("note"); log::info!( "{note}: --lockfile-only updates only within existing version requirements.\n Drop --lockfile-only to update deno.json/package.json requirements.", ); } } else { let maybe_matching = if filter_set.is_empty() { "" } else { "matching " }; if !update_to_latest && can_update_to_latest { let note = deno_terminal::colors::intense_blue("note"); log::info!( "All {maybe_matching}dependencies are at newest compatible versions.\n{note}: newer, incompatible versions are available.\n Run with `--latest` to update", ); } else { log::info!("All {maybe_matching}dependencies are up to date."); } } print_skipped_warning(&skipped); Ok(()) } async fn dep_manager_args( factory: &CliFactory, npm_fetch_resolver: Arc<NpmFetchResolver>, jsr_fetch_resolver: Arc<JsrFetchResolver>, ) -> Result<DepManagerArgs, AnyError> { Ok(DepManagerArgs { module_load_preparer: factory.module_load_preparer().await?.clone(), jsr_fetch_resolver, npm_fetch_resolver, npm_resolver: factory.npm_resolver().await?.clone(), npm_installer: factory.npm_installer().await?.clone(), npm_version_resolver: factory.npm_version_resolver()?.clone(), progress_bar: factory.text_only_progress_bar().clone(), permissions_container: factory.root_permissions_container()?.clone(), main_module_graph_container: factory .main_module_graph_container() .await? .clone(), lockfile: factory.maybe_lockfile().await?.cloned(), }) } pub(crate) mod filter { use deno_core::anyhow::Context; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_semver::VersionReq; enum FilterKind { Exclude, Include, } pub struct Filter { kind: FilterKind, regex: regex::Regex, version_spec: Option<VersionReq>, } fn pattern_to_regex(pattern: &str) -> Result<regex::Regex, AnyError> { let escaped = regex::escape(pattern); let unescaped_star = escaped.replace(r"\*", ".*"); Ok(regex::Regex::new(&format!("^{}$", unescaped_star))?) } impl Filter { pub fn version_spec(&self) -> Option<&VersionReq> { self.version_spec.as_ref() } pub fn from_str(input: &str) -> Result<Self, AnyError> { let (kind, first_idx) = if input.starts_with('!') { (FilterKind::Exclude, 1) } else { (FilterKind::Include, 0) }; let s = &input[first_idx..]; let (pattern, version_spec) = if let Some(scope_name) = s.strip_prefix('@') { if let Some(idx) = scope_name.find('@') { let (pattern, version_spec) = s.split_at(idx + 1); ( pattern, Some( VersionReq::parse_from_specifier( version_spec.trim_start_matches('@'), ) .with_context(|| format!("Invalid filter \"{input}\""))?, ), ) } else { (s, None) } } else { let mut parts = s.split('@'); let Some(pattern) = parts.next() else { return Err(anyhow!("Invalid filter \"{input}\"")); }; ( pattern, parts .next() .map(VersionReq::parse_from_specifier) .transpose() .with_context(|| format!("Invalid filter \"{input}\""))?, ) }; Ok(Filter { kind, regex: pattern_to_regex(pattern) .with_context(|| format!("Invalid filter \"{input}\""))?, version_spec, }) } pub fn matches(&self, name: &str) -> bool { self.regex.is_match(name) } } pub struct FilterSet { filters: Vec<Filter>, has_exclude: bool, has_include: bool, } impl FilterSet { pub fn from_filter_strings<'a>( filter_strings: impl IntoIterator<Item = &'a str>, ) -> Result<Self, AnyError> { let filters = filter_strings .into_iter() .map(Filter::from_str) .collect::<Result<Vec<_>, _>>()?; let has_exclude = filters .iter() .any(|f| matches!(f.kind, FilterKind::Exclude)); let has_include = filters .iter() .any(|f| matches!(f.kind, FilterKind::Include)); Ok(FilterSet { filters, has_exclude, has_include, }) } pub fn is_empty(&self) -> bool { self.filters.is_empty() } pub fn matches(&self, name: &str) -> bool { self.matching_filter(name).is_included() } pub fn matching_filter(&self, name: &str) -> MatchResult<'_> { if self.filters.is_empty() { return MatchResult::Included; } let mut matched = None; for filter in &self.filters { match filter.kind { FilterKind::Include => { if matched.is_none() && filter.matches(name) { matched = Some(filter); } } FilterKind::Exclude => { if filter.matches(name) { return MatchResult::Excluded; } } } } if let Some(filter) = matched { MatchResult::Matches(filter) } else if self.has_exclude && !self.has_include { MatchResult::Included } else { MatchResult::Excluded } } } pub enum MatchResult<'a> { Matches(&'a Filter), Included, Excluded, } impl MatchResult<'_> { pub fn version_spec(&self) -> Option<&VersionReq> { match self { MatchResult::Matches(filter) => filter.version_spec(), _ => None, } } pub fn is_included(&self) -> bool { matches!(self, MatchResult::Included | MatchResult::Matches(_)) } } #[cfg(test)] mod test { fn matches_filters<'a, 'b>( filters: impl IntoIterator<Item = &'a str>, name: &str, ) -> bool { let filters = super::FilterSet::from_filter_strings(filters).unwrap(); filters.matches(name) } fn version_spec(s: &str) -> deno_semver::VersionReq { deno_semver::VersionReq::parse_from_specifier(s).unwrap() } #[test] fn basic_glob() { assert!(matches_filters(["foo*"], "foo")); assert!(matches_filters(["foo*"], "foobar")); assert!(!matches_filters(["foo*"], "barfoo")); assert!(matches_filters(["*foo"], "foo")); assert!(matches_filters(["*foo"], "barfoo")); assert!(!matches_filters(["*foo"], "foobar")); assert!(matches_filters(["@scope/foo*"], "@scope/foobar")); } #[test] fn basic_glob_with_version() { assert!(matches_filters(["foo*@1"], "foo",)); assert!(matches_filters(["foo*@1"], "foobar",)); assert!(matches_filters(["foo*@1"], "foo-bar",)); assert!(!matches_filters(["foo*@1"], "barfoo",)); assert!(matches_filters(["@scope/*@1"], "@scope/foo")); } #[test] fn glob_exclude() { assert!(!matches_filters(["!foo*"], "foo")); assert!(!matches_filters(["!foo*"], "foobar")); assert!(matches_filters(["!foo*"], "barfoo")); assert!(!matches_filters(["!*foo"], "foo")); assert!(!matches_filters(["!*foo"], "barfoo")); assert!(matches_filters(["!*foo"], "foobar")); assert!(!matches_filters(["!@scope/foo*"], "@scope/foobar")); } #[test] fn multiple_globs() { assert!(matches_filters(["foo*", "bar*"], "foo")); assert!(matches_filters(["foo*", "bar*"], "bar")); assert!(!matches_filters(["foo*", "bar*"], "baz")); assert!(matches_filters(["foo*", "!bar*"], "foo")); assert!(!matches_filters(["foo*", "!bar*"], "bar")); assert!(matches_filters(["foo*", "!bar*"], "foobar")); assert!(!matches_filters(["foo*", "!*bar"], "foobar")); assert!(!matches_filters(["foo*", "!*bar"], "baz")); let filters = super::FilterSet::from_filter_strings(["foo*@1", "bar*@2"]).unwrap(); assert_eq!( filters.matching_filter("foo").version_spec().cloned(), Some(version_spec("1")) ); assert_eq!( filters.matching_filter("bar").version_spec().cloned(), Some(version_spec("2")) ); } } }