deps_lsp/handlers/
diagnostics.rs

1//! Diagnostics handler using ecosystem trait delegation.
2
3use crate::config::{DepsConfig, DiagnosticsConfig};
4use crate::document::{ServerState, ensure_document_loaded};
5use std::sync::Arc;
6use tokio::sync::RwLock;
7use tower_lsp_server::Client;
8use tower_lsp_server::ls_types::{Diagnostic, Uri};
9
10/// Handles diagnostic requests using trait-based delegation.
11pub async fn handle_diagnostics(
12    state: Arc<ServerState>,
13    uri: &Uri,
14    _config: &DiagnosticsConfig,
15    client: Client,
16    full_config: Arc<RwLock<DepsConfig>>,
17) -> Vec<Diagnostic> {
18    // Ensure document is loaded (cold start support)
19    if !ensure_document_loaded(uri, Arc::clone(&state), client, full_config).await {
20        tracing::warn!("Could not load document for diagnostics: {:?}", uri);
21        return vec![];
22    }
23
24    generate_diagnostics_internal(state, uri).await
25}
26
27/// Internal diagnostic generation without cold start support.
28///
29/// This is used when we know the document is already loaded (e.g., from background tasks).
30pub(crate) async fn generate_diagnostics_internal(
31    state: Arc<ServerState>,
32    uri: &Uri,
33) -> Vec<Diagnostic> {
34    // Single document lookup: extract all needed data at once
35    let doc = match state.get_document(uri) {
36        Some(d) => d,
37        None => {
38            tracing::warn!("Document not found for diagnostics: {:?}", uri);
39            return vec![];
40        }
41    };
42
43    let ecosystem = match state.ecosystem_registry.get(doc.ecosystem_id) {
44        Some(e) => e,
45        None => {
46            tracing::warn!("Ecosystem not found for diagnostics: {}", doc.ecosystem_id);
47            return vec![];
48        }
49    };
50
51    // Skip diagnostics while versions are still loading to avoid
52    // false "Unknown package" warnings from empty cache
53    if doc.loading_state == deps_core::LoadingState::Loading {
54        return vec![];
55    }
56
57    let parse_result = match doc.parse_result() {
58        Some(p) => p,
59        None => return vec![],
60    };
61
62    // Generate diagnostics while holding the lock
63    ecosystem
64        .generate_diagnostics(
65            parse_result,
66            &doc.cached_versions,
67            &doc.resolved_versions,
68            uri,
69        )
70        .await
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::config::DiagnosticsConfig;
77    use crate::document::ServerState;
78    use crate::test_utils::test_helpers::create_test_client_and_config;
79
80    // Generic tests (no feature flag required)
81
82    #[tokio::test]
83    async fn test_handle_diagnostics_missing_document() {
84        let state = Arc::new(ServerState::new());
85        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
86        let config = DiagnosticsConfig::default();
87
88        let (client, full_config) = create_test_client_and_config();
89        let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
90        assert!(result.is_empty());
91    }
92
93    // Cargo-specific tests
94    #[cfg(feature = "cargo")]
95    mod cargo_tests {
96        use super::*;
97        use crate::document::{DocumentState, Ecosystem};
98
99        #[tokio::test]
100        async fn test_handle_diagnostics() {
101            let state = Arc::new(ServerState::new());
102            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
103            let config = DiagnosticsConfig::default();
104
105            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
106            let content = r#"[dependencies]
107serde = "1.0.0"
108"#
109            .to_string();
110
111            let parse_result = ecosystem
112                .parse_manifest(&content, &uri)
113                .await
114                .expect("Failed to parse manifest");
115
116            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
117            state.update_document(uri.clone(), doc_state);
118
119            let (client, full_config) = create_test_client_and_config();
120            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
121            // Test passes if no panic occurs
122        }
123
124        #[tokio::test]
125        async fn test_handle_diagnostics_no_parse_result() {
126            let state = Arc::new(ServerState::new());
127            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
128            let config = DiagnosticsConfig::default();
129
130            let doc_state = DocumentState::new(Ecosystem::Cargo, String::new(), vec![]);
131            state.update_document(uri.clone(), doc_state);
132
133            let (client, full_config) = create_test_client_and_config();
134            let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
135            assert!(result.is_empty());
136        }
137    }
138
139    // npm-specific tests
140    #[cfg(feature = "npm")]
141    mod npm_tests {
142        use super::*;
143        use crate::document::DocumentState;
144
145        #[tokio::test]
146        async fn test_handle_diagnostics() {
147            let state = Arc::new(ServerState::new());
148            let uri = Uri::from_file_path("/test/package.json").unwrap();
149            let config = DiagnosticsConfig::default();
150
151            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
152            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
153
154            let parse_result = ecosystem
155                .parse_manifest(&content, &uri)
156                .await
157                .expect("Failed to parse manifest");
158
159            let doc_state = DocumentState::new_from_parse_result("npm", content, parse_result);
160            state.update_document(uri.clone(), doc_state);
161
162            let (client, full_config) = create_test_client_and_config();
163            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
164            // Test passes if no panic occurs
165        }
166    }
167
168    // PyPI-specific tests
169    #[cfg(feature = "pypi")]
170    mod pypi_tests {
171        use super::*;
172        use crate::document::DocumentState;
173
174        #[tokio::test]
175        async fn test_handle_diagnostics() {
176            let state = Arc::new(ServerState::new());
177            let uri = Uri::from_file_path("/test/pyproject.toml").unwrap();
178            let config = DiagnosticsConfig::default();
179
180            let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
181            let content = r#"[project]
182dependencies = ["requests>=2.0.0"]
183"#
184            .to_string();
185
186            let parse_result = ecosystem
187                .parse_manifest(&content, &uri)
188                .await
189                .expect("Failed to parse manifest");
190
191            let doc_state = DocumentState::new_from_parse_result("pypi", content, parse_result);
192            state.update_document(uri.clone(), doc_state);
193
194            let (client, full_config) = create_test_client_and_config();
195            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
196            // Test passes if no panic occurs
197        }
198    }
199}