1use 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
9const MAX_PACKAGE_ID_LENGTH: usize = 100;
11
12fn 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
25enum 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 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 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 version.to_string()
99 }
100
101 fn package_url(&self, name: &PackageName) -> String {
102 crate::registry::package_url(name.as_str())
103 }
104
105 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 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 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 fn requirement_is_unresolved(&self, requirement: &VersionReq) -> bool {
161 requirement.as_str().contains("$(")
162 }
163
164 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 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 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 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 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 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 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 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 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 #[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 assert!(f.compile_requirement(&VersionReq::new("1.*.0")).is_none());
473 }
474
475 #[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 #[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 #[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 #[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 #[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}