Skip to main content

deps_nuget/
formatter.rs

1//! Version formatting for the NuGet ecosystem.
2
3use deps_core::lsp_helpers::{
4    DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
5    RequirementMatcher, RequirementResolution, SourcePolicy, compile_requirement_unless,
6};
7use deps_core::{ConcreteVersion, InvalidPackageName, PackageName, VersionReq};
8
9/// Maximum package ID length NuGet's client-side `PackageIdValidator` accepts.
10const MAX_PACKAGE_ID_LENGTH: usize = 100;
11
12/// Whether `name` matches NuGet's package ID rule (`PackageIdValidator.IdRegex` in
13/// NuGet.Client: `^\w+([_.-]\w+)*$`, `\w` restricted to ASCII here): one or more ASCII
14/// alphanumeric/`_` "words" separated by single `.` or `-` characters, with no leading,
15/// trailing, or consecutive `.`/`-`. `_` is itself a `\w` character in .NET regex, not a
16/// separator, so it is treated as ordinary word content — `_foo`/`foo__bar`/a bare `_` are
17/// all accepted by NuGet's real validator (confirmed live: `_` is a published package id,
18/// nuget.org id `_`, #402 critique M1) but were previously rejected here by splitting on `_`
19/// as if it were a separator too.
20fn is_valid_nuget_id(name: &str) -> bool {
21    name.split(['.', '-'])
22        .all(|word| !word.is_empty() && word.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
23}
24
25/// NuGet interval/floating-pattern matcher, compiled once per dependency by
26/// [`NuGetFormatter::compile_requirement`] — the range or floating pattern is parsed once
27/// here rather than being re-parsed for every candidate version scanned. Always decidable
28/// (`Some`) — matching a candidate against an already-parsed range/pattern has no separate
29/// "candidate failed to parse" signal, only "range/pattern failed to parse" (already ruled
30/// out by `compile_requirement` before this is constructed).
31enum NuGetMatcher {
32    Range(crate::version::VersionRange),
33    Float(crate::version::FloatPattern),
34}
35
36impl RequirementMatcher for NuGetMatcher {
37    fn matches(&self, version: &ConcreteVersion) -> Option<bool> {
38        let version = version.as_str();
39        Some(match self {
40            Self::Range(range) => crate::version::range_contains(version, range),
41            Self::Float(pattern) => {
42                let parsed = crate::version::ParsedVersion::parse(version);
43                crate::version::float_matches(version, &parsed, pattern)
44            }
45        })
46    }
47}
48
49pub struct NuGetFormatter;
50
51impl PackageNaming for NuGetFormatter {
52    /// Lints `name` against NuGet's own `PackageIdValidator` rule (see
53    /// `is_valid_nuget_id`), so a structurally invalid package ID is reported as "Invalid
54    /// package name" instead of falling through to a registry lookup and rendering the
55    /// generic "Registry lookup failed" diagnostic (#402).
56    ///
57    /// An unresolved MSBuild property reference (e.g. `<PackageReference
58    /// Include="$(MyPackageId)" />`) is checked first and always accepted — the same
59    /// unresolvable-variable treatment `requirement_is_unresolved` already gives a
60    /// `$(...)`-containing *version* string (#402 critique M2): `name` here is not a
61    /// concrete package id at all until MSBuild expands the property, so it has no shape to
62    /// validate.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`InvalidPackageName`] if `name` is empty, exceeds 100 characters, or contains
67    /// a character outside NuGet's `\w+([_.-]\w+)*` shape.
68    fn validate_package_name(&self, name: &str) -> Result<(), InvalidPackageName> {
69        if name.contains("$(") {
70            return Ok(());
71        }
72        if name.is_empty() {
73            return Err(InvalidPackageName::new("name cannot be empty"));
74        }
75        if name.chars().count() > MAX_PACKAGE_ID_LENGTH {
76            return Err(InvalidPackageName::new(format!(
77                "name cannot exceed {MAX_PACKAGE_ID_LENGTH} characters"
78            )));
79        }
80        if !is_valid_nuget_id(name) {
81            return Err(InvalidPackageName::new(
82                "name must be ASCII alphanumeric/'_' words separated by single '.' or '-' characters",
83            ));
84        }
85        Ok(())
86    }
87
88    /// NuGet package ids are case-insensitive and every V3 API path segment is lowercased.
89    fn normalize_package_name(&self, name: &PackageName) -> String {
90        name.as_str().to_lowercase()
91    }
92}
93
94impl PackageRendering for NuGetFormatter {
95    fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
96        let version = version.as_str();
97        // NuGet manifests store plain version text; no prefix/wrapping on insert.
98        version.to_string()
99    }
100
101    fn package_url(&self, name: &PackageName) -> String {
102        crate::registry::package_url(name.as_str())
103    }
104
105    /// FR-011 (issue #523, M1): an `AlternateRegistry` dependency (resolved against a private
106    /// `NuGet.Config`-declared feed) must not render nuget.org's package-page link alongside
107    /// live private-feed data — a nuget.org link next to that would read as confirmation the
108    /// link is real, which is worse than showing no link at all. Delegates to
109    /// [`SourcePolicy::source_is_public_registry_content`], which is `true` only for plain
110    /// `Registry` (the default) — so this suppresses the link for exactly the sources
111    /// [`Self::can_resolve_source`] newly opts into resolving.
112    fn suppress_package_url(&self, source: &deps_core::parser::DependencySource) -> bool {
113        !self.source_is_public_registry_content(source)
114    }
115}
116
117impl RequirementResolution for NuGetFormatter {
118    /// Overridden because the default npm caret/tilde semantics do not apply to NuGet's
119    /// interval-notation ranges (`[1.0,2.0)`) and floating patterns (`1.1.*`).
120    fn version_satisfies_requirement(&self, version: &ConcreteVersion, requirement: &str) -> bool {
121        let version = version.as_str();
122        if requirement.contains('*') {
123            let versions = [version.to_string()];
124            return crate::version::resolve_float(&versions, requirement).is_some();
125        }
126        crate::version::satisfies(version, requirement)
127    }
128
129    /// Overridden because a minimum-only range (a bare `Version="1.0.0"`, or its explicit
130    /// open-ended-minimum spellings `[1.0.0,)`/`(1.0.0,)`/`[1.0.0,]`) is a floor under
131    /// `PackageReference`/`PackageVersion` semantics, not an auto-following range:
132    /// `version_satisfies_requirement` accepts any version `>= 1.0.0`, so delegating to it
133    /// here would never flag a floor as outdated. `latest` behind the floor is not
134    /// "outdated" either (it would read as a downgrade suggestion), so up to date is
135    /// `latest <= floor` here, not `latest == floor`. Exact pins, maximums, bounded ranges,
136    /// and floating patterns (`1.1.*`) already express the intended forward-compatibility
137    /// window, so those keep the general satisfies check.
138    fn is_requirement_up_to_date(
139        &self,
140        requirement: &VersionReq,
141        latest: &ConcreteVersion,
142    ) -> bool {
143        let requirement = requirement.as_str();
144        if requirement.contains('*') {
145            return self.version_satisfies_requirement(latest, requirement);
146        }
147        match crate::version::compare_minimum_floor(requirement, latest.as_str()) {
148            Some(ordering) => ordering != std::cmp::Ordering::Less,
149            None => self.version_satisfies_requirement(latest, requirement),
150        }
151    }
152
153    /// M3: an unexpanded MSBuild property reference (`$(PropertyName)`) inside a version
154    /// string — most commonly `[$(MinVersion),$(MaxVersion))`, which parses to a `Bounded`
155    /// interval whose min/max both fall back to `0.0.0` (a fail-safe floor, not an error),
156    /// but then rejects every real candidate as out of range. A bare `$(X)` (unbracketed) is
157    /// already fail-safe via that same floor fallback and needs no guard; the bracketed form
158    /// does not. Mirrors Maven's `${property}` / Gradle's `$var`/`${var}` unresolved-variable
159    /// guards.
160    fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool {
161        requirement.as_str().contains("$(")
162    }
163
164    /// Uses [`compile_requirement_unless`] (see that function and
165    /// [`deps_core::lsp_helpers::RequirementResolution::compile_requirement`] for the shared "undecidable" contract).
166    ///
167    /// The undecidable predicate rejects a syntactically malformed range or floating pattern
168    /// (parsing fails) — without this guard, a malformed requirement string would make
169    /// `satisfies`/`resolve_float` return `false` for every candidate, producing a false
170    /// "unsatisfiable" verdict instead of correctly suppressing the check.
171    fn compile_requirement(&self, requirement: &VersionReq) -> Option<Box<dyn RequirementMatcher>> {
172        let requirement = requirement.as_str();
173        if requirement.contains('*') {
174            compile_requirement_unless(
175                requirement,
176                |r| crate::version::parse_float(r).is_none(),
177                |r| {
178                    NuGetMatcher::Float(
179                        crate::version::parse_float(&r).expect("validated by undecidable guard"),
180                    )
181                },
182            )
183        } else {
184            compile_requirement_unless(
185                requirement,
186                |r| crate::version::parse_range(r).is_none(),
187                |r| {
188                    NuGetMatcher::Range(
189                        crate::version::parse_range(&r).expect("validated by undecidable guard"),
190                    )
191                },
192            )
193        }
194    }
195}
196
197impl DiagnosticMessages for NuGetFormatter {}
198
199impl DiagnosticPolicy for NuGetFormatter {}
200
201impl SourcePolicy for NuGetFormatter {
202    /// FR-011 (issue #523): a NuGet dependency resolved against a private `NuGet.Config`
203    /// feed is version-resolvable through `NuGetRegistry`'s alternate-feed chain, not just
204    /// the default `Registry`/`AlternateRegistry`-excluding set
205    /// [`deps_core::parser::DependencySource::is_version_resolvable`] would otherwise answer.
206    /// `source_is_public_registry_content` stays at its default (`Registry` only) — an
207    /// `AlternateRegistry` dependency is resolvable but is never treated as public-registry
208    /// content for OSV/deps.dev/hover-trust-signal purposes (M3: deliberate privacy
209    /// protection, since those signals would otherwise send a private package's name to a
210    /// public service by default).
211    fn can_resolve_source(&self, source: &deps_core::parser::DependencySource) -> bool {
212        matches!(
213            source,
214            deps_core::parser::DependencySource::Registry
215                | deps_core::parser::DependencySource::AlternateRegistry { .. }
216        )
217    }
218}
219
220impl OsvNaming for NuGetFormatter {}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn test_format_version() {
228        let f = NuGetFormatter;
229        assert_eq!(
230            f.format_version_for_text_edit(&ConcreteVersion::new("13.0.3")),
231            "13.0.3"
232        );
233    }
234
235    #[test]
236    fn test_package_url() {
237        let f = NuGetFormatter;
238        assert_eq!(
239            f.package_url(&PackageName::new("Newtonsoft.Json")),
240            "https://www.nuget.org/packages/Newtonsoft.Json"
241        );
242    }
243
244    #[test]
245    fn test_version_satisfies_exact_pin() {
246        let f = NuGetFormatter;
247        assert!(f.version_satisfies_requirement(&ConcreteVersion::new("1.0.0"), "[1.0.0]"));
248        assert!(!f.version_satisfies_requirement(&ConcreteVersion::new("1.0.1"), "[1.0.0]"));
249    }
250
251    #[test]
252    fn test_version_satisfies_bare_floor() {
253        let f = NuGetFormatter;
254        assert!(f.version_satisfies_requirement(&ConcreteVersion::new("2.0.0"), "1.0.0"));
255        assert!(!f.version_satisfies_requirement(&ConcreteVersion::new("0.9.0"), "1.0.0"));
256    }
257
258    #[test]
259    fn test_version_satisfies_floating() {
260        let f = NuGetFormatter;
261        assert!(f.version_satisfies_requirement(&ConcreteVersion::new("1.1.5"), "1.1.*"));
262        assert!(!f.version_satisfies_requirement(&ConcreteVersion::new("1.2.0"), "1.1.*"));
263    }
264
265    #[test]
266    fn test_is_up_to_date_bare_floor_outdated() {
267        let f = NuGetFormatter;
268        // Bare floors are pins under PackageReference: a newer latest is outdated,
269        // even though it satisfies the floor (>= 13.0.3).
270        assert!(!f.is_requirement_up_to_date(
271            &VersionReq::new("13.0.3"),
272            &ConcreteVersion::new("13.0.4")
273        ));
274        assert!(!f.is_requirement_up_to_date(
275            &VersionReq::new("13.0.3"),
276            &ConcreteVersion::new("14.0.0")
277        ));
278    }
279
280    #[test]
281    fn test_is_up_to_date_bare_floor_matches_latest() {
282        let f = NuGetFormatter;
283        assert!(f.is_requirement_up_to_date(
284            &VersionReq::new("13.0.3"),
285            &ConcreteVersion::new("13.0.3")
286        ));
287    }
288
289    #[test]
290    fn test_is_up_to_date_open_ended_minimum_bracket_forms_outdated() {
291        let f = NuGetFormatter;
292        // Same floor semantics as a bare version, spelled with explicit interval brackets.
293        assert!(!f.is_requirement_up_to_date(
294            &VersionReq::new("[13.0.3,)"),
295            &ConcreteVersion::new("13.0.4")
296        ));
297        assert!(!f.is_requirement_up_to_date(
298            &VersionReq::new("(13.0.3,)"),
299            &ConcreteVersion::new("13.0.4")
300        ));
301        assert!(!f.is_requirement_up_to_date(
302            &VersionReq::new("[13.0.3,]"),
303            &ConcreteVersion::new("13.0.4")
304        ));
305        assert!(f.is_requirement_up_to_date(
306            &VersionReq::new("[13.0.3,)"),
307            &ConcreteVersion::new("13.0.3")
308        ));
309    }
310
311    #[test]
312    fn test_is_up_to_date_floor_ahead_of_latest_is_not_outdated() {
313        let f = NuGetFormatter;
314        // A floor already ahead of the registry's latest (a preview/prerelease pin, or a
315        // latest that regressed) must not render a downgrade suggestion.
316        assert!(f.is_requirement_up_to_date(
317            &VersionReq::new("13.0.5"),
318            &ConcreteVersion::new("13.0.4")
319        ));
320        assert!(f.is_requirement_up_to_date(
321            &VersionReq::new("9.0.0-preview.5"),
322            &ConcreteVersion::new("8.0.11")
323        ));
324        // A prerelease pin genuinely behind a newer stable release is still outdated.
325        assert!(!f.is_requirement_up_to_date(
326            &VersionReq::new("9.0.0-preview.5"),
327            &ConcreteVersion::new("9.0.0")
328        ));
329    }
330
331    #[test]
332    fn test_is_up_to_date_exact_pin_and_ranges_keep_satisfies_semantics() {
333        let f = NuGetFormatter;
334        assert!(f.is_requirement_up_to_date(
335            &VersionReq::new("[13.0.3]"),
336            &ConcreteVersion::new("13.0.3")
337        ));
338        assert!(!f.is_requirement_up_to_date(
339            &VersionReq::new("[13.0.3]"),
340            &ConcreteVersion::new("14.0.0")
341        ));
342        assert!(f.is_requirement_up_to_date(
343            &VersionReq::new("[1.0,2.0)"),
344            &ConcreteVersion::new("1.5.0")
345        ));
346        assert!(
347            f.is_requirement_up_to_date(&VersionReq::new("1.1.*"), &ConcreteVersion::new("1.1.5"))
348        );
349        assert!(
350            !f.is_requirement_up_to_date(&VersionReq::new("1.1.*"), &ConcreteVersion::new("1.2.0"))
351        );
352    }
353
354    #[test]
355    fn test_normalize_lowercases() {
356        let f = NuGetFormatter;
357        assert_eq!(
358            f.normalize_package_name(&PackageName::new("Newtonsoft.Json")),
359            "newtonsoft.json"
360        );
361    }
362
363    #[test]
364    fn test_osv_package_name_preserves_case_unlike_normalize_package_name() {
365        // OSV's NuGet ecosystem is case-preserving (`Newtonsoft.Json`, verified
366        // live — architecture.md §2), unlike `normalize_package_name`'s
367        // lowercase internal lookup key. `osv_package_name` has no override
368        // here (the default identity impl is already correct for NuGet), so
369        // this test is the regression guard: a future "tidy-up" routing it
370        // through `normalize_package_name` would silently zero out NuGet OSV
371        // scanning, and this is the test that would catch it (critique #10).
372        use deps_core::Dependency;
373        use deps_core::parser::DependencySource;
374        use tower_lsp_server::ls_types::{Position, Range};
375
376        let dep = crate::types::NuGetDependency {
377            name: "Newtonsoft.Json".into(),
378            name_range: Range::new(Position::new(0, 0), Position::new(0, 1)),
379            version_requirement: Some("12.0.1".into()),
380            version_range: None,
381            source: DependencySource::Registry,
382        };
383        assert_eq!(dep.source(), DependencySource::Registry);
384
385        let f = NuGetFormatter;
386        assert_eq!(
387            f.osv_package_name(&dep),
388            Some("Newtonsoft.Json".to_string())
389        );
390        assert_ne!(
391            f.osv_package_name(&dep).unwrap(),
392            f.normalize_package_name(&dep.name)
393        );
394    }
395
396    #[test]
397    fn test_osv_version_to_native_round_trips_through_own_parser() {
398        // Critic S2 gate: `osv_version_to_native` is identity for NuGet, so
399        // the version it hands to `format_version_for_text_edit` must
400        // itself satisfy the requirement text that edit produces.
401        let f = NuGetFormatter;
402        let osv_version = "12.0.1";
403        let native = f.osv_version_to_native(osv_version);
404        assert_eq!(native, osv_version);
405        let native = ConcreteVersion::new(native);
406        let edit_text = f.format_version_for_text_edit(&native);
407        assert!(f.version_satisfies_requirement(&native, &edit_text));
408    }
409
410    #[test]
411    fn test_compile_requirement_exact_pin_satisfiable() {
412        let f = NuGetFormatter;
413        let matcher = f
414            .compile_requirement(&VersionReq::new("[13.0.3]"))
415            .expect("well-formed exact pin must compile");
416        assert_eq!(matcher.matches(&ConcreteVersion::new("13.0.3")), Some(true));
417        assert_eq!(
418            matcher.matches(&ConcreteVersion::new("13.0.4")),
419            Some(false)
420        );
421    }
422
423    #[test]
424    fn test_compile_requirement_range_satisfiable() {
425        let f = NuGetFormatter;
426        let matcher = f
427            .compile_requirement(&VersionReq::new("[1.0,2.0)"))
428            .expect("well-formed range must compile");
429        assert_eq!(matcher.matches(&ConcreteVersion::new("1.5.0")), Some(true));
430        assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(false));
431    }
432
433    #[test]
434    fn test_compile_requirement_floating_pattern_satisfiable() {
435        let f = NuGetFormatter;
436        let matcher = f
437            .compile_requirement(&VersionReq::new("1.1.*"))
438            .expect("well-formed floating pattern must compile");
439        assert_eq!(matcher.matches(&ConcreteVersion::new("1.1.5")), Some(true));
440        assert_eq!(matcher.matches(&ConcreteVersion::new("1.2.0")), Some(false));
441    }
442
443    #[test]
444    fn test_compile_requirement_bare_floor_satisfiable() {
445        // A bare version is a minimum-floor requirement under `satisfies` (unlike
446        // `is_requirement_up_to_date`'s floor-pin override) — any version `>= floor` counts
447        // as a match for the unsatisfiable check.
448        let f = NuGetFormatter;
449        let matcher = f
450            .compile_requirement(&VersionReq::new("1.0.0"))
451            .expect("a bare version is a well-formed minimum floor");
452        assert_eq!(matcher.matches(&ConcreteVersion::new("2.0.0")), Some(true));
453        assert_eq!(matcher.matches(&ConcreteVersion::new("0.9.0")), Some(false));
454    }
455
456    /// The malformed-requirement guard this formatter's `compile_requirement` adds — the
457    /// same class of fix Maven/Gradle carry, but previously untested for NuGet.
458    #[test]
459    fn test_compile_requirement_malformed_range_returns_none() {
460        let f = NuGetFormatter;
461        assert!(
462            f.compile_requirement(&VersionReq::new("[1.0,2.0"))
463                .is_none()
464        );
465    }
466
467    #[test]
468    fn test_compile_requirement_malformed_floating_pattern_returns_none() {
469        let f = NuGetFormatter;
470        // Not a valid interval (contains '*') and not a valid float pattern either
471        // (`resolve_float`'s grammar requires a trailing `.*`/`*` segment).
472        assert!(f.compile_requirement(&VersionReq::new("1.*.0")).is_none());
473    }
474
475    /// M3: a bracketed MSBuild property reference parses as a `Bounded` 0.0.0-0.0.0
476    /// interval that rejects every real candidate — must be treated as unresolved, not
477    /// checked against `available`.
478    #[test]
479    fn test_requirement_is_unresolved_bracketed_msbuild_property() {
480        let f = NuGetFormatter;
481        assert!(f.requirement_is_unresolved(&VersionReq::new("[$(MinVersion),$(MaxVersion))")));
482    }
483
484    #[test]
485    fn test_requirement_is_unresolved_false_for_ordinary_requirements() {
486        let f = NuGetFormatter;
487        assert!(!f.requirement_is_unresolved(&VersionReq::new("13.0.3")));
488        assert!(!f.requirement_is_unresolved(&VersionReq::new("[1.0,2.0)")));
489    }
490
491    #[test]
492    fn test_validate_package_name_accepts_valid_names() {
493        let f = NuGetFormatter;
494        for name in ["Newtonsoft.Json", "Microsoft.Extensions.Logging", "moq"] {
495            assert!(
496                f.validate_package_name(name).is_ok(),
497                "expected {name:?} to be accepted"
498            );
499        }
500    }
501
502    /// #402: a structurally invalid NuGet package ID must be reported as an invalid package
503    /// name, not forwarded to the registry lookup that produces the misleading generic
504    /// diagnostic.
505    #[test]
506    fn test_validate_package_name_rejects_invalid_names() {
507        let f = NuGetFormatter;
508        for name in ["", ".Json", "Json.", "New..Json", "New Json", "日本語"] {
509            assert!(
510                f.validate_package_name(name).is_err(),
511                "expected {name:?} to be rejected"
512            );
513        }
514    }
515
516    /// #402 critique M1: `_` is a `\w` character in NuGet's real `PackageIdValidator.IdRegex`,
517    /// not a separator, so a leading/doubled/bare underscore is accepted (confirmed live:
518    /// nuget.org publishes a package with id `_`).
519    #[test]
520    fn test_validate_package_name_accepts_underscore_as_word_character() {
521        let f = NuGetFormatter;
522        for name in ["_foo", "foo__bar", "_", "foo_bar"] {
523            assert!(
524                f.validate_package_name(name).is_ok(),
525                "expected {name:?} to be accepted"
526            );
527        }
528    }
529
530    /// #402 critique M2: an unexpanded MSBuild property reference in `Include` (e.g.
531    /// `<PackageReference Include="$(MyPackageId)" />`) is not yet a concrete package id and
532    /// must not be flagged as an invalid package name.
533    #[test]
534    fn test_validate_package_name_accepts_unresolved_msbuild_property() {
535        let f = NuGetFormatter;
536        assert!(f.validate_package_name("$(MyPackageId)").is_ok());
537    }
538
539    #[test]
540    fn test_validate_package_name_rejects_too_long() {
541        let f = NuGetFormatter;
542        let too_long = "a".repeat(101);
543        assert!(f.validate_package_name(&too_long).is_err());
544    }
545
546    // --- SourcePolicy / suppress_package_url (issue #523, M1/FR-011) ---
547
548    #[test]
549    fn test_can_resolve_source_includes_alternate_registry() {
550        use deps_core::parser::DependencySource;
551
552        let f = NuGetFormatter;
553        assert!(f.can_resolve_source(&DependencySource::Registry));
554        assert!(f.can_resolve_source(&DependencySource::AlternateRegistry {
555            index: "nuget-chain:0".to_string(),
556            mirrors_crates_io: false,
557        }));
558        assert!(!f.can_resolve_source(&DependencySource::CustomRegistry {
559            url: "unresolved".to_string(),
560        }));
561    }
562
563    #[test]
564    fn test_suppress_package_url_only_for_non_registry_source() {
565        use deps_core::parser::DependencySource;
566
567        let f = NuGetFormatter;
568        assert!(!f.suppress_package_url(&DependencySource::Registry));
569        assert!(
570            f.suppress_package_url(&DependencySource::AlternateRegistry {
571                index: "nuget-chain:0".to_string(),
572                mirrors_crates_io: false,
573            })
574        );
575    }
576}