1use crate::{
2 custom_requests::{
3 ApolloCanComposeNotification, ApolloCanComposeNotificationParams,
4 ApolloComposeServicesParams, ApolloComposeServicesRequest, ApolloComposeServicesResponse,
5 ApolloConfigureAutoCompositionNotification, ApolloConfigureAutoCompositionParams,
6 APOLLO_COMPOSITION_PROGRESS_TOKEN,
7 },
8 graph::{
9 supergraph::{KnownSubgraphs, Supergraph},
10 Graph, GraphConfig,
11 },
12 semantic_tokens::{incomplete_tokens_to_deltas, LEGEND_TYPE},
13 telemetry::{AnalyticsEvent, CompositionTiming, TelemetryEvent},
14};
15use apollo_federation_types::{
16 composition::Issue, config::SchemaSource, javascript::SubgraphDefinition,
17};
18use debounced::debounced;
19use futures::{
20 channel::mpsc::{channel, Receiver, Sender},
21 channel::oneshot,
22 future::join_all,
23 lock::Mutex,
24 SinkExt, StreamExt,
25};
26use serde::{Deserialize, Serialize};
27use std::path::PathBuf;
28use std::{borrow::Cow, collections::HashMap};
29use std::{sync::Arc, time::Duration};
30use tower_lsp::{
31 async_trait, jsonrpc,
32 lsp_types::{self as lsp, notification::Notification, request::Request},
33 Client, ClientSocket, LanguageServer, LspService,
34};
35
36#[cfg(all(feature = "wasm", not(test)))]
37use wasm_bindgen_futures::spawn_local as spawn;
38
39#[cfg(any(feature = "tokio", test))]
40use tokio::spawn;
41
42pub const LANGUAGE_IDS: [&str; 2] = ["apollo-graphql", "graphql"];
43
44#[derive(Debug, Clone, Deserialize, Serialize, Default)]
45pub struct MaxSpecVersions {
46 pub connect: Option<semver::Version>,
47 pub federation: Option<semver::Version>,
48}
49
50#[derive(Debug, Clone, Deserialize, Serialize)]
52#[serde(rename_all = "camelCase", default)]
53pub struct Config {
54 pub root_uri: String,
56 pub enable_auto_composition: bool,
58 pub force_federation: bool,
60 pub disable_telemetry: bool,
62 pub max_spec_versions: MaxSpecVersions,
65}
66
67impl Default for Config {
68 fn default() -> Self {
69 Self {
70 root_uri: "/".to_string(),
71 enable_auto_composition: false,
72 force_federation: false,
73 disable_telemetry: false,
74 max_spec_versions: MaxSpecVersions {
75 connect: None,
76 federation: None,
77 },
78 }
79 }
80}
81
82#[derive(Clone)]
84pub struct ApolloLanguageServer {
85 state: Arc<State>,
86}
87
88pub type SpecLookup = Sender<(PathBuf, oneshot::Sender<Option<String>>)>;
89
90#[derive(Debug)]
91struct State {
92 graph: Mutex<Option<Graph>>,
93 client: Mutex<Client>,
94 request_composition: Mutex<Sender<Vec<SubgraphDefinition>>>,
95 config: Mutex<Config>,
96 document_change_queue_sender: Mutex<Sender<lsp::DidChangeTextDocumentParams>>,
97 spec_lookup: Option<Mutex<SpecLookup>>,
98}
99
100const DOCUMENT_SIZE_DEBOUNCE_THRESHOLD: usize = 50_000;
101const DOCUMENT_UPDATE_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(150);
102
103impl ApolloLanguageServer {
104 fn new(
105 client: Client,
106 request_composition: Sender<Vec<SubgraphDefinition>>,
107 config: Config,
108 known_subgraphs: KnownSubgraphs,
109 spec_lookup: Option<SpecLookup>,
110 ) -> Self {
111 let (document_change_queue_sender, document_change_queue_receiver) =
112 channel::<lsp::DidChangeTextDocumentParams>(1);
113
114 let graph = (!known_subgraphs.by_name.is_empty() || config.force_federation).then_some(
116 Graph::Supergraph(Box::new(Supergraph::new(known_subgraphs))),
117 );
118
119 let state = State {
120 graph: Mutex::new(graph),
121 client: Mutex::new(client),
122 request_composition: Mutex::new(request_composition),
123 config: Mutex::new(config),
124 document_change_queue_sender: Mutex::new(document_change_queue_sender),
125 spec_lookup: spec_lookup.map(Mutex::new),
126 };
127 let state = Arc::new(state);
128 State::initialize_document_update_debouncer(state.clone(), document_change_queue_receiver);
129
130 Self { state }
131 }
132
133 pub fn build_service(
152 config: Config,
153 known_subgraphs: HashMap<String, SchemaSource>,
154 spec_lookup: Option<SpecLookup>,
155 ) -> (
156 LspService<Self>,
157 ClientSocket,
158 Receiver<Vec<SubgraphDefinition>>,
159 ) {
160 let (composition_request_sender, composition_request_receiver) =
161 channel::<Vec<SubgraphDefinition>>(1);
162
163 #[cfg(any(not(feature = "wasm"), test))]
164 let known_subgraphs = KnownSubgraphs {
165 root_uri: config.root_uri.clone(),
166 by_name: known_subgraphs
167 .iter()
168 .map(|(name, source)| (name.clone(), source.clone()))
169 .collect(),
170 by_uri: known_subgraphs
171 .iter()
172 .filter_map(|(name, source)| match source {
173 SchemaSource::File { file } => {
174 let uri = if file.is_relative() {
175 let url = lsp::Url::from_directory_path(&config.root_uri)
177 .expect("Failed to parse URL");
178 url.join(file.to_str().expect("Failed to convert path to string"))
179 .expect("Failed to join URL")
180 } else {
181 lsp::Url::from_file_path(file).expect("Failed to convert path to URL")
182 };
183
184 Some((uri, name.clone()))
185 }
186 _ => None,
187 })
188 .collect(),
189 };
190
191 #[cfg(all(feature = "wasm", not(test)))]
192 let known_subgraphs = KnownSubgraphs::default();
193
194 let (service, client_socket) = LspService::build(|client| {
195 ApolloLanguageServer::new(
196 client,
197 composition_request_sender,
198 config,
199 known_subgraphs,
200 spec_lookup,
201 )
202 })
203 .custom_method(
204 ApolloConfigureAutoCompositionNotification::METHOD,
205 ApolloLanguageServer::configure_auto_composition,
206 )
207 .custom_method(
208 ApolloComposeServicesRequest::METHOD,
209 ApolloLanguageServer::request_recompose,
210 )
211 .finish();
212 (service, client_socket, composition_request_receiver)
213 }
214
215 fn capabilities() -> lsp::InitializeResult {
216 lsp::InitializeResult {
217 capabilities: lsp::ServerCapabilities {
218 text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
219 lsp::TextDocumentSyncOptions {
220 change: Some(lsp::TextDocumentSyncKind::FULL),
221 open_close: Some(true),
222 ..Default::default()
223 },
224 )),
225 completion_provider: Some(lsp::CompletionOptions {
226 trigger_characters: Some(vec![
229 "@".to_string(),
231 "|".to_string(),
233 "&".to_string(),
235 "\"".to_string(),
237 ]),
238 ..Default::default()
239 }),
240 semantic_tokens_provider: Some(
241 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
242 lsp::SemanticTokensOptions {
243 work_done_progress_options: lsp::WorkDoneProgressOptions {
244 work_done_progress: None,
245 },
246 legend: lsp::SemanticTokensLegend {
247 token_types: LEGEND_TYPE.into(),
248 token_modifiers: vec![],
249 },
250 range: Some(false),
251 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: Some(true) }),
252 },
253 ),
254 ),
255 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
256 definition_provider: Some(lsp::OneOf::Left(true)),
257 ..Default::default()
258 },
259 ..Default::default()
260 }
261 }
262
263 async fn request_recompose(
264 &self,
265 _params: ApolloComposeServicesParams,
266 ) -> jsonrpc::Result<ApolloComposeServicesResponse> {
267 let did_compose = self.state.maybe_recompose().await;
268 Ok(ApolloComposeServicesResponse { did_compose })
269 }
270
271 async fn configure_auto_composition(&self, params: ApolloConfigureAutoCompositionParams) {
272 {
273 let mut config = self.state.config.lock().await;
274 config.enable_auto_composition = params.enabled;
275 }
276 self.state.maybe_auto_recompose().await;
277 }
278
279 pub async fn composition_did_start(&self) {
284 self.state
285 .client
286 .lock()
287 .await
288 .send_notification::<lsp::notification::Progress>(lsp::ProgressParams {
289 token: lsp::ProgressToken::String(APOLLO_COMPOSITION_PROGRESS_TOKEN.to_string()),
290 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
291 lsp::WorkDoneProgressBegin {
292 title: "Composing services".to_string(),
293 cancellable: Some(false),
294 message: None,
295 percentage: None,
296 },
297 )),
298 })
299 .await;
300 }
301
302 pub async fn composition_did_update(
309 &self,
310 _supergraph_sdl: Option<String>,
311 issues: Vec<Issue>,
312 timing: Option<Duration>,
313 ) {
314 self.state.composition_did_update(issues).await;
315 if let Some(timing) = timing {
316 self.send_telemetry_if_enabled(TelemetryEvent::Analytics(
317 AnalyticsEvent::CompositionTimeInMs(CompositionTiming {
318 value: timing.as_millis() as u64,
319 }),
320 ))
321 .await;
322 }
323 }
324
325 #[cfg(any(not(feature = "wasm"), test))]
329 pub async fn add_subgraph(&self, name: String, source: SchemaSource) {
330 let Some(Graph::Supergraph(supergraph)) = &mut *self.state.graph.lock().await else {
331 panic!("Graph is unexpectedly not a supergraph");
332 };
333
334 supergraph.add_known_subgraph(name, source).await;
335 }
336
337 #[cfg(any(not(feature = "wasm"), test))]
341 pub async fn remove_subgraph(&self, name: &str) {
342 let Some(Graph::Supergraph(supergraph)) = &mut *self.state.graph.lock().await else {
343 panic!("Graph is unexpectedly not a supergraph");
344 };
345
346 if let Some(uri) = supergraph.uri_for_name(name) {
347 self.state
348 .client
349 .lock()
350 .await
351 .publish_diagnostics(uri.clone(), vec![], None)
352 .await;
353 }
354
355 supergraph.remove_known_subgraph(name).await;
356 }
357
358 pub async fn publish_diagnostics(&self, uri: lsp::Url, diagnostics: Vec<lsp::Diagnostic>) {
360 let graph = self.state.graph.lock().await;
361 let version = graph.as_ref().and_then(|graph| graph.version_for_uri(&uri));
362
363 self.state
364 .client
365 .lock()
366 .await
367 .publish_diagnostics(uri, diagnostics, version)
368 .await;
369 }
370
371 async fn send_telemetry_if_enabled(&self, event: TelemetryEvent) {
372 if !self.state.config.lock().await.disable_telemetry {
373 self.state.client.lock().await.telemetry_event(event).await;
374 }
375 }
376
377 #[cfg(test)]
378 async fn config(&self) -> Config {
379 self.state.config.lock().await.clone()
380 }
381}
382
383impl State {
384 async fn maybe_auto_recompose(&self) {
385 if !self.config.lock().await.enable_auto_composition {
386 return;
387 }
388 self.maybe_recompose().await;
389 }
390
391 async fn maybe_send_can_compose_notification(&self) {
392 let invalid_subgraph_names = if let Some(Some(supergraph)) =
393 self.graph.lock().await.as_ref().map(Graph::supergraph)
394 {
395 supergraph.get_invalid_subgraph_uris()
399 } else {
400 return;
401 };
402
403 self.client
404 .lock()
405 .await
406 .send_notification::<ApolloCanComposeNotification>(ApolloCanComposeNotificationParams {
407 can_compose: invalid_subgraph_names.is_empty(),
408 subgraphs_with_errors: invalid_subgraph_names,
409 })
410 .await;
411 }
412
413 async fn maybe_recompose(&self) -> bool {
414 let subgraph_definitions = if let Some(Some(supergraph)) =
415 self.graph.lock().await.as_ref().map(Graph::supergraph)
416 {
417 if !supergraph.subgraphs_are_invalid() {
421 supergraph.subgraph_definitions()
422 } else {
423 return false;
424 }
425 } else {
426 return false;
427 };
428
429 self.request_composition
430 .lock()
431 .await
432 .send(subgraph_definitions)
433 .await
434 .expect("Failed to send message");
435 true
436 }
437
438 async fn composition_did_update(&self, issues: Vec<Issue>) {
439 let (diagnostics_by_subgraph, unattributed_diagnostics) = {
440 let graph = self.graph.lock().await;
441 match graph.as_ref() {
442 Some(Graph::Supergraph(supergraph)) => supergraph.diagnostics_for_composition(issues),
443 _ => panic!("Programming error: called `composition_did_update` when graph is not a supergraph."),
444 }
445 };
446
447 let client = self.client.lock().await;
448 join_all(
449 diagnostics_by_subgraph
450 .into_iter()
451 .map(|(url, (diagnostics, version))| {
452 client.publish_diagnostics(url, diagnostics, Some(version))
453 }),
454 )
455 .await;
456
457 client
458 .publish_diagnostics(
459 lsp::Url::parse(&self.config.lock().await.root_uri).expect("Failed to parse URL"),
460 unattributed_diagnostics,
461 0.into(),
462 )
463 .await;
464
465 client
466 .send_notification::<lsp::notification::Progress>(lsp::ProgressParams {
467 token: lsp::ProgressToken::String(APOLLO_COMPOSITION_PROGRESS_TOKEN.to_string()),
468 value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
469 lsp::WorkDoneProgressEnd { message: None },
470 )),
471 })
472 .await;
473 }
474
475 fn initialize_document_update_debouncer(
476 state: Arc<State>,
477 document_change_queue_receiver: Receiver<lsp::DidChangeTextDocumentParams>,
478 ) {
479 spawn(async move {
480 let mut debounced_document_change_queue = debounced(
481 document_change_queue_receiver,
482 DOCUMENT_UPDATE_DEBOUNCE_INTERVAL,
483 );
484
485 while let Some(data) = debounced_document_change_queue.next().await {
486 state.handle_document_update(data).await;
487 }
488 });
489 }
490
491 async fn get_fed_spec_for_uri(&self, uri: &lsp::Url) -> Option<String> {
492 let lookup = self.spec_lookup.as_ref()?;
493 let (sender, receiver) = oneshot::channel();
494 let mut guard = lookup.lock().await;
495 guard.send((PathBuf::from(uri.path()), sender)).await.ok();
496 receiver.await.ok().flatten()
497 }
498
499 async fn handle_document_update(&self, data: lsp::DidChangeTextDocumentParams) {
500 let lsp::DidChangeTextDocumentParams {
501 text_document,
502 content_changes,
503 } = data;
504 let lsp::VersionedTextDocumentIdentifier { uri, version } = text_document;
505 let text = content_changes
506 .into_iter()
507 .next()
508 .expect("Expected at least one content change")
509 .text;
510
511 let graph_config = {
512 let federation_spec = self.get_fed_spec_for_uri(&uri).await;
513 let config = self.config.lock().await;
514
515 GraphConfig {
516 force_federation: config.force_federation,
517 federation_spec,
518 }
519 };
520
521 let (diagnostics, version) = {
522 let mut graph = self.graph.lock().await;
523
524 let Some(graph) = graph.as_mut() else {
525 panic!("Attempted to change a document that hasn't been opened");
526 };
527 graph.update(uri.clone(), text, version, graph_config);
528
529 graph.diagnostics_for_uri(&uri)
530 };
531
532 self.client
533 .lock()
534 .await
535 .publish_diagnostics(uri.clone(), diagnostics, Some(version))
536 .await;
537
538 self.maybe_auto_recompose().await;
539 self.maybe_send_can_compose_notification().await;
540 }
541}
542
543#[async_trait]
544impl LanguageServer for ApolloLanguageServer {
545 async fn initialize(
546 &self,
547 initialize_params: lsp::InitializeParams,
548 ) -> jsonrpc::Result<lsp::InitializeResult> {
549 let options: Option<Config> = initialize_params
550 .initialization_options
551 .map(|options| {
552 serde_json::from_value(options).map_err(|err| jsonrpc::Error {
553 message: Cow::from(err.to_string()),
554 code: jsonrpc::ErrorCode::InvalidParams,
555 data: None,
556 })
557 })
558 .transpose()?;
559
560 let mut config = self.state.config.lock().await;
561 if let Some(options) = options {
562 *config = options;
563 }
564 config.root_uri = initialize_params
565 .root_uri
566 .map(|uri| uri.to_string())
567 .unwrap_or("inmemory://".to_string());
568
569 Ok(ApolloLanguageServer::capabilities())
570 }
571
572 async fn initialized(&self, _: lsp::InitializedParams) {
573 self.send_telemetry_if_enabled(TelemetryEvent::Analytics(AnalyticsEvent::Initialized))
574 .await;
575 }
576
577 async fn shutdown(&self) -> jsonrpc::Result<()> {
578 Ok(())
579 }
580
581 async fn did_open(&self, params: lsp::DidOpenTextDocumentParams) {
582 let lsp::TextDocumentItem {
583 uri,
584 version,
585 text,
586 language_id,
587 } = params.text_document;
588
589 if !LANGUAGE_IDS.contains(&language_id.as_str()) {
590 return;
591 }
592
593 let graph_config = {
594 let federation_spec = self.state.get_fed_spec_for_uri(&uri).await;
595 let config = self.state.config.lock().await;
596 GraphConfig {
597 force_federation: config.force_federation,
598 federation_spec,
599 }
600 };
601
602 let (diagnostics_for_uri, _) = {
606 let mut graph = self.state.graph.lock().await;
607 if graph.is_some() {
608 graph
609 .as_mut()
610 .unwrap()
611 .update(uri.clone(), text, version, graph_config);
612 } else {
613 let new_graph = Graph::new(
614 uri.clone(),
615 text,
616 version,
617 KnownSubgraphs::default(),
618 graph_config,
619 );
620 *graph = Some(new_graph);
621 };
622
623 graph.as_ref().unwrap().diagnostics_for_uri(&uri)
624 };
625
626 if !diagnostics_for_uri.is_empty() {
627 self.state
628 .client
629 .lock()
630 .await
631 .publish_diagnostics(uri, diagnostics_for_uri, Some(version))
632 .await;
633 }
634
635 self.state.maybe_auto_recompose().await;
636 self.state.maybe_send_can_compose_notification().await;
637 }
638
639 async fn did_change(&self, params: lsp::DidChangeTextDocumentParams) {
640 if params.content_changes[0].text.len() < DOCUMENT_SIZE_DEBOUNCE_THRESHOLD {
641 self.state.handle_document_update(params).await;
642 } else {
643 self.state
644 .document_change_queue_sender
645 .lock()
646 .await
647 .send(params.clone())
648 .await
649 .expect("Failed to send message")
650 }
651 }
652
653 async fn did_close(&self, params: lsp::DidCloseTextDocumentParams) {
654 {
655 let mut graph = self.state.graph.lock().await;
656 match &mut *graph {
657 Some(Graph::Monolith(_)) => {
658 *graph = None;
659 }
660 Some(Graph::Supergraph(supergraph)) => {
661 if supergraph.remove(¶ms.text_document.uri).is_none() {
662 panic!("Subgraph is unexpectedly None");
663 }
664 }
665 None => panic!("Graph is unexpectedly None"),
666 }
667 }
668 self.state.maybe_auto_recompose().await;
669 self.state.maybe_send_can_compose_notification().await;
670 }
671
672 async fn completion(
673 &self,
674 params: lsp::CompletionParams,
675 ) -> jsonrpc::Result<Option<lsp::CompletionResponse>> {
676 let graph = self.state.graph.lock().await;
677 let max_spec_versions = &self.state.config.lock().await.max_spec_versions;
678
679 Ok(graph.as_ref().and_then(|graph| {
680 graph.completions(
681 ¶ms.text_document_position.text_document.uri,
682 params.text_document_position.position,
683 max_spec_versions,
684 )
685 }))
686 }
687
688 async fn semantic_tokens_full(
689 &self,
690 params: lsp::SemanticTokensParams,
691 ) -> jsonrpc::Result<Option<lsp::SemanticTokensResult>> {
692 let graph = self.state.graph.lock().await;
693 Ok(graph.as_ref().map(|graph| {
694 let tokens = graph
695 .semantic_tokens_full(¶ms.text_document.uri)
696 .unwrap_or_default();
697 lsp::SemanticTokensResult::Tokens(lsp::SemanticTokens {
698 result_id: None,
699 data: incomplete_tokens_to_deltas(tokens),
700 })
701 }))
702 }
703
704 async fn hover(&self, params: lsp::HoverParams) -> jsonrpc::Result<Option<lsp::Hover>> {
705 let graph = self.state.graph.lock().await;
706 Ok(graph.as_ref().and_then(|graph| {
707 graph.on_hover(
708 ¶ms.text_document_position_params.text_document.uri,
709 ¶ms.text_document_position_params.position,
710 )
711 }))
712 }
713
714 async fn goto_definition(
715 &self,
716 params: lsp::GotoDefinitionParams,
717 ) -> jsonrpc::Result<Option<lsp::GotoDefinitionResponse>> {
718 let graph = self.state.graph.lock().await;
719 Ok(graph.as_ref().map(|graph| {
720 lsp::GotoDefinitionResponse::from(
721 graph
722 .goto_definition(
723 ¶ms.text_document_position_params.text_document.uri,
724 ¶ms.text_document_position_params.position,
725 )
726 .unwrap_or_default(),
727 )
728 }))
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use apollo_federation_types::composition::{Severity, SubgraphLocation};
736 use lsp::{InitializeParams, PublishDiagnosticsParams};
737 use serde_json::{from_value, json, to_value};
738 use tokio::{sync::Mutex, task::yield_now};
739 use tower::{Service, ServiceExt};
740 use tower_lsp::{jsonrpc, LspService, Server};
741
742 fn initialize_request(id: i64) -> jsonrpc::Request {
743 jsonrpc::Request::build("initialize")
744 .params(json!({"capabilities":{}}))
745 .id(id)
746 .finish()
747 }
748
749 fn initialize_request_with_server_configurations(id: i64) -> jsonrpc::Request {
750 jsonrpc::Request::build("initialize")
751 .params(
752 to_value(InitializeParams {
753 initialization_options: Some(json!({
754 "enableAutoComposition": true,
755 "disableTelemetry": true,
756 })),
757 process_id: None,
758 root_uri: Some(lsp::Url::from_directory_path("/path/to/project").unwrap()),
759 ..Default::default()
760 })
761 .unwrap(),
762 )
763 .id(id)
764 .finish()
765 }
766
767 fn initialized_notification() -> jsonrpc::Request {
768 jsonrpc::Request::build("initialized")
769 .params(json!({}))
770 .finish()
771 }
772
773 fn shutdown_request() -> jsonrpc::Request {
774 jsonrpc::Request::build("shutdown").finish()
775 }
776
777 fn server_capabilities() -> serde_json::Value {
778 serde_json::to_value(ApolloLanguageServer::capabilities()).unwrap()
779 }
780
781 async fn request(
782 server: &mut LspService<ApolloLanguageServer>,
783 request: jsonrpc::Request,
784 ) -> Option<jsonrpc::Response> {
785 server.ready().await.unwrap().call(request).await.unwrap()
786 }
787
788 #[allow(clippy::type_complexity)]
789 fn listen_to_channels(
790 mut socket: ClientSocket,
791 mut composition_listener: Receiver<Vec<SubgraphDefinition>>,
792 ) -> (
793 Arc<Mutex<Vec<jsonrpc::Request>>>,
794 Arc<Mutex<Vec<Vec<SubgraphDefinition>>>>,
795 ) {
796 let socket_messages = Arc::new(Mutex::new(Vec::default()));
797 let composition_listener_messages = Arc::new(Mutex::new(Vec::default()));
798
799 let socket_messages_inner = socket_messages.clone();
800 let composition_listener_messages_inner = composition_listener_messages.clone();
801 tokio::spawn(async move {
802 while let Some(data) = socket.next().await {
803 socket_messages_inner.lock().await.push(data)
804 }
805 });
806 tokio::spawn(async move {
807 while let Some(data) = composition_listener.next().await {
808 composition_listener_messages_inner.lock().await.push(data)
809 }
810 });
811
812 (socket_messages, composition_listener_messages)
813 }
814
815 #[tokio::test]
816 async fn initialization_and_shutdown() {
817 let (mut server, ..) =
818 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
819
820 assert_eq!(
821 request(&mut server, initialize_request(1)).await,
822 Some(jsonrpc::Response::from_ok(1.into(), server_capabilities()))
823 );
824
825 assert_eq!(request(&mut server, initialized_notification()).await, None);
826
827 assert_eq!(request(&mut server, shutdown_request()).await, None);
830 assert_eq!(request(&mut server, shutdown_request()).await, None);
831 }
832
833 #[tokio::test]
834 async fn initialization_with_client_config() {
835 let (mut server, ..) =
836 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
837
838 assert_eq!(
839 request(
840 &mut server,
841 initialize_request_with_server_configurations(1)
842 )
843 .await,
844 Some(jsonrpc::Response::from_ok(1.into(), server_capabilities()))
845 );
846
847 assert!(server.inner().config().await.enable_auto_composition);
848 assert_eq!(
849 server.inner().config().await.root_uri,
850 "file:///path/to/project/".to_string()
851 );
852
853 assert_eq!(request(&mut server, initialized_notification()).await, None);
854 }
855
856 #[tokio::test]
857 async fn initialization_without_root_uri_specified() {
858 let (mut server, ..) =
859 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
860
861 assert_eq!(
862 request(&mut server, initialize_request(1)).await,
863 Some(jsonrpc::Response::from_ok(1.into(), server_capabilities()))
864 );
865
866 assert_eq!(
867 server.inner().state.config.lock().await.root_uri,
868 "inmemory://".to_string()
869 );
870
871 assert_eq!(request(&mut server, initialized_notification()).await, None);
872 }
873
874 #[tokio::test]
875 async fn diagnostics() {
876 let (mut server, socket, composition_listener) =
877 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
878
879 let (socket_messages, composition_listener_messages) =
880 listen_to_channels(socket, composition_listener);
881
882 request(
883 &mut server,
884 initialize_request_with_server_configurations(1),
886 )
887 .await;
888 request(&mut server, initialized_notification()).await;
889
890 request(
894 &mut server,
895 jsonrpc::Request::build("textDocument/didOpen")
896 .params(json!({
897 "textDocument": {
898 "uri": "file:///path/to/file",
899 "version": 1,
900 "languageId": "apollo-graphql",
901 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }"
902 }
903 }))
904 .finish(),
905 )
906 .await;
907
908 request(
909 &mut server,
910 jsonrpc::Request::build("textDocument/didChange")
911 .params(json!({
912 "textDocument": {
913 "uri": "file:///path/to/file",
914 "version": 2,
915 },
916 "contentChanges": [{
917 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String! }"
918 }]
919 }))
920 .finish(),
921 )
922 .await;
923
924 yield_now().await;
926
927 let messages = &*socket_messages.lock().await;
928 assert_eq!(
929 messages,
930 &[
931 jsonrpc::Request::build("apollo/canCompose")
933 .params(json!({"canCompose": true, "subgraphsWithErrors": []}))
934 .finish(),
935 jsonrpc::Request::build("textDocument/publishDiagnostics")
937 .params(json!({"uri": "file:///path/to/file", "diagnostics": [], "version": 2}))
938 .finish(),
939 jsonrpc::Request::build("apollo/canCompose")
940 .params(json!({"canCompose": true, "subgraphsWithErrors": []}))
941 .finish(),
942 ]
943 );
944
945 let composition_messages = &*composition_listener_messages.lock().await;
946 assert_eq!(composition_messages, &[
947 [
948 SubgraphDefinition {
949 name: "file:///path/to/file".into(),
950 url: "file:///path/to/file".into(),
951 sdl: "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }".into()
952 }
953 ],[
954 SubgraphDefinition {
955 name: "file:///path/to/file".into(),
956 url: "file:///path/to/file".into(),
957 sdl: "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String! }".into()
958 }
959 ]
960 ]);
961 }
962
963 #[tokio::test]
964 async fn registers_non_zero_document_version_on_open() {
965 let (mut server, socket, composition_listener) =
966 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
967
968 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
969
970 request(
971 &mut server,
972 initialize_request_with_server_configurations(1),
974 )
975 .await;
976 request(&mut server, initialized_notification()).await;
977
978 request(
979 &mut server,
980 jsonrpc::Request::build("textDocument/didOpen")
981 .params(json!({
982 "textDocument": {
983 "uri": "file:///path/to/file",
984 "version": 42,
985 "languageId": "apollo-graphql",
986 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String"
987 }
988 }))
989 .finish(),
990 )
991 .await;
992
993 let messages = &*socket_messages.lock().await;
994 assert_eq!(messages[0].params().unwrap()["version"], 42);
997 }
998
999 #[tokio::test]
1000 async fn runs_in_tower_lsp_server() {
1001 fn mock_msg(msg: &str) -> String {
1002 format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg)
1003 }
1004
1005 let mock_request = mock_msg(
1006 r#"{"jsonrpc":"2.0","method":"initialize","params":{"capabilities":{}},"id":0}"#,
1007 );
1008
1009 let mock_response = mock_msg(
1010 &serde_json::to_string(&jsonrpc::Response::from_ok(0.into(), server_capabilities()))
1011 .unwrap(),
1012 );
1013
1014 let (sender, _) = channel::<Vec<SubgraphDefinition>>(1);
1015 let (service, socket) = LspService::new(|client| {
1016 ApolloLanguageServer::new(client, sender, Config::default(), Default::default(), None)
1017 });
1018
1019 let mut stdout = Vec::new();
1020
1021 Server::new(&mut mock_request.as_bytes(), &mut stdout, socket)
1022 .serve(service)
1023 .await;
1024
1025 let stdout = String::from_utf8(stdout).unwrap();
1026 assert_eq!(stdout, mock_response);
1027 }
1028
1029 #[tokio::test]
1030 async fn did_close() {
1031 let (mut server, _, mut composition_listener) =
1032 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
1033
1034 request(
1035 &mut server,
1036 initialize_request_with_server_configurations(1),
1038 )
1039 .await;
1040 request(&mut server, initialized_notification()).await;
1041
1042 request(
1045 &mut server,
1046 jsonrpc::Request::build("textDocument/didOpen")
1047 .params(json!({
1048 "textDocument": {
1049 "uri": "file:///path/to/file1",
1050 "version": 1,
1051 "languageId": "apollo-graphql",
1052 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }"
1053 }
1054 }))
1055 .finish(),
1056 )
1057 .await;
1058
1059 composition_listener.next().await.unwrap();
1062
1063 request(
1064 &mut server,
1065 jsonrpc::Request::build("textDocument/didOpen")
1066 .params(json!({
1067 "textDocument": {
1068 "uri": "file:///path/to/file2",
1069 "version": 1,
1070 "languageId": "apollo-graphql",
1071 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { goodbye: String! }"
1072 },
1073 }))
1074 .finish(),
1075 )
1076 .await;
1077
1078 composition_listener.next().await.unwrap();
1081
1082 request(
1083 &mut server,
1084 jsonrpc::Request::build("textDocument/didClose")
1085 .params(json!({
1086 "textDocument": {
1087 "uri": "file:///path/to/file1",
1088 }
1089 }))
1090 .finish(),
1091 )
1092 .await;
1093
1094 composition_listener.next().await.unwrap();
1097 }
1098
1099 #[tokio::test]
1100 async fn custom_notification_toggle_auto_composition() {
1101 let (mut server, _, _) =
1102 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
1103
1104 request(
1105 &mut server,
1106 initialize_request_with_server_configurations(1),
1107 )
1108 .await;
1109 request(&mut server, initialized_notification()).await;
1110
1111 assert!(server.inner().config().await.enable_auto_composition);
1112 request(
1113 &mut server,
1114 jsonrpc::Request::build(ApolloConfigureAutoCompositionNotification::METHOD)
1115 .params(json!({"enabled": false}))
1116 .finish(),
1117 )
1118 .await;
1119 assert!(!server.inner().config().await.enable_auto_composition);
1120 request(
1121 &mut server,
1122 jsonrpc::Request::build(ApolloConfigureAutoCompositionNotification::METHOD)
1123 .params(json!({"enabled": true}))
1124 .finish(),
1125 )
1126 .await;
1127 assert!(server.inner().config().await.enable_auto_composition);
1128 }
1129
1130 #[tokio::test]
1131 async fn custom_notification_trigger_recomposition() {
1132 let (mut server, socket, composition_listener) =
1133 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
1134
1135 let (socket_messages, composition_listener_messages) =
1136 listen_to_channels(socket, composition_listener);
1137
1138 request(
1139 &mut server,
1140 initialize_request_with_server_configurations(1),
1141 )
1142 .await;
1143
1144 request(&mut server, initialized_notification()).await;
1145
1146 request(
1147 &mut server,
1148 jsonrpc::Request::build("textDocument/didOpen")
1149 .params(json!({
1150 "textDocument": {
1151 "uri": "file:///path/to/file1",
1152 "version": 1,
1153 "languageId": "apollo-graphql",
1154 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }"
1155 }
1156 }))
1157 .finish(),
1158 ).await;
1159
1160 request(
1161 &mut server,
1162 jsonrpc::Request::build(ApolloComposeServicesRequest::METHOD)
1163 .id(100)
1164 .params(json!({}))
1165 .finish(),
1166 )
1167 .await;
1168
1169 let messages = &*socket_messages.lock().await;
1170 assert_eq!(
1171 messages,
1172 &[jsonrpc::Request::build("apollo/canCompose")
1173 .params(json!({"canCompose": true, "subgraphsWithErrors": []}))
1174 .finish(),]
1175 );
1176
1177 let composition_messages = &*composition_listener_messages.lock().await;
1178 assert_eq!(composition_messages, &[
1179 [
1180 SubgraphDefinition {
1181 name: "file:///path/to/file1".into(),
1182 url: "file:///path/to/file1".into(),
1183 sdl: "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }".into()
1184 }
1185 ],
1186 [
1188 SubgraphDefinition {
1189 name: "file:///path/to/file1".into(),
1190 url: "file:///path/to/file1".into(),
1191 sdl: "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }".into()
1192 }
1193 ]
1194 ]);
1195 }
1196
1197 #[tokio::test]
1198 async fn with_graphql_language_id() {
1199 let (mut server, _, mut composition_listener) =
1200 ApolloLanguageServer::build_service(Config::default(), HashMap::new(), None);
1201
1202 request(
1203 &mut server,
1204 initialize_request_with_server_configurations(1),
1206 )
1207 .await;
1208 request(&mut server, initialized_notification()).await;
1209
1210 request(
1213 &mut server,
1214 jsonrpc::Request::build("textDocument/didOpen")
1215 .params(json!({
1216 "textDocument": {
1217 "uri": "file:///path/to/file1",
1218 "version": 1,
1219 "languageId": "graphql",
1220 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }"
1221 }
1222 }))
1223 .finish(),
1224 )
1225 .await;
1226
1227 composition_listener.next().await.unwrap();
1230
1231 request(
1232 &mut server,
1233 jsonrpc::Request::build("textDocument/didChange")
1234 .params(json!({
1235 "textDocument": {
1236 "uri": "file:///path/to/file1",
1237 "version": 2,
1238 },
1239 "contentChanges": [{
1240 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String! }"
1241 }]
1242 }))
1243 .finish(),
1244 )
1245 .await;
1246
1247 composition_listener.next().await.unwrap();
1250
1251 request(
1252 &mut server,
1253 jsonrpc::Request::build("textDocument/didClose")
1254 .params(json!({
1255 "textDocument": {
1256 "uri": "file:///path/to/file1",
1257 }
1258 }))
1259 .finish(),
1260 )
1261 .await;
1262
1263 composition_listener.next().await.unwrap();
1266 }
1267
1268 #[tokio::test]
1269 async fn publishing_diagnostics_to_known_subgraphs() {
1270 let (mut service, socket, composition_listener) = ApolloLanguageServer::build_service(
1274 Config {
1275 root_uri: "/path/to/project".into(),
1276 ..Default::default()
1277 },
1278 [
1279 (
1280 "local".into(),
1281 SchemaSource::File {
1282 file: "local.graphql".into(),
1283 },
1284 ),
1285 (
1286 "local_relative".into(),
1287 SchemaSource::File {
1288 file: "../../local_relative.graphql".into(),
1289 },
1290 ),
1291 (
1292 "remote".into(),
1293 SchemaSource::Subgraph {
1294 graphref: "testing@current".into(),
1295 subgraph: "remote".into(),
1296 },
1297 ),
1298 ]
1299 .into_iter()
1300 .collect(),
1301 None,
1302 );
1303
1304 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
1305
1306 request(
1307 &mut service,
1308 initialize_request_with_server_configurations(1),
1309 )
1310 .await;
1311
1312 request(&mut service, initialized_notification()).await;
1313
1314 request(&mut service, jsonrpc::Request::build("textDocument/didOpen")
1316 .params(json!({
1317 "textDocument": {
1318 "uri": "file:///path/to/project/local.graphql",
1319 "version": 1,
1320 "languageId": "graphql",
1321 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello: String }"
1322 }
1323 }))
1324 .finish()).await;
1325
1326 request(&mut service, jsonrpc::Request::build("textDocument/didOpen")
1328 .params(json!({
1329 "textDocument": {
1330 "uri": "file:///path/local_relative.graphql",
1331 "version": 1,
1332 "languageId": "graphql",
1333 "text": "extend schema @link(url: \"https://specs.apollo.dev/federation/v2.10\")\ntype Query { hello2: String }"
1334 }
1335 }))
1336 .finish()).await;
1337
1338 service
1339 .inner()
1340 .composition_did_update(
1341 None,
1342 vec![
1343 Issue {
1344 code: "TEST_REMOTE".into(),
1345 message: "Test issue".into(),
1346 locations: vec![SubgraphLocation {
1347 subgraph: Some("remote".into()),
1348 range: None,
1349 }],
1350 severity: Severity::Error,
1351 },
1352 Issue {
1353 code: "TEST_LOCAL".into(),
1354 message: "Test issue".into(),
1355 locations: vec![SubgraphLocation {
1356 subgraph: Some("local".into()),
1357 range: None,
1358 }],
1359 severity: Severity::Error,
1360 },
1361 Issue {
1362 code: "TEST_LOCAL_RELATIVE".into(),
1363 message: "Test issue".into(),
1364 locations: vec![SubgraphLocation {
1365 subgraph: Some("local_relative".into()),
1366 range: None,
1367 }],
1368 severity: Severity::Error,
1369 },
1370 ],
1371 None,
1372 )
1373 .await;
1374
1375 let lock = socket_messages.lock().await;
1376 let diagnostics = lock
1377 .iter()
1378 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1379 .map(|msg| {
1380 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1381 })
1382 .collect::<Vec<_>>();
1383
1384 assert_eq!(diagnostics.len(), 3);
1385 assert!(diagnostics
1386 .iter()
1387 .any(|diag| diag.uri.as_str() == "file:///path/to/project/"));
1388 assert!(diagnostics
1389 .iter()
1390 .any(|diag| diag.uri.as_str() == "file:///path/local_relative.graphql"));
1391
1392 let local = diagnostics
1396 .iter()
1397 .find(|diag| diag.uri.as_str() == "file:///path/to/project/local.graphql")
1398 .unwrap();
1399 assert_eq!(&local.version, &Some(1));
1400 }
1401
1402 #[tokio::test]
1403 async fn publishing_diagnostics_to_known_subgraphs_without_opening_files() {
1404 let (mut service, socket, composition_listener) = ApolloLanguageServer::build_service(
1408 Config {
1409 root_uri: "/path/to/project".into(),
1410 ..Default::default()
1411 },
1412 [
1413 (
1414 "local".into(),
1415 SchemaSource::File {
1416 file: "local.graphql".into(),
1417 },
1418 ),
1419 (
1420 "local_relative".into(),
1421 SchemaSource::File {
1422 file: "../../local_relative.graphql".into(),
1423 },
1424 ),
1425 (
1426 "remote".into(),
1427 SchemaSource::Subgraph {
1428 graphref: "testing@current".into(),
1429 subgraph: "remote".into(),
1430 },
1431 ),
1432 ]
1433 .into_iter()
1434 .collect(),
1435 None,
1436 );
1437
1438 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
1439
1440 request(
1441 &mut service,
1442 initialize_request_with_server_configurations(1),
1443 )
1444 .await;
1445
1446 request(&mut service, initialized_notification()).await;
1447
1448 service
1449 .inner()
1450 .composition_did_update(
1451 None,
1452 vec![
1453 Issue {
1454 code: "TEST_REMOTE".into(),
1455 message: "Test issue".into(),
1456 locations: vec![SubgraphLocation {
1457 subgraph: Some("remote".into()),
1458 range: None,
1459 }],
1460 severity: Severity::Error,
1461 },
1462 Issue {
1463 code: "TEST_LOCAL".into(),
1464 message: "Test issue".into(),
1465 locations: vec![SubgraphLocation {
1466 subgraph: Some("local".into()),
1467 range: None,
1468 }],
1469 severity: Severity::Error,
1470 },
1471 Issue {
1472 code: "TEST_LOCAL_RELATIVE".into(),
1473 message: "Test issue".into(),
1474 locations: vec![SubgraphLocation {
1475 subgraph: Some("local_relative".into()),
1476 range: None,
1477 }],
1478 severity: Severity::Error,
1479 },
1480 ],
1481 None,
1482 )
1483 .await;
1484
1485 let lock = socket_messages.lock().await;
1486 let diagnostics = lock
1487 .iter()
1488 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1489 .map(|msg| {
1490 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1491 })
1492 .collect::<Vec<_>>();
1493
1494 assert_eq!(diagnostics.len(), 3);
1495 assert!(diagnostics
1496 .iter()
1497 .any(|diag| diag.uri.as_str() == "file:///path/to/project/"));
1498 assert!(diagnostics
1499 .iter()
1500 .any(|diag| diag.uri.as_str() == "file:///path/to/project/local.graphql"));
1501 assert!(diagnostics
1502 .iter()
1503 .any(|diag| diag.uri.as_str() == "file:///path/local_relative.graphql"));
1504 }
1505
1506 #[tokio::test]
1507 async fn host_can_publish_diagnostics() {
1508 let (mut service, socket, composition_listener) = ApolloLanguageServer::build_service(
1509 Config {
1510 root_uri: "/path/to/project".into(),
1511 ..Default::default()
1512 },
1513 HashMap::new(),
1514 None,
1515 );
1516
1517 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
1518
1519 request(
1520 &mut service,
1521 initialize_request_with_server_configurations(1),
1522 )
1523 .await;
1524
1525 request(&mut service, initialized_notification()).await;
1526
1527 let diagnostics = vec![lsp::Diagnostic::new_simple(
1528 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1529 "supergraph.yaml missing fields".into(),
1530 )];
1531
1532 service
1533 .inner()
1534 .publish_diagnostics(
1535 lsp::Url::from_file_path("/path/to/project/supergraph.yaml").unwrap(),
1536 diagnostics,
1537 )
1538 .await;
1539
1540 yield_now().await;
1541
1542 let lock = socket_messages.lock().await;
1543 let diagnostics_published_to_client = lock
1544 .iter()
1545 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1546 .map(|msg| {
1547 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1548 })
1549 .collect::<Vec<_>>();
1550
1551 assert_eq!(diagnostics_published_to_client.len(), 1);
1552 assert_eq!(
1553 diagnostics_published_to_client[0].uri.as_str(),
1554 "file:///path/to/project/supergraph.yaml"
1555 );
1556 }
1557
1558 #[tokio::test]
1559 async fn stale_diagnostics_removed_on_remote_subgraph_removal() {
1560 let (mut service, socket, composition_listener) = ApolloLanguageServer::build_service(
1561 Config {
1562 root_uri: "/path/to/project".into(),
1563 ..Default::default()
1564 },
1565 [(
1566 "remote".into(),
1567 SchemaSource::Subgraph {
1568 graphref: "testing@current".into(),
1569 subgraph: "remote".into(),
1570 },
1571 )]
1572 .into_iter()
1573 .collect(),
1574 None,
1575 );
1576
1577 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
1578
1579 request(
1580 &mut service,
1581 initialize_request_with_server_configurations(1),
1582 )
1583 .await;
1584
1585 request(&mut service, initialized_notification()).await;
1586
1587 service
1588 .inner()
1589 .composition_did_update(
1590 None,
1591 vec![Issue {
1592 code: "TEST_REMOTE".into(),
1593 message: "Test issue".into(),
1594 locations: vec![SubgraphLocation {
1595 subgraph: Some("remote".into()),
1596 range: None,
1597 }],
1598 severity: Severity::Error,
1599 }],
1600 None,
1601 )
1602 .await;
1603 {
1604 let messages = &mut *socket_messages.lock().await;
1605 let diagnostics = messages
1606 .iter()
1607 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1608 .map(|msg| {
1609 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1610 })
1611 .collect::<Vec<_>>();
1612
1613 assert_eq!(diagnostics.len(), 1);
1614 assert!(diagnostics
1615 .iter()
1616 .any(|diag| diag.uri.as_str() == "file:///path/to/project/"
1617 && diag.diagnostics.len() == 1));
1618
1619 messages.clear();
1620 }
1621
1622 service.inner().remove_subgraph("remote").await;
1623
1624 {
1625 let messages = socket_messages.lock().await;
1626 let diagnostics = messages
1627 .iter()
1628 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1629 .map(|msg| {
1630 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1631 })
1632 .collect::<Vec<_>>();
1633
1634 assert_eq!(diagnostics.len(), 0);
1640 }
1641 }
1642
1643 #[tokio::test]
1644 async fn stale_diagnostics_removed_on_local_removal() {
1645 let (mut service, socket, composition_listener) = ApolloLanguageServer::build_service(
1646 Config {
1647 root_uri: "/path/to/project".into(),
1648 ..Default::default()
1649 },
1650 [(
1651 "local".into(),
1652 SchemaSource::File {
1653 file: "local.graphql".into(),
1654 },
1655 )]
1656 .into_iter()
1657 .collect(),
1658 None,
1659 );
1660
1661 let (socket_messages, _) = listen_to_channels(socket, composition_listener);
1662
1663 request(
1664 &mut service,
1665 initialize_request_with_server_configurations(1),
1666 )
1667 .await;
1668
1669 request(&mut service, initialized_notification()).await;
1670
1671 service
1672 .inner()
1673 .composition_did_update(
1674 None,
1675 vec![Issue {
1676 code: "TEST_LOCAL".into(),
1677 message: "Test issue".into(),
1678 locations: vec![SubgraphLocation {
1679 subgraph: Some("local".into()),
1680 range: None,
1681 }],
1682 severity: Severity::Error,
1683 }],
1684 None,
1685 )
1686 .await;
1687 {
1688 let messages = &mut *socket_messages.lock().await;
1689 let diagnostics = messages
1690 .iter()
1691 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1692 .map(|msg| {
1693 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1694 })
1695 .collect::<Vec<_>>();
1696
1697 assert_eq!(diagnostics.len(), 2);
1698 assert!(diagnostics.iter().any(|diag| diag.uri.as_str()
1699 == "file:///path/to/project/local.graphql"
1700 && diag.diagnostics.len() == 1));
1701
1702 messages.clear();
1703 }
1704
1705 service.inner().remove_subgraph("local").await;
1706
1707 {
1708 let messages = socket_messages.lock().await;
1709 let diagnostics = messages
1710 .iter()
1711 .filter(|msg| msg.method() == "textDocument/publishDiagnostics")
1712 .map(|msg| {
1713 from_value::<PublishDiagnosticsParams>(msg.params().unwrap().clone()).unwrap()
1714 })
1715 .collect::<Vec<_>>();
1716
1717 assert_eq!(diagnostics.len(), 1);
1719 assert!(diagnostics.iter().any(|diag| diag.uri.as_str()
1720 == "file:///path/to/project/local.graphql"
1721 && diag.diagnostics.is_empty()));
1722 }
1723 }
1724}