1use crate::config::{DepsConfig, DiagnosticsConfig};
4use crate::document::{ServerState, ensure_document_loaded};
5use deps_core::VersionData;
6use std::sync::Arc;
7use tokio::sync::RwLock;
8use tower_lsp_server::Client;
9use tower_lsp_server::ls_types::{Diagnostic, Uri};
10
11pub async fn handle_diagnostics(
13 state: Arc<ServerState>,
14 uri: &Uri,
15 config: &DiagnosticsConfig,
16 client: Client,
17 full_config: Arc<RwLock<DepsConfig>>,
18) -> Vec<Diagnostic> {
19 if !ensure_document_loaded(uri, Arc::clone(&state), client, Arc::clone(&full_config)).await {
21 tracing::warn!("Could not load document for diagnostics: {:?}", uri);
22 return vec![];
23 }
24
25 let (freshness, offline) = {
27 let full_config = full_config.read().await;
28 (
29 full_config.freshness.to_settings(),
30 full_config.network.offline,
31 )
32 };
33 let severities = config.to_severities();
34
35 generate_diagnostics_internal(state, uri, freshness, severities, offline).await
36}
37
38pub(crate) async fn generate_diagnostics_internal(
42 state: Arc<ServerState>,
43 uri: &Uri,
44 freshness: deps_core::FreshnessSettings,
45 severities: deps_core::DiagnosticSeverities,
46 offline: bool,
47) -> Vec<Diagnostic> {
48 let Some(extracted) = state.with_document(uri, |doc| {
52 let Some(ecosystem) = state.ecosystem_registry.get(doc.ecosystem_id()) else {
53 tracing::warn!(
54 "Ecosystem not found for diagnostics: {}",
55 doc.ecosystem_id()
56 );
57 return None;
58 };
59
60 if doc.loading_state == deps_core::LoadingState::Loading {
64 return None;
65 }
66
67 let parse_result = doc.parse_result_arc()?;
68 Some((
69 ecosystem,
70 doc.ecosystem,
71 parse_result,
72 doc.cached_versions.clone(),
73 doc.resolved_versions.clone(),
74 doc.vulnerabilities.clone(),
75 doc.outcomes.clone(),
76 ))
77 }) else {
78 tracing::warn!("Document not found for diagnostics: {:?}", uri);
79 return vec![];
80 };
81
82 let Some((
83 ecosystem,
84 ecosystem_id,
85 parse_result,
86 cached_versions,
87 resolved_versions,
88 vulnerabilities,
89 outcomes,
90 )) = extracted
91 else {
92 return vec![];
93 };
94
95 ecosystem
96 .generate_diagnostics(
97 parse_result.as_ref(),
98 VersionData::new(&cached_versions, &resolved_versions)
99 .with_vulnerabilities(&vulnerabilities)
100 .with_outcomes(&outcomes)
101 .with_ecosystem(ecosystem_id)
102 .with_offline(offline),
103 uri,
104 freshness,
105 severities,
106 )
107 .await
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use crate::config::DiagnosticsConfig;
114 use crate::document::ServerState;
115 use crate::test_utils::test_helpers::create_test_client_and_config;
116 use deps_core::EcosystemId;
117
118 #[tokio::test]
121 async fn test_handle_diagnostics_missing_document() {
122 let state = Arc::new(ServerState::new());
123 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
124 let config = DiagnosticsConfig::default();
125
126 let (client, full_config) = create_test_client_and_config();
127 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
128 assert!(result.is_empty());
129 }
130
131 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
143 async fn test_concurrent_document_write_not_blocked_by_in_flight_diagnostics() {
144 use crate::document::DocumentState;
145 use crate::test_utils::blocking_ecosystem::{
146 BlockingEcosystem, BlockingHook, MockParseResult,
147 };
148 use deps_core::ParseResult;
149 use tokio::sync::Barrier;
150
151 let state = Arc::new(ServerState::new());
152 let started = Arc::new(Barrier::new(2));
153 state
154 .ecosystem_registry
155 .register(Arc::new(BlockingEcosystem {
156 started: Arc::clone(&started),
157 hook: BlockingHook::Diagnostics,
158 }));
159
160 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
161 let content = "[dependencies]\nserde = \"1.0\"\n".to_string();
162 let parse_result: Box<dyn ParseResult> = Box::new(MockParseResult { uri: uri.clone() });
163 let doc = DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
164 state.update_document(uri.clone(), doc);
165
166 let config = DiagnosticsConfig::default();
167 let (client, full_config) = create_test_client_and_config();
168
169 let handler_task = tokio::spawn({
170 let state = Arc::clone(&state);
171 let uri = uri.clone();
172 async move { handle_diagnostics(state, &uri, &config, client, full_config).await }
173 });
174
175 tokio::time::timeout(std::time::Duration::from_secs(5), started.wait())
180 .await
181 .expect("handle_diagnostics did not reach generate_diagnostics within 5s");
182
183 let write_task = tokio::spawn({
187 let state = Arc::clone(&state);
188 let uri = uri.clone();
189 async move {
190 state.documents.get_mut(&uri).unwrap().set_loading();
191 }
192 });
193 let write_result =
194 tokio::time::timeout(std::time::Duration::from_millis(500), write_task).await;
195
196 handler_task.abort();
197
198 assert!(
199 write_result.is_ok(),
200 "#333 regression: a concurrent documents.get_mut on the same URI must not \
201 block on an in-flight generate_diagnostics call — the DashMap shard Ref \
202 must be dropped before the call is awaited, not after it"
203 );
204 }
205
206 #[cfg(feature = "cargo")]
210 mod severity_wiring_tests {
211 use super::*;
212 use crate::document::DocumentState;
213 use std::collections::HashMap;
214 use tower_lsp_server::ls_types::DiagnosticSeverity;
215
216 #[tokio::test]
217 async fn test_unknown_package_uses_configured_severity() {
218 let state = Arc::new(ServerState::new());
219 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
220 let config = DiagnosticsConfig {
221 unknown_severity: DiagnosticSeverity::ERROR,
222 ..DiagnosticsConfig::default()
223 };
224
225 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
226 let content = r#"[dependencies]
227serde = "1.0.0"
228"#
229 .to_string();
230 let parse_result = ecosystem
231 .parse_manifest(&content, &uri)
232 .await
233 .expect("Failed to parse manifest");
234
235 let doc_state =
236 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
237 state.update_document(uri.clone(), doc_state);
238
239 let (client, full_config) = create_test_client_and_config();
240 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
241
242 assert_eq!(result.len(), 1);
243 assert_eq!(result[0].severity, Some(DiagnosticSeverity::ERROR));
244 }
245
246 #[tokio::test]
247 async fn test_unknown_package_default_severity_unchanged() {
248 let state = Arc::new(ServerState::new());
249 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
250 let config = DiagnosticsConfig::default();
251
252 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
253 let content = r#"[dependencies]
254serde = "1.0.0"
255"#
256 .to_string();
257 let parse_result = ecosystem
258 .parse_manifest(&content, &uri)
259 .await
260 .expect("Failed to parse manifest");
261
262 let doc_state =
263 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
264 state.update_document(uri.clone(), doc_state);
265
266 let (client, full_config) = create_test_client_and_config();
267 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
268
269 assert_eq!(result.len(), 1);
270 assert_eq!(result[0].severity, Some(DiagnosticSeverity::WARNING));
271 }
272
273 #[tokio::test]
274 async fn test_outdated_dependency_uses_configured_severity() {
275 let state = Arc::new(ServerState::new());
276 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
277 let config = DiagnosticsConfig {
278 outdated_severity: DiagnosticSeverity::ERROR,
279 ..DiagnosticsConfig::default()
280 };
281
282 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
283 let content = r#"[dependencies]
284serde = "1.0.0"
285"#
286 .to_string();
287 let parse_result = ecosystem
288 .parse_manifest(&content, &uri)
289 .await
290 .expect("Failed to parse manifest");
291
292 let mut doc_state =
293 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
294 let mut cached = HashMap::new();
295 cached.insert(
300 "serde".into(),
301 deps_core::PackageVersions {
302 latest: "2.0.0".into(),
303 available: std::sync::Arc::from(vec!["2.0.0".into(), "1.0.0".into()]),
304 yanked: std::sync::Arc::from(Vec::new()),
305 published_at: None,
306 },
307 );
308 doc_state.update_cached_versions(cached);
309 state.update_document(uri.clone(), doc_state);
310
311 let (client, full_config) = create_test_client_and_config();
312 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
313
314 assert_eq!(result.len(), 1);
315 assert_eq!(result[0].severity, Some(DiagnosticSeverity::ERROR));
316 assert!(result[0].message.contains("Newer version available"));
317 }
318
319 #[tokio::test]
320 async fn test_outdated_dependency_default_severity_unchanged() {
321 let state = Arc::new(ServerState::new());
322 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
323 let config = DiagnosticsConfig::default();
324
325 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
326 let content = r#"[dependencies]
327serde = "1.0.0"
328"#
329 .to_string();
330 let parse_result = ecosystem
331 .parse_manifest(&content, &uri)
332 .await
333 .expect("Failed to parse manifest");
334
335 let mut doc_state =
336 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
337 let mut cached = HashMap::new();
338 cached.insert(
341 "serde".into(),
342 deps_core::PackageVersions {
343 latest: "2.0.0".into(),
344 available: std::sync::Arc::from(vec!["2.0.0".into(), "1.0.0".into()]),
345 yanked: std::sync::Arc::from(Vec::new()),
346 published_at: None,
347 },
348 );
349 doc_state.update_cached_versions(cached);
350 state.update_document(uri.clone(), doc_state);
351
352 let (client, full_config) = create_test_client_and_config();
353 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
354
355 assert_eq!(result.len(), 1);
356 assert_eq!(result[0].severity, Some(DiagnosticSeverity::HINT));
357 }
358
359 #[tokio::test]
360 async fn test_unsatisfiable_requirement_uses_configured_severity() {
361 let state = Arc::new(ServerState::new());
362 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
363 let config = DiagnosticsConfig {
364 unsatisfiable_severity: DiagnosticSeverity::ERROR,
365 ..DiagnosticsConfig::default()
366 };
367
368 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
369 let content = "[dependencies]\nserde = \"99\"\n".to_string();
370 let parse_result = ecosystem
371 .parse_manifest(&content, &uri)
372 .await
373 .expect("Failed to parse manifest");
374
375 let mut doc_state =
376 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
377 let mut cached = HashMap::new();
378 cached.insert(
379 "serde".into(),
380 deps_core::PackageVersions {
381 latest: "1.0.214".into(),
382 available: std::sync::Arc::from(vec!["1.0.214".into(), "1.0.213".into()]),
383 yanked: std::sync::Arc::from(Vec::new()),
384 published_at: None,
385 },
386 );
387 doc_state.update_cached_versions(cached);
388 state.update_document(uri.clone(), doc_state);
389
390 let (client, full_config) = create_test_client_and_config();
391 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
392
393 assert_eq!(result.len(), 1);
394 assert_eq!(result[0].severity, Some(DiagnosticSeverity::ERROR));
395 assert!(result[0].message.contains("No published version satisfies"));
396 }
397 }
398
399 #[cfg(feature = "cargo")]
401 mod cargo_tests {
402 use super::*;
403 use crate::document::DocumentState;
404
405 #[tokio::test]
406 async fn test_handle_diagnostics() {
407 let state = Arc::new(ServerState::new());
408 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
409 let config = DiagnosticsConfig::default();
410
411 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
412 let content = r#"[dependencies]
413serde = "1.0.0"
414"#
415 .to_string();
416
417 let parse_result = ecosystem
418 .parse_manifest(&content, &uri)
419 .await
420 .expect("Failed to parse manifest");
421
422 let doc_state =
423 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
424 state.update_document(uri.clone(), doc_state);
425
426 let (client, full_config) = create_test_client_and_config();
427 let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
428 }
430
431 #[tokio::test]
432 async fn test_handle_diagnostics_no_parse_result() {
433 let state = Arc::new(ServerState::new());
434 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
435 let config = DiagnosticsConfig::default();
436
437 let doc_state =
438 DocumentState::new_without_parse_result(EcosystemId::Cargo, String::new());
439 state.update_document(uri.clone(), doc_state);
440
441 let (client, full_config) = create_test_client_and_config();
442 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
443 assert!(result.is_empty());
444 }
445
446 #[tokio::test]
451 async fn test_handle_diagnostics_unsatisfiable_requirement_yields_one_warning() {
452 let state = Arc::new(ServerState::new());
453 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
454 let config = DiagnosticsConfig::default();
455
456 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
457 let content = "[dependencies]\nserde = \"99\"\n".to_string();
458 let parse_result = ecosystem
459 .parse_manifest(&content, &uri)
460 .await
461 .expect("Failed to parse manifest");
462
463 let mut doc_state =
464 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
465 let mut cached = std::collections::HashMap::new();
466 cached.insert(
467 "serde".into(),
468 deps_core::PackageVersions {
469 latest: "1.0.214".into(),
470 available: std::sync::Arc::from(vec!["1.0.214".into(), "1.0.213".into()]),
471 yanked: std::sync::Arc::from(Vec::new()),
472 published_at: None,
473 },
474 );
475 doc_state.update_cached_versions(cached);
476 state.update_document(uri.clone(), doc_state);
477
478 let (client, full_config) = create_test_client_and_config();
479 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
480
481 assert_eq!(result.len(), 1, "expected exactly one diagnostic");
482 assert_eq!(
483 result[0].severity,
484 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING)
485 );
486 assert!(result[0].message.contains("No published version satisfies"));
487 assert!(
488 !result
489 .iter()
490 .any(|d| d.message.contains("Newer version available")),
491 "the unsatisfiable WARNING must replace the outdated HINT, not add to it"
492 );
493 }
494
495 #[tokio::test]
499 async fn test_handle_diagnostics_unsatisfiable_requirement_empty_available_yields_nothing()
500 {
501 let state = Arc::new(ServerState::new());
502 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
503 let config = DiagnosticsConfig::default();
504
505 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
506 let content = "[dependencies]\nserde = \"99\"\n".to_string();
507 let parse_result = ecosystem
508 .parse_manifest(&content, &uri)
509 .await
510 .expect("Failed to parse manifest");
511
512 let mut doc_state =
513 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
514 let mut cached = std::collections::HashMap::new();
515 cached.insert(
516 "serde".into(),
517 deps_core::PackageVersions::latest_without_list("1.0.214"),
518 );
519 doc_state.update_cached_versions(cached);
520 state.update_document(uri.clone(), doc_state);
521
522 let (client, full_config) = create_test_client_and_config();
523 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
524 assert!(
525 !result
526 .iter()
527 .any(|d| d.message.contains("No published version satisfies")),
528 "an empty available list must suppress the unsatisfiable check, got: {result:?}"
529 );
530 }
531
532 #[tokio::test]
535 async fn test_handle_diagnostics_unsatisfiable_and_outdated_side_by_side() {
536 let state = Arc::new(ServerState::new());
537 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
538 let config = DiagnosticsConfig::default();
539
540 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
541 let content = "[dependencies]\nserde = \"99\"\ntokio = \"1.0\"\n".to_string();
542 let parse_result = ecosystem
543 .parse_manifest(&content, &uri)
544 .await
545 .expect("Failed to parse manifest");
546
547 let mut doc_state =
548 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
549 let mut cached = std::collections::HashMap::new();
550 cached.insert(
551 "serde".into(),
552 deps_core::PackageVersions {
553 latest: "1.0.214".into(),
554 available: std::sync::Arc::from(vec!["1.0.214".into()]),
555 yanked: std::sync::Arc::from(Vec::new()),
556 published_at: None,
557 },
558 );
559 cached.insert(
560 "tokio".into(),
561 deps_core::PackageVersions {
562 latest: "2.0.0".into(),
563 available: std::sync::Arc::from(vec!["2.0.0".into(), "1.5.0".into()]),
568 yanked: std::sync::Arc::from(Vec::new()),
569 published_at: None,
570 },
571 );
572 doc_state.update_cached_versions(cached);
573 state.update_document(uri.clone(), doc_state);
574
575 let (client, full_config) = create_test_client_and_config();
576 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
577
578 assert_eq!(result.len(), 2, "expected one warning and one hint");
579 assert!(
580 result
581 .iter()
582 .any(|d| d.message.contains("No published version satisfies"))
583 );
584 assert!(
585 result
586 .iter()
587 .any(|d| d.message.contains("Newer version available"))
588 );
589 }
590
591 #[tokio::test]
598 async fn test_handle_diagnostics_yanked_only_match_yields_one_warning() {
599 let state = Arc::new(ServerState::new());
600 let uri = deps_core::test_util::test_uri("/test/Cargo.toml");
601 let config = DiagnosticsConfig::default();
602
603 let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
604 let content = "[dependencies]\nserde = \"=1.0.213\"\n".to_string();
605 let parse_result = ecosystem
606 .parse_manifest(&content, &uri)
607 .await
608 .expect("Failed to parse manifest");
609
610 let mut doc_state =
611 DocumentState::new_from_parse_result(EcosystemId::Cargo, content, parse_result);
612 let mut cached = std::collections::HashMap::new();
613 cached.insert(
614 "serde".into(),
615 deps_core::PackageVersions {
616 latest: "1.0.214".into(),
617 available: std::sync::Arc::from(vec!["1.0.214".into(), "1.0.213".into()]),
618 yanked: std::sync::Arc::from(vec![(
619 "1.0.213".into(),
620 deps_core::RemovalStatus::Yanked,
621 )]),
622 published_at: None,
623 },
624 );
625 doc_state.update_cached_versions(cached);
626 state.update_document(uri.clone(), doc_state);
627
628 let (client, full_config) = create_test_client_and_config();
629 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
630
631 assert_eq!(result.len(), 1, "expected exactly one diagnostic");
632 assert_eq!(
633 result[0].severity,
634 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING)
635 );
636 assert_eq!(
637 result[0].message,
638 "This version has been yanked; latest is 1.0.214"
639 );
640 }
641 }
642
643 #[cfg(feature = "npm")]
645 mod npm_tests {
646 use super::*;
647 use crate::document::DocumentState;
648 use deps_core::DiagnosticMessages;
649
650 #[tokio::test]
651 async fn test_handle_diagnostics() {
652 let state = Arc::new(ServerState::new());
653 let uri = deps_core::test_util::test_uri("/test/package.json");
654 let config = DiagnosticsConfig::default();
655
656 let ecosystem = state.ecosystem_registry.get("npm").unwrap();
657 let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
658
659 let parse_result = ecosystem
660 .parse_manifest(&content, &uri)
661 .await
662 .expect("Failed to parse manifest");
663
664 let doc_state =
665 DocumentState::new_from_parse_result(EcosystemId::Npm, content, parse_result);
666 state.update_document(uri.clone(), doc_state);
667
668 let (client, full_config) = create_test_client_and_config();
669 let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
670 }
672
673 #[tokio::test]
687 async fn test_handle_diagnostics_in_use_version_yanked_still_fires_post_436() {
688 let state = Arc::new(ServerState::new());
689 let uri = deps_core::test_util::test_uri("/test/package.json");
690 let config = DiagnosticsConfig::default();
691
692 let ecosystem = state.ecosystem_registry.get("npm").unwrap();
693 let content = r#"{"dependencies": {"left-pad": "^1.0.0"}}"#.to_string();
694 let parse_result = ecosystem
695 .parse_manifest(&content, &uri)
696 .await
697 .expect("Failed to parse manifest");
698
699 let mut doc_state =
700 DocumentState::new_from_parse_result(EcosystemId::Npm, content, parse_result);
701
702 let mut cached = std::collections::HashMap::new();
703 cached.insert(
704 "left-pad".into(),
705 deps_core::PackageVersions {
706 latest: "1.0.2".into(),
707 available: std::sync::Arc::from(vec![
708 "1.0.2".into(),
709 "1.0.1".into(),
710 "1.0.0".into(),
711 ]),
712 yanked: std::sync::Arc::from(Vec::new()),
713 published_at: None,
714 },
715 );
716 doc_state.update_cached_versions(cached);
717
718 let mut resolved = std::collections::HashMap::new();
722 resolved.insert("left-pad".into(), "1.0.1".into());
723 doc_state.update_resolved_versions(resolved);
724
725 doc_state.replace_outcomes(deps_core::DependencyOutcomes::new().with_yanked(
726 "left-pad",
727 ("1.0.1".into(), deps_core::RemovalStatus::AdvisoryDeprecated),
728 ));
729
730 state.update_document(uri.clone(), doc_state);
731
732 let (client, full_config) = create_test_client_and_config();
733 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
734
735 assert_eq!(
736 result.len(),
737 1,
738 "expected exactly one diagnostic, got {result:?}"
739 );
740 assert_eq!(
741 result[0].severity,
742 Some(tower_lsp_server::ls_types::DiagnosticSeverity::WARNING)
743 );
744 assert_eq!(
745 result[0].message,
746 format!("{} (1.0.1)", deps_npm::NpmFormatter.yanked_message())
747 );
748 }
749
750 #[tokio::test]
758 async fn test_handle_diagnostics_manifest_requirement_yanked_stays_suppressed_for_exact_pin()
759 {
760 let state = Arc::new(ServerState::new());
761 let uri = deps_core::test_util::test_uri("/test/package.json");
762 let config = DiagnosticsConfig::default();
763
764 let ecosystem = state.ecosystem_registry.get("npm").unwrap();
765 let content = r#"{"dependencies": {"old-pkg": "1.0.1"}}"#.to_string();
768 let parse_result = ecosystem
769 .parse_manifest(&content, &uri)
770 .await
771 .expect("Failed to parse manifest");
772
773 let mut doc_state =
774 DocumentState::new_from_parse_result(EcosystemId::Npm, content, parse_result);
775
776 let mut cached = std::collections::HashMap::new();
777 cached.insert(
778 "old-pkg".into(),
779 deps_core::PackageVersions {
780 latest: "1.0.1".into(),
783 available: std::sync::Arc::from(vec!["1.0.1".into()]),
784 yanked: std::sync::Arc::from(vec![(
785 "1.0.1".into(),
786 deps_core::RemovalStatus::AdvisoryDeprecated,
787 )]),
788 published_at: None,
789 },
790 );
791 doc_state.update_cached_versions(cached);
792 state.update_document(uri.clone(), doc_state);
795
796 let (client, full_config) = create_test_client_and_config();
797 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
798
799 assert!(
800 result.is_empty(),
801 "expected no diagnostics for an exact pin satisfiable only by a flagged \
802 version — the #247 manifest-requirement diagnostic must stay suppressed for \
803 npm regardless of requirement shape (#436), got {result:?}"
804 );
805 }
806 }
807
808 #[cfg(feature = "deno")]
810 mod deno_tests {
811 use super::*;
812 use crate::document::DocumentState;
813 use deps_core::DiagnosticMessages;
814
815 #[tokio::test]
823 async fn test_handle_diagnostics_npm_scheme_exact_pin_yanked_stays_suppressed() {
824 let state = Arc::new(ServerState::new());
825 let uri = deps_core::test_util::test_uri("/test/deno.json");
826 let config = DiagnosticsConfig::default();
827
828 let ecosystem = state.ecosystem_registry.get("deno").unwrap();
829 let content = r#"{"imports": {"lodash": "npm:lodash@4.17.20"}}"#.to_string();
830 let parse_result = ecosystem
831 .parse_manifest(&content, &uri)
832 .await
833 .expect("Failed to parse manifest");
834
835 let mut doc_state =
836 DocumentState::new_from_parse_result(EcosystemId::Deno, content, parse_result);
837
838 let mut cached = std::collections::HashMap::new();
839 cached.insert(
840 "npm:lodash".into(),
841 deps_core::PackageVersions {
842 latest: "4.17.20".into(),
845 available: std::sync::Arc::from(vec!["4.17.20".into()]),
846 yanked: std::sync::Arc::from(vec![(
847 "4.17.20".into(),
848 deps_core::RemovalStatus::AdvisoryDeprecated,
849 )]),
850 published_at: None,
851 },
852 );
853 doc_state.update_cached_versions(cached);
854 state.update_document(uri.clone(), doc_state);
857
858 let (client, full_config) = create_test_client_and_config();
859 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
860
861 assert!(
862 result.is_empty(),
863 "expected no diagnostics for an exact-pin npm: specifier satisfiable only by a \
864 flagged version — the #247 manifest-requirement diagnostic must stay \
865 suppressed for deno's npm: scheme (#448), got {result:?}"
866 );
867 }
868
869 #[tokio::test]
874 async fn test_handle_diagnostics_jsr_scheme_exact_pin_yanked_still_fires() {
875 let state = Arc::new(ServerState::new());
876 let uri = deps_core::test_util::test_uri("/test/deno.json");
877 let config = DiagnosticsConfig::default();
878
879 let ecosystem = state.ecosystem_registry.get("deno").unwrap();
880 let content = r#"{"imports": {"@std/fs": "jsr:@std/fs@1.0.0"}}"#.to_string();
881 let parse_result = ecosystem
882 .parse_manifest(&content, &uri)
883 .await
884 .expect("Failed to parse manifest");
885
886 let mut doc_state =
887 DocumentState::new_from_parse_result(EcosystemId::Deno, content, parse_result);
888
889 let mut cached = std::collections::HashMap::new();
890 cached.insert(
891 "jsr:@std/fs".into(),
892 deps_core::PackageVersions {
893 latest: "1.0.1".into(),
894 available: std::sync::Arc::from(vec!["1.0.1".into(), "1.0.0".into()]),
895 yanked: std::sync::Arc::from(vec![(
896 "1.0.0".into(),
897 deps_core::RemovalStatus::Yanked,
898 )]),
899 published_at: None,
900 },
901 );
902 doc_state.update_cached_versions(cached);
903 state.update_document(uri.clone(), doc_state);
904
905 let (client, full_config) = create_test_client_and_config();
906 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
907
908 assert_eq!(
909 result.len(),
910 1,
911 "expected the #247 diagnostic to still fire for an exact-pin jsr: specifier, \
912 got {result:?}"
913 );
914 assert_eq!(
915 result[0].message,
916 format!(
917 "{}; latest is 1.0.1",
918 deps_deno::DenoFormatter.yanked_message()
919 )
920 );
921 }
922
923 #[tokio::test]
931 async fn test_handle_diagnostics_jsr_scheme_range_yanked_only_now_fires() {
932 let state = Arc::new(ServerState::new());
933 let uri = deps_core::test_util::test_uri("/test/deno.json");
934 let config = DiagnosticsConfig::default();
935
936 let ecosystem = state.ecosystem_registry.get("deno").unwrap();
937 let content = r#"{"imports": {"@std/fs": "jsr:@std/fs@^1.0.0"}}"#.to_string();
938 let parse_result = ecosystem
939 .parse_manifest(&content, &uri)
940 .await
941 .expect("Failed to parse manifest");
942
943 let mut doc_state =
944 DocumentState::new_from_parse_result(EcosystemId::Deno, content, parse_result);
945
946 let mut cached = std::collections::HashMap::new();
947 cached.insert(
948 "jsr:@std/fs".into(),
949 deps_core::PackageVersions {
950 latest: "1.0.1".into(),
953 available: std::sync::Arc::from(vec!["1.0.1".into(), "1.0.0".into()]),
954 yanked: std::sync::Arc::from(vec![
955 ("1.0.1".into(), deps_core::RemovalStatus::Yanked),
956 ("1.0.0".into(), deps_core::RemovalStatus::Yanked),
957 ]),
958 published_at: None,
959 },
960 );
961 doc_state.update_cached_versions(cached);
962 state.update_document(uri.clone(), doc_state);
965
966 let (client, full_config) = create_test_client_and_config();
967 let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
968
969 assert_eq!(
970 result.len(),
971 1,
972 "expected exactly one diagnostic for a jsr: range satisfiable only by yanked \
973 versions (#454), got {result:?}"
974 );
975 assert_eq!(
976 result[0].message,
977 format!(
978 "{}; latest is 1.0.1",
979 deps_deno::DenoFormatter.yanked_message()
980 )
981 );
982 }
983 }
984
985 #[cfg(feature = "pypi")]
987 mod pypi_tests {
988 use super::*;
989 use crate::document::DocumentState;
990
991 #[tokio::test]
992 async fn test_handle_diagnostics() {
993 let state = Arc::new(ServerState::new());
994 let uri = deps_core::test_util::test_uri("/test/pyproject.toml");
995 let config = DiagnosticsConfig::default();
996
997 let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
998 let content = r#"[project]
999dependencies = ["requests>=2.0.0"]
1000"#
1001 .to_string();
1002
1003 let parse_result = ecosystem
1004 .parse_manifest(&content, &uri)
1005 .await
1006 .expect("Failed to parse manifest");
1007
1008 let doc_state =
1009 DocumentState::new_from_parse_result(EcosystemId::Pypi, content, parse_result);
1010 state.update_document(uri.clone(), doc_state);
1011
1012 let (client, full_config) = create_test_client_and_config();
1013 let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
1014 }
1016 }
1017}