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    let parse_result = match doc.parse_result() {
52        Some(p) => p,
53        None => return vec![],
54    };
55
56    // Generate diagnostics while holding the lock
57    ecosystem
58        .generate_diagnostics(
59            parse_result,
60            &doc.cached_versions,
61            &doc.resolved_versions,
62            uri,
63        )
64        .await
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::config::DiagnosticsConfig;
71    use crate::document::ServerState;
72    use crate::test_utils::test_helpers::create_test_client_and_config;
73
74    // Generic tests (no feature flag required)
75
76    #[tokio::test]
77    async fn test_handle_diagnostics_missing_document() {
78        let state = Arc::new(ServerState::new());
79        let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
80        let config = DiagnosticsConfig::default();
81
82        let (client, full_config) = create_test_client_and_config();
83        let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
84        assert!(result.is_empty());
85    }
86
87    // Cargo-specific tests
88    #[cfg(feature = "cargo")]
89    mod cargo_tests {
90        use super::*;
91        use crate::document::{DocumentState, Ecosystem};
92
93        #[tokio::test]
94        async fn test_handle_diagnostics() {
95            let state = Arc::new(ServerState::new());
96            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
97            let config = DiagnosticsConfig::default();
98
99            let ecosystem = state.ecosystem_registry.get("cargo").unwrap();
100            let content = r#"[dependencies]
101serde = "1.0.0"
102"#
103            .to_string();
104
105            let parse_result = ecosystem
106                .parse_manifest(&content, &uri)
107                .await
108                .expect("Failed to parse manifest");
109
110            let doc_state = DocumentState::new_from_parse_result("cargo", content, parse_result);
111            state.update_document(uri.clone(), doc_state);
112
113            let (client, full_config) = create_test_client_and_config();
114            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
115            // Test passes if no panic occurs
116        }
117
118        #[tokio::test]
119        async fn test_handle_diagnostics_no_parse_result() {
120            let state = Arc::new(ServerState::new());
121            let uri = Uri::from_file_path("/test/Cargo.toml").unwrap();
122            let config = DiagnosticsConfig::default();
123
124            let doc_state = DocumentState::new(Ecosystem::Cargo, String::new(), vec![]);
125            state.update_document(uri.clone(), doc_state);
126
127            let (client, full_config) = create_test_client_and_config();
128            let result = handle_diagnostics(state, &uri, &config, client, full_config).await;
129            assert!(result.is_empty());
130        }
131    }
132
133    // npm-specific tests
134    #[cfg(feature = "npm")]
135    mod npm_tests {
136        use super::*;
137        use crate::document::DocumentState;
138
139        #[tokio::test]
140        async fn test_handle_diagnostics() {
141            let state = Arc::new(ServerState::new());
142            let uri = Uri::from_file_path("/test/package.json").unwrap();
143            let config = DiagnosticsConfig::default();
144
145            let ecosystem = state.ecosystem_registry.get("npm").unwrap();
146            let content = r#"{"dependencies": {"express": "4.0.0"}}"#.to_string();
147
148            let parse_result = ecosystem
149                .parse_manifest(&content, &uri)
150                .await
151                .expect("Failed to parse manifest");
152
153            let doc_state = DocumentState::new_from_parse_result("npm", content, parse_result);
154            state.update_document(uri.clone(), doc_state);
155
156            let (client, full_config) = create_test_client_and_config();
157            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
158            // Test passes if no panic occurs
159        }
160    }
161
162    // PyPI-specific tests
163    #[cfg(feature = "pypi")]
164    mod pypi_tests {
165        use super::*;
166        use crate::document::DocumentState;
167
168        #[tokio::test]
169        async fn test_handle_diagnostics() {
170            let state = Arc::new(ServerState::new());
171            let uri = Uri::from_file_path("/test/pyproject.toml").unwrap();
172            let config = DiagnosticsConfig::default();
173
174            let ecosystem = state.ecosystem_registry.get("pypi").unwrap();
175            let content = r#"[project]
176dependencies = ["requests>=2.0.0"]
177"#
178            .to_string();
179
180            let parse_result = ecosystem
181                .parse_manifest(&content, &uri)
182                .await
183                .expect("Failed to parse manifest");
184
185            let doc_state = DocumentState::new_from_parse_result("pypi", content, parse_result);
186            state.update_document(uri.clone(), doc_state);
187
188            let (client, full_config) = create_test_client_and_config();
189            let _result = handle_diagnostics(state, &uri, &config, client, full_config).await;
190            // Test passes if no panic occurs
191        }
192    }
193}