1use std::ffi::OsStr;
4use std::fmt;
5use std::future::Future;
6use std::mem::ManuallyDrop;
7use std::ops::Range;
8use std::path::Path;
9use std::path::PathBuf;
10use std::path::absolute;
11use std::sync::Arc;
12use std::thread::JoinHandle;
13
14use anyhow::Context;
15use anyhow::Error;
16use anyhow::Result;
17use anyhow::anyhow;
18use anyhow::bail;
19use ignore::WalkBuilder;
20use indexmap::IndexSet;
21use line_index::LineCol;
22use line_index::LineIndex;
23use line_index::WideEncoding;
24use line_index::WideLineCol;
25use lsp_types::CallHierarchyIncomingCall;
26use lsp_types::CallHierarchyItem;
27use lsp_types::CallHierarchyOutgoingCall;
28use lsp_types::CodeLens;
29use lsp_types::CompletionResponse;
30use lsp_types::DocumentSymbolResponse;
31use lsp_types::FoldingRange;
32use lsp_types::GotoDefinitionResponse;
33use lsp_types::Hover;
34use lsp_types::InlayHint;
35use lsp_types::Location;
36use lsp_types::SemanticTokensResult;
37use lsp_types::SignatureHelp;
38use lsp_types::SymbolInformation;
39use lsp_types::WorkspaceEdit;
40use path_clean::PathClean;
41use tokio::runtime::Handle;
42use tokio::sync::mpsc;
43use tokio::sync::oneshot;
44use url::Url;
45
46use crate::config::Config;
47use crate::document::Document;
48use crate::graph::DocumentGraphNode;
49use crate::graph::ParseState;
50use crate::queue::AddRequest;
51use crate::queue::AnalysisQueue;
52use crate::queue::AnalyzeRequest;
53use crate::queue::CallHierarchyRequest;
54use crate::queue::CodeLensRequest;
55use crate::queue::CompletionRequest;
56use crate::queue::DeleteRequest;
57use crate::queue::DocumentSymbolRequest;
58use crate::queue::FindAllReferencesRequest;
59use crate::queue::FoldingRangeRequest;
60use crate::queue::FormatRequest;
61use crate::queue::GotoDefinitionRequest;
62use crate::queue::HoverRequest;
63use crate::queue::IncomingCallsRequest;
64use crate::queue::InlayHintsRequest;
65use crate::queue::NotifyChangeRequest;
66use crate::queue::NotifyIncrementalChangeRequest;
67use crate::queue::OutgoingCallsRequest;
68use crate::queue::RenameRequest;
69use crate::queue::Request;
70use crate::queue::SemanticTokenRequest;
71use crate::queue::SignatureHelpRequest;
72use crate::queue::SwapValidatorRequest;
73use crate::queue::UnrootDocumentsRequest;
74use crate::queue::WorkspaceSymbolRequest;
75use crate::rayon::RayonHandle;
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ProgressKind {
80 Parsing,
82 Analyzing,
84}
85
86impl fmt::Display for ProgressKind {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::Parsing => write!(f, "parsing"),
90 Self::Analyzing => write!(f, "analyzing"),
91 }
92 }
93}
94
95pub fn path_to_uri(path: impl AsRef<Path>) -> Option<Url> {
97 Url::from_file_path(absolute(path).ok()?.clean()).ok()
98}
99
100#[derive(Debug, Clone)]
104pub struct AnalysisResult {
105 error: Option<Arc<Error>>,
108 version: Option<i32>,
114 lines: Option<Arc<LineIndex>>,
116 document: Document,
118}
119
120impl AnalysisResult {
121 pub(crate) fn new(node: &DocumentGraphNode) -> Self {
123 if let Some(error) = node.analysis_error() {
124 return Self {
125 error: Some(error.clone()),
126 version: node.parse_state().version(),
127 lines: node.parse_state().lines().cloned(),
128 document: Document::default_from_uri(node.uri().clone()),
129 };
130 }
131
132 let (error, version, lines) = match node.parse_state() {
133 ParseState::NotParsed => unreachable!("document should have been parsed"),
134 ParseState::Error(e) => (Some(e), None, None),
135 ParseState::Parsed { version, lines, .. } => (None, *version, Some(lines)),
136 };
137
138 Self {
139 error: error.cloned(),
140 version,
141 lines: lines.cloned(),
142 document: node
143 .document()
144 .expect("analysis should have completed")
145 .clone(),
146 }
147 }
148
149 pub fn error(&self) -> Option<&Arc<Error>> {
155 self.error.as_ref()
156 }
157
158 pub fn version(&self) -> Option<i32> {
163 self.version
164 }
165
166 pub fn lines(&self) -> Option<&Arc<LineIndex>> {
170 self.lines.as_ref()
171 }
172
173 pub fn document(&self) -> &Document {
175 &self.document
176 }
177}
178
179#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default)]
181pub struct SourcePosition {
182 pub line: u32,
185 pub character: u32,
188}
189
190impl SourcePosition {
191 pub fn new(line: u32, character: u32) -> Self {
193 Self { line, character }
194 }
195}
196
197#[derive(Debug, Eq, PartialEq, Copy, Clone)]
199pub enum SourcePositionEncoding {
200 UTF8,
204 UTF16,
208}
209
210#[derive(Debug, Clone)]
212pub struct SourceEdit {
213 range: Range<SourcePosition>,
217 encoding: SourcePositionEncoding,
219 text: String,
221}
222
223impl SourceEdit {
224 pub fn new(
226 range: Range<SourcePosition>,
227 encoding: SourcePositionEncoding,
228 text: impl Into<String>,
229 ) -> Self {
230 Self {
231 range,
232 encoding,
233 text: text.into(),
234 }
235 }
236
237 pub(crate) fn range(&self) -> Range<SourcePosition> {
239 self.range.start..self.range.end
240 }
241
242 pub(crate) fn apply(&self, source: &mut String, lines: &LineIndex) -> Result<()> {
244 let (start, end) = match self.encoding {
245 SourcePositionEncoding::UTF8 => (
246 LineCol {
247 line: self.range.start.line,
248 col: self.range.start.character,
249 },
250 LineCol {
251 line: self.range.end.line,
252 col: self.range.end.character,
253 },
254 ),
255 SourcePositionEncoding::UTF16 => (
256 lines
257 .to_utf8(
258 WideEncoding::Utf16,
259 WideLineCol {
260 line: self.range.start.line,
261 col: self.range.start.character,
262 },
263 )
264 .context("invalid edit start position")?,
265 lines
266 .to_utf8(
267 WideEncoding::Utf16,
268 WideLineCol {
269 line: self.range.end.line,
270 col: self.range.end.character,
271 },
272 )
273 .context("invalid edit end position")?,
274 ),
275 };
276
277 let range: Range<usize> = lines
278 .offset(start)
279 .context("invalid edit start position")?
280 .into()
281 ..lines
282 .offset(end)
283 .context("invalid edit end position")?
284 .into();
285
286 if !source.is_char_boundary(range.start) {
287 bail!("edit start position is not at a character boundary");
288 }
289
290 if !source.is_char_boundary(range.end) {
291 bail!("edit end position is not at a character boundary");
292 }
293
294 source.replace_range(range, &self.text);
295 Ok(())
296 }
297}
298
299#[derive(Clone, Debug)]
301pub struct IncrementalChange {
302 pub version: i32,
306 pub start: Option<String>,
314 pub edits: Vec<SourceEdit>,
316}
317
318pub struct Analyzer<Context> {
332 sender: ManuallyDrop<mpsc::UnboundedSender<Request<Context>>>,
334 handle: Option<JoinHandle<()>>,
336 config: Config,
338 resolution: ResolutionContext,
340}
341
342impl<Context> fmt::Debug for Analyzer<Context> {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 f.debug_struct("Analyzer")
345 .field("config", &self.config)
346 .field("resolution", &self.resolution)
347 .finish_non_exhaustive()
348 }
349}
350
351#[derive(Clone, Debug, Default)]
360pub enum ResolutionContext {
361 #[default]
363 Disabled,
364 Enabled {
366 resolver: Arc<dyn wdl_modules::Resolver>,
368 consumer_module: wdl_modules::module::Module,
376 },
377}
378
379impl ResolutionContext {
380 pub fn enabled(
383 resolver: Arc<dyn wdl_modules::Resolver>,
384 consumer_module: wdl_modules::module::Module,
385 ) -> Self {
386 Self::Enabled {
387 resolver,
388 consumer_module,
389 }
390 }
391
392 pub fn module_root(&self) -> Option<&Path> {
395 match self {
396 Self::Disabled => None,
397 Self::Enabled {
398 consumer_module, ..
399 } => Some(consumer_module.root.as_path()),
400 }
401 }
402
403 pub(crate) fn into_parts(
410 self,
411 ) -> (
412 Option<Arc<dyn wdl_modules::Resolver>>,
413 Option<wdl_modules::module::Module>,
414 ) {
415 match self {
416 Self::Disabled => (None, None),
417 Self::Enabled {
418 resolver,
419 consumer_module,
420 } => (Some(resolver), Some(consumer_module)),
421 }
422 }
423}
424
425impl<Context> Analyzer<Context>
426where
427 Context: Send + Clone + 'static,
428{
429 pub fn new<Progress, Return>(config: Config, progress: Progress) -> Self
437 where
438 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
439 Return: Future<Output = ()>,
440 {
441 Self::new_with_resolution(config, ResolutionContext::default(), progress)
442 }
443
444 pub fn new_with_resolution<Progress, Return>(
452 config: Config,
453 resolution: ResolutionContext,
454 progress: Progress,
455 ) -> Self
456 where
457 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
458 Return: Future<Output = ()>,
459 {
460 Self::new_with_validator_and_resolution(
461 config,
462 resolution,
463 progress,
464 crate::Validator::default,
465 )
466 }
467
468 pub fn new_with_validator<Progress, Return, Validator>(
477 config: Config,
478 progress: Progress,
479 validator: Validator,
480 ) -> Self
481 where
482 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
483 Return: Future<Output = ()>,
484 Validator: Fn() -> crate::Validator + Send + Sync + 'static,
485 {
486 Self::new_with_validator_and_resolution(
487 config,
488 ResolutionContext::default(),
489 progress,
490 validator,
491 )
492 }
493
494 pub fn new_with_validator_and_resolution<Progress, Return, Validator>(
504 config: Config,
505 resolution: ResolutionContext,
506 progress: Progress,
507 validator: Validator,
508 ) -> Self
509 where
510 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
511 Return: Future<Output = ()>,
512 Validator: Fn() -> crate::Validator + Send + Sync + 'static,
513 {
514 let (tx, rx) = mpsc::unbounded_channel();
515 let tokio = Handle::current();
516 let inner_config = config.clone();
517 let inner_resolution = resolution.clone();
518 let handle = std::thread::spawn(move || {
519 let queue = AnalysisQueue::new(
520 inner_config,
521 tokio,
522 inner_resolution,
523 progress,
524 Arc::new(validator),
525 );
526 queue.run(rx);
527 });
528
529 Self {
530 sender: ManuallyDrop::new(tx),
531 handle: Some(handle),
532 config,
533 resolution,
534 }
535 }
536
537 pub async fn swap_validator<Validator>(&self, validator: Validator) -> Result<()>
541 where
542 Validator: Fn() -> crate::Validator + Send + Sync + 'static,
543 {
544 let (tx, rx) = oneshot::channel();
545 self.sender
546 .send(Request::SwapValidator(SwapValidatorRequest {
547 validator: Arc::new(validator),
548 completed: tx,
549 }))
550 .map_err(|_| {
551 anyhow!("failed to send request to analysis queue because the channel has closed")
552 })?;
553
554 rx.await.map_err(|_| {
555 anyhow!("failed to receive response from analysis queue because the channel has closed")
556 })?;
557
558 Ok(())
559 }
560
561 pub async fn add_document(&self, uri: Url) -> Result<()> {
565 let mut documents = IndexSet::new();
566 documents.insert(uri);
567
568 let (tx, rx) = oneshot::channel();
569 self.sender
570 .send(Request::Add(AddRequest {
571 documents,
572 completed: tx,
573 }))
574 .map_err(|_| {
575 anyhow!("failed to send request to analysis queue because the channel has closed")
576 })?;
577
578 rx.await.map_err(|_| {
579 anyhow!("failed to receive response from analysis queue because the channel has closed")
580 })?;
581
582 Ok(())
583 }
584
585 pub async fn add_directory(&self, path: impl Into<PathBuf>) -> Result<()> {
591 let path = path.into();
592 let config = self.config.clone();
593 let stop_at_module_boundaries = self
600 .resolution
601 .module_root()
602 .is_some_and(|root| path.starts_with(root));
603 let documents = RayonHandle::spawn(move || -> Result<IndexSet<Url>> {
605 let mut documents = IndexSet::new();
606
607 let metadata = path.metadata().with_context(|| {
608 format!(
609 "failed to read metadata for `{path}`",
610 path = path.display()
611 )
612 })?;
613
614 if metadata.is_file() {
615 bail!("`{path}` is a file, not a directory", path = path.display());
616 }
617
618 let mut walker = WalkBuilder::new(&path);
619 if let Some(ignore_filename) = config.ignore_filename() {
620 walker.add_custom_ignore_filename(ignore_filename);
621 }
622 if stop_at_module_boundaries {
623 let root_for_filter = path.clone();
629 walker.filter_entry(move |entry| {
630 if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
631 return true;
632 }
633 if entry.path() == root_for_filter {
634 return true;
635 }
636 !wdl_modules::module::is_module_root(entry.path())
637 });
638 }
639 let walker = walker
640 .standard_filters(false)
641 .parents(true)
642 .follow_links(true)
643 .build();
644
645 for result in walker {
646 let entry = result.with_context(|| {
647 format!("failed to read directory `{path}`", path = path.display())
648 })?;
649
650 let Some(file_type) = entry.file_type() else {
652 continue;
653 };
654 if !file_type.is_file() {
656 continue;
657 }
658 if entry.path().extension() != Some(OsStr::new("wdl")) {
660 continue;
661 }
662
663 documents.insert(path_to_uri(entry.path()).with_context(|| {
664 format!(
665 "failed to convert path `{path}` to a URI",
666 path = entry.path().display()
667 )
668 })?);
669 }
670
671 Ok(documents)
672 })
673 .await?;
674
675 if documents.is_empty() {
676 return Ok(());
677 }
678
679 let (tx, rx) = oneshot::channel();
681 self.sender
682 .send(Request::Add(AddRequest {
683 documents,
684 completed: tx,
685 }))
686 .map_err(|_| {
687 anyhow!("failed to send request to analysis queue because the channel has closed")
688 })?;
689
690 rx.await.map_err(|_| {
691 anyhow!("failed to receive response from analysis queue because the channel has closed")
692 })?;
693
694 Ok(())
695 }
696
697 pub async fn unroot_documents(&self, documents: Vec<Url>) -> Result<()> {
706 let (tx, rx) = oneshot::channel();
708 self.sender
709 .send(Request::UnrootDocuments(UnrootDocumentsRequest {
710 documents,
711 completed: tx,
712 }))
713 .map_err(|_| {
714 anyhow!("failed to send request to analysis queue because the channel has closed")
715 })?;
716
717 rx.await.map_err(|_| {
718 anyhow!("failed to receive response from analysis queue because the channel has closed")
719 })?;
720
721 Ok(())
722 }
723
724 pub async fn delete_documents(&self, documents: Vec<Url>) -> Result<()> {
729 let (tx, rx) = oneshot::channel();
731 self.sender
732 .send(Request::Delete(DeleteRequest {
733 documents,
734 completed: tx,
735 }))
736 .map_err(|_| {
737 anyhow!("failed to send request to analysis queue because the channel has closed")
738 })?;
739
740 rx.await.map_err(|_| {
741 anyhow!("failed to receive response from analysis queue because the channel has closed")
742 })?;
743
744 Ok(())
745 }
746
747 pub fn notify_incremental_change(
751 &self,
752 document: Url,
753 change: IncrementalChange,
754 ) -> Result<()> {
755 self.sender
756 .send(Request::NotifyIncrementalChange(
757 NotifyIncrementalChangeRequest { document, change },
758 ))
759 .map_err(|_| {
760 anyhow!("failed to send request to analysis queue because the channel has closed")
761 })
762 }
763
764 pub fn notify_change(&self, document: Url, discard_pending: bool) -> Result<()> {
773 self.sender
774 .send(Request::NotifyChange(NotifyChangeRequest {
775 document,
776 discard_pending,
777 }))
778 .map_err(|_| {
779 anyhow!("failed to send request to analysis queue because the channel has closed")
780 })
781 }
782
783 pub async fn analyze_document(
792 &self,
793 context: Context,
794 document: Url,
795 ) -> Result<Vec<AnalysisResult>> {
796 let (tx, rx) = oneshot::channel();
798 self.sender
799 .send(Request::Analyze(AnalyzeRequest {
800 document: Some(document),
801 context,
802 completed: tx,
803 }))
804 .map_err(|_| {
805 anyhow!("failed to send request to analysis queue because the channel has closed")
806 })?;
807
808 rx.await.map_err(|_| {
809 anyhow!("failed to receive response from analysis queue because the channel has closed")
810 })?
811 }
812
813 pub async fn analyze(&self, context: Context) -> Result<Vec<AnalysisResult>> {
822 let (tx, rx) = oneshot::channel();
824 self.sender
825 .send(Request::Analyze(AnalyzeRequest {
826 document: None, context,
828 completed: tx,
829 }))
830 .map_err(|_| {
831 anyhow!("failed to send request to analysis queue because the channel has closed")
832 })?;
833
834 rx.await.map_err(|_| {
835 anyhow!("failed to receive response from analysis queue because the channel has closed")
836 })?
837 }
838
839 pub async fn call_hierarchy(
841 &self,
842 document: Url,
843 position: SourcePosition,
844 encoding: SourcePositionEncoding,
845 ) -> Result<Option<Vec<CallHierarchyItem>>> {
846 let (tx, rx) = oneshot::channel();
847 self.sender
848 .send(Request::CallHierarchy(CallHierarchyRequest {
849 document,
850 position,
851 encoding,
852 completed: tx,
853 }))
854 .map_err(|_| {
855 anyhow!(
856 "failed to send call hierarchy request to analysis queue because the channel \
857 has closed"
858 )
859 })?;
860
861 rx.await.map_err(|_| {
862 anyhow!(
863 "failed to receive call hierarchy response from analysis queue because the \
864 channel has closed"
865 )
866 })
867 }
868
869 pub async fn format_document(&self, document: Url) -> Result<Option<(u32, u32, String)>> {
871 let (tx, rx) = oneshot::channel();
872 self.sender
873 .send(Request::Format(FormatRequest {
874 document,
875 completed: tx,
876 }))
877 .map_err(|_| {
878 anyhow!("failed to send format request to the queue because the channel has closed")
879 })?;
880
881 rx.await.map_err(|_| {
882 anyhow!("failed to send format request to the queue because the channel has closed")
883 })
884 }
885
886 pub async fn folding_range(&self, document: Url) -> Result<Option<Vec<FoldingRange>>> {
888 let (tx, rx) = oneshot::channel();
889 self.sender
890 .send(Request::FoldingRange(FoldingRangeRequest {
891 document,
892 completed: tx,
893 }))
894 .map_err(|_| {
895 anyhow!(
896 "failed to send folding range request to the queue because the channel has \
897 closed"
898 )
899 })?;
900
901 rx.await.map_err(|_| {
902 anyhow!(
903 "failed to receive folding range response from analysis queue because the channel \
904 has closed"
905 )
906 })
907 }
908
909 pub async fn goto_definition(
911 &self,
912 document: Url,
913 position: SourcePosition,
914 encoding: SourcePositionEncoding,
915 ) -> Result<Option<GotoDefinitionResponse>> {
916 let (tx, rx) = oneshot::channel();
917 self.sender
918 .send(Request::GotoDefinition(GotoDefinitionRequest {
919 document,
920 position,
921 encoding,
922 completed: tx,
923 }))
924 .map_err(|_| {
925 anyhow!(
926 "failed to send goto definition request to analysis queue because the channel \
927 has closed"
928 )
929 })?;
930
931 rx.await.map_err(|_| {
932 anyhow!(
933 "failed to receive goto definition response from analysis queue because the \
934 channel has closed"
935 )
936 })
937 }
938
939 pub async fn find_all_references(
941 &self,
942 document: Url,
943 position: SourcePosition,
944 encoding: SourcePositionEncoding,
945 include_declaration: bool,
946 ) -> Result<Vec<Location>> {
947 let (tx, rx) = oneshot::channel();
948 self.sender
949 .send(Request::FindAllReferences(FindAllReferencesRequest {
950 document,
951 position,
952 encoding,
953 include_declaration,
954 completed: tx,
955 }))
956 .map_err(|_| {
957 anyhow!(
958 "failed to send find all references request to analysis queue because the \
959 channel has closed"
960 )
961 })?;
962
963 rx.await.map_err(|_| {
964 anyhow!(
965 "failed to receive find all references response from analysis queue because the \
966 client channel has closed"
967 )
968 })
969 }
970
971 pub async fn code_lens(&self, document: Url) -> Result<Option<Vec<CodeLens>>> {
973 let (tx, rx) = oneshot::channel();
974 self.sender
975 .send(Request::CodeLens(CodeLensRequest {
976 document,
977 completed: tx,
978 }))
979 .map_err(|_| {
980 anyhow!(
981 "failed to send codelens request to analysis queue because the channel has \
982 closed"
983 )
984 })?;
985
986 rx.await.map_err(|_| {
987 anyhow!(
988 "failed to send codelens request to analysis queue because the channel has closed"
989 )
990 })
991 }
992
993 pub async fn completion(
995 &self,
996 context: Context,
997 document: Url,
998 position: SourcePosition,
999 encoding: SourcePositionEncoding,
1000 ) -> Result<Option<CompletionResponse>> {
1001 let (tx, rx) = oneshot::channel();
1002 self.sender
1003 .send(Request::Completion(CompletionRequest {
1004 document,
1005 position,
1006 encoding,
1007 context,
1008 completed: tx,
1009 }))
1010 .map_err(|_| {
1011 anyhow!(
1012 "failed to send completion request to analysis queue because the channel has \
1013 closed"
1014 )
1015 })?;
1016
1017 rx.await.map_err(|_| {
1018 anyhow!(
1019 "failed to send completion request to analysis queue because the channel has \
1020 closed"
1021 )
1022 })
1023 }
1024
1025 pub async fn hover(
1027 &self,
1028 document: Url,
1029 position: SourcePosition,
1030 encoding: SourcePositionEncoding,
1031 ) -> Result<Option<Hover>> {
1032 let (tx, rx) = oneshot::channel();
1033 self.sender
1034 .send(Request::Hover(HoverRequest {
1035 document,
1036 position,
1037 encoding,
1038 completed: tx,
1039 }))
1040 .map_err(|_| {
1041 anyhow!(
1042 "failed to send hover request to analysis queue because the channel has closed"
1043 )
1044 })?;
1045
1046 rx.await.map_err(|_| {
1047 anyhow!("failed to send hover request to analysis queue because the channel has closed")
1048 })
1049 }
1050
1051 pub async fn rename(
1053 &self,
1054 document: Url,
1055 position: SourcePosition,
1056 encoding: SourcePositionEncoding,
1057 new_name: String,
1058 ) -> Result<Option<WorkspaceEdit>> {
1059 let (tx, rx) = oneshot::channel();
1060 self.sender
1061 .send(Request::Rename(RenameRequest {
1062 document,
1063 position,
1064 encoding,
1065 new_name,
1066 completed: tx,
1067 }))
1068 .map_err(|_| {
1069 anyhow!(
1070 "failed to send rename request to analysis queue because the channel has \
1071 closed"
1072 )
1073 })?;
1074
1075 rx.await.map_err(|_| {
1076 anyhow!(
1077 "failed to receive rename response from analysis queue because the channel has \
1078 closed"
1079 )
1080 })
1081 }
1082
1083 pub async fn semantic_tokens(&self, document: Url) -> Result<Option<SemanticTokensResult>> {
1085 let (tx, rx) = oneshot::channel();
1086 self.sender
1087 .send(Request::SemanticTokens(SemanticTokenRequest {
1088 document,
1089 completed: tx,
1090 }))
1091 .map_err(|_| {
1092 anyhow!(
1093 "failed to send semantic tokens request to analysis queue because the channel \
1094 has closed"
1095 )
1096 })?;
1097
1098 rx.await.map_err(|_| {
1099 anyhow!(
1100 "failed to receive semantic tokens response from analysis queue because the \
1101 channel has closed"
1102 )
1103 })
1104 }
1105
1106 pub async fn document_symbol(&self, document: Url) -> Result<Option<DocumentSymbolResponse>> {
1108 let (tx, rx) = oneshot::channel();
1109 self.sender
1110 .send(Request::DocumentSymbol(DocumentSymbolRequest {
1111 document,
1112 completed: tx,
1113 }))
1114 .map_err(|_| {
1115 anyhow!(
1116 "failed to send document symbol request to analysis queue because the channel \
1117 has closed"
1118 )
1119 })?;
1120
1121 rx.await.map_err(|_| {
1122 anyhow!(
1123 "failed to receive document symbol request to analysis queue because the channel \
1124 has closed"
1125 )
1126 })
1127 }
1128
1129 pub async fn workspace_symbol(&self, query: String) -> Result<Option<Vec<SymbolInformation>>> {
1131 let (tx, rx) = oneshot::channel();
1132 self.sender
1133 .send(Request::WorkspaceSymbol(WorkspaceSymbolRequest {
1134 query,
1135 completed: tx,
1136 }))
1137 .map_err(|_| {
1138 anyhow!(
1139 "failed to send workspace symbol request to analysis queue because the \
1140 channel has closed"
1141 )
1142 })?;
1143
1144 rx.await.map_err(|_| {
1145 anyhow!(
1146 "failed to receive workspace symbol response from analysis queue because the \
1147 channel has closed"
1148 )
1149 })
1150 }
1151
1152 pub async fn incoming_calls(
1154 &self,
1155 document: Url,
1156 position: SourcePosition,
1157 encoding: SourcePositionEncoding,
1158 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
1159 let (tx, rx) = oneshot::channel();
1160 self.sender
1161 .send(Request::IncomingCalls(IncomingCallsRequest {
1162 document,
1163 position,
1164 encoding,
1165 completed: tx,
1166 }))
1167 .map_err(|_| {
1168 anyhow!(
1169 "failed to send incoming calls request to analysis queue because the channel \
1170 has closed"
1171 )
1172 })?;
1173
1174 rx.await.map_err(|_| {
1175 anyhow!(
1176 "failed to receive incoming calls response from analysis queue because the \
1177 channel has closed"
1178 )
1179 })
1180 }
1181
1182 pub async fn outgoing_calls(
1184 &self,
1185 document: Url,
1186 position: SourcePosition,
1187 encoding: SourcePositionEncoding,
1188 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
1189 let (tx, rx) = oneshot::channel();
1190 self.sender
1191 .send(Request::OutgoingCalls(OutgoingCallsRequest {
1192 document,
1193 position,
1194 encoding,
1195 completed: tx,
1196 }))
1197 .map_err(|_| {
1198 anyhow!(
1199 "failed to send outgoing calls request to analysis queue because the channel \
1200 has closed"
1201 )
1202 })?;
1203
1204 rx.await.map_err(|_| {
1205 anyhow!(
1206 "failed to receive outgoing calls response from analysis queue because the \
1207 channel has closed"
1208 )
1209 })
1210 }
1211
1212 pub async fn signature_help(
1214 &self,
1215 document: Url,
1216 position: SourcePosition,
1217 encoding: SourcePositionEncoding,
1218 ) -> Result<Option<SignatureHelp>> {
1219 let (tx, rx) = oneshot::channel();
1220 self.sender
1221 .send(Request::SignatureHelp(SignatureHelpRequest {
1222 document,
1223 position,
1224 encoding,
1225 completed: tx,
1226 }))
1227 .map_err(|_| {
1228 anyhow!(
1229 "failed to send signature help request to analysis queue because the channel \
1230 has closed"
1231 )
1232 })?;
1233
1234 rx.await.map_err(|_| {
1235 anyhow!(
1236 "failed to receive signature help response from analysis queue because the \
1237 channel has closed"
1238 )
1239 })
1240 }
1241
1242 pub async fn inlay_hints(
1244 &self,
1245 document: Url,
1246 range: lsp_types::Range,
1247 ) -> Result<Option<Vec<InlayHint>>> {
1248 let (tx, rx) = oneshot::channel();
1249 self.sender
1250 .send(Request::InlayHints(InlayHintsRequest {
1251 document,
1252 range,
1253 completed: tx,
1254 }))
1255 .map_err(|_| {
1256 anyhow!(
1257 "failed to send inlay hints request to analysis queue because the channel has \
1258 closed"
1259 )
1260 })?;
1261
1262 rx.await.map_err(|_| {
1263 anyhow!(
1264 "failed to receive inlay hints response from analysis queue because the channel \
1265 has closed"
1266 )
1267 })
1268 }
1269}
1270
1271impl Default for Analyzer<()> {
1272 fn default() -> Self {
1273 Self::new(Default::default(), |_, _, _, _| async {})
1274 }
1275}
1276
1277impl<C> Drop for Analyzer<C> {
1278 fn drop(&mut self) {
1279 unsafe { ManuallyDrop::drop(&mut self.sender) };
1280 if let Some(handle) = self.handle.take() {
1281 handle.join().unwrap();
1282 }
1283 }
1284}
1285
1286const _: () = {
1289 const fn _assert<T: Send + Sync>() {}
1291 _assert::<Analyzer<()>>();
1292};
1293
1294#[cfg(test)]
1295mod test {
1296 use std::fs;
1297 use std::path::PathBuf;
1298
1299 use tempfile::TempDir;
1300 use wdl_ast::Severity;
1301
1302 use super::*;
1303
1304 #[tokio::test]
1305 #[test_log::test]
1306 async fn it_returns_empty_results() {
1307 let analyzer = Analyzer::default();
1308 let results = analyzer.analyze(()).await.unwrap();
1309 assert!(results.is_empty());
1310 }
1311
1312 #[tokio::test]
1313 #[test_log::test]
1314 async fn it_analyzes_a_document() {
1315 let dir = TempDir::new().expect("failed to create temporary directory");
1316 let path = dir.path().join("foo.wdl");
1317 fs::write(
1318 &path,
1319 r#"version 1.1
1320
1321task test {
1322 command <<<>>>
1323}
1324
1325workflow test {
1326}
1327"#,
1328 )
1329 .expect("failed to create test file");
1330
1331 let analyzer = Analyzer::default();
1333 analyzer
1334 .add_document(path_to_uri(&path).expect("should convert to URI"))
1335 .await
1336 .expect("should add document");
1337
1338 let results = analyzer.analyze(()).await.unwrap();
1339 assert_eq!(results.len(), 1);
1340 assert_eq!(results[0].document.diagnostics().count(), 1);
1341 assert_eq!(
1342 results[0].document.diagnostics().next().unwrap().rule(),
1343 None
1344 );
1345 assert_eq!(
1346 results[0].document.diagnostics().next().unwrap().severity(),
1347 Severity::Error
1348 );
1349 assert_eq!(
1350 results[0].document.diagnostics().next().unwrap().message(),
1351 "conflicting workflow name `test`"
1352 );
1353
1354 let id = results[0].document.id().clone();
1356 let results = analyzer.analyze(()).await.unwrap();
1357 assert_eq!(results.len(), 1);
1358 assert_eq!(results[0].document.id().as_ref(), id.as_ref());
1359 assert_eq!(results[0].document.diagnostics().count(), 1);
1360 assert_eq!(
1361 results[0].document.diagnostics().next().unwrap().rule(),
1362 None
1363 );
1364 assert_eq!(
1365 results[0].document.diagnostics().next().unwrap().severity(),
1366 Severity::Error
1367 );
1368 assert_eq!(
1369 results[0].document.diagnostics().next().unwrap().message(),
1370 "conflicting workflow name `test`"
1371 );
1372 }
1373
1374 #[tokio::test]
1375 #[test_log::test]
1376 async fn it_reanalyzes_a_document_on_change() {
1377 let dir = TempDir::new().expect("failed to create temporary directory");
1378 let path = dir.path().join("foo.wdl");
1379 fs::write(
1380 &path,
1381 r#"version 1.1
1382
1383task test {
1384 command <<<>>>
1385}
1386
1387workflow test {
1388}
1389"#,
1390 )
1391 .expect("failed to create test file");
1392
1393 let analyzer = Analyzer::default();
1395 analyzer
1396 .add_document(path_to_uri(&path).expect("should convert to URI"))
1397 .await
1398 .expect("should add document");
1399
1400 let results = analyzer.analyze(()).await.unwrap();
1401 assert_eq!(results.len(), 1);
1402 assert_eq!(results[0].document.diagnostics().count(), 1);
1403 assert_eq!(
1404 results[0].document.diagnostics().next().unwrap().rule(),
1405 None
1406 );
1407 assert_eq!(
1408 results[0].document.diagnostics().next().unwrap().severity(),
1409 Severity::Error
1410 );
1411 assert_eq!(
1412 results[0].document.diagnostics().next().unwrap().message(),
1413 "conflicting workflow name `test`"
1414 );
1415
1416 fs::write(
1418 &path,
1419 r#"version 1.1
1420
1421task test {
1422 command <<<>>>
1423}
1424
1425workflow something_else {
1426}
1427"#,
1428 )
1429 .expect("failed to create test file");
1430
1431 let uri = path_to_uri(&path).expect("should convert to URI");
1432 analyzer.notify_change(uri.clone(), false).unwrap();
1433
1434 let id = results[0].document.id().clone();
1437 let results = analyzer.analyze(()).await.unwrap();
1438 assert_eq!(results.len(), 1);
1439 assert_ne!(results[0].document.id().as_ref(), id.as_ref());
1440 assert_eq!(results[0].document.diagnostics().count(), 0);
1441
1442 let id = results[0].document.id().clone();
1444 let results = analyzer.analyze_document((), uri).await.unwrap();
1445 assert_eq!(results.len(), 1);
1446 assert_eq!(results[0].document.id().as_ref(), id.as_ref());
1447 assert_eq!(results[0].document.diagnostics().count(), 0);
1448 }
1449
1450 #[tokio::test]
1451 #[test_log::test]
1452 async fn it_reanalyzes_a_document_on_incremental_change() {
1453 let dir = TempDir::new().expect("failed to create temporary directory");
1454 let path = dir.path().join("foo.wdl");
1455 fs::write(
1456 &path,
1457 r#"version 1.1
1458
1459task test {
1460 command <<<>>>
1461}
1462
1463workflow test {
1464}
1465"#,
1466 )
1467 .expect("failed to create test file");
1468
1469 let analyzer = Analyzer::default();
1471 analyzer
1472 .add_document(path_to_uri(&path).expect("should convert to URI"))
1473 .await
1474 .expect("should add document");
1475
1476 let results = analyzer.analyze(()).await.unwrap();
1477 assert_eq!(results.len(), 1);
1478 assert_eq!(results[0].document.diagnostics().count(), 1);
1479 assert_eq!(
1480 results[0].document.diagnostics().next().unwrap().rule(),
1481 None
1482 );
1483 assert_eq!(
1484 results[0].document.diagnostics().next().unwrap().severity(),
1485 Severity::Error
1486 );
1487 assert_eq!(
1488 results[0].document.diagnostics().next().unwrap().message(),
1489 "conflicting workflow name `test`"
1490 );
1491
1492 let uri = path_to_uri(&path).expect("should convert to URI");
1494 analyzer
1495 .notify_incremental_change(
1496 uri.clone(),
1497 IncrementalChange {
1498 version: 2,
1499 start: None,
1500 edits: vec![SourceEdit {
1501 range: SourcePosition::new(6, 9)..SourcePosition::new(6, 13),
1502 encoding: SourcePositionEncoding::UTF8,
1503 text: "something_else".to_string(),
1504 }],
1505 },
1506 )
1507 .unwrap();
1508
1509 let id = results[0].document.id().clone();
1512 let results = analyzer.analyze_document((), uri).await.unwrap();
1513 assert_eq!(results.len(), 1);
1514 assert_ne!(results[0].document.id().as_ref(), id.as_ref());
1515 assert_eq!(results[0].document.diagnostics().count(), 0);
1516 }
1517
1518 #[tokio::test]
1519 #[test_log::test]
1520 async fn it_removes_documents() {
1521 let dir = TempDir::new().expect("failed to create temporary directory");
1522 let foo = dir.path().join("foo.wdl");
1523 fs::write(
1524 &foo,
1525 r#"version 1.1
1526workflow test {
1527}
1528"#,
1529 )
1530 .expect("failed to create test file");
1531
1532 let bar = dir.path().join("bar.wdl");
1533 fs::write(
1534 &bar,
1535 r#"version 1.1
1536workflow test {
1537}
1538"#,
1539 )
1540 .expect("failed to create test file");
1541
1542 let baz = dir.path().join("baz.wdl");
1543 fs::write(
1544 &baz,
1545 r#"version 1.1
1546workflow test {
1547}
1548"#,
1549 )
1550 .expect("failed to create test file");
1551
1552 let analyzer = Analyzer::default();
1554 analyzer
1555 .add_directory(dir.path())
1556 .await
1557 .expect("should add documents");
1558
1559 let results = analyzer.analyze(()).await.unwrap();
1561 assert_eq!(results.len(), 3);
1562 assert!(results[0].document.diagnostics().next().is_none());
1563 assert!(results[1].document.diagnostics().next().is_none());
1564 assert!(results[2].document.diagnostics().next().is_none());
1565
1566 let results = analyzer.analyze(()).await.unwrap();
1568 assert_eq!(results.len(), 3);
1569
1570 analyzer
1572 .unroot_documents(vec![
1573 path_to_uri(dir.path()).expect("should convert to URI"),
1574 ])
1575 .await
1576 .unwrap();
1577 let results = analyzer.analyze(()).await.unwrap();
1578 assert!(results.is_empty());
1579 }
1580
1581 #[tokio::test]
1582 #[test_log::test]
1583 async fn selected_imported_task_conflicts_with_local_workflow() {
1584 let dir = TempDir::new().expect("failed to create temporary directory");
1585 fs::write(
1586 dir.path().join("lib.wdl"),
1587 r#"version 1.4
1588task run {
1589 command <<<>>>
1590}
1591"#,
1592 )
1593 .expect("failed to create library document");
1594 fs::write(
1595 dir.path().join("source.wdl"),
1596 r#"version 1.4
1597import { run } from "lib.wdl"
1598workflow run {
1599}
1600"#,
1601 )
1602 .expect("failed to create source document");
1603
1604 let config = Config::default()
1605 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1606 let analyzer = Analyzer::new(config, |(), _, _, _| async {});
1607 analyzer
1608 .add_document(path_to_uri(dir.path().join("source.wdl")).expect("should convert"))
1609 .await
1610 .expect("should add document");
1611
1612 let results = analyzer.analyze(()).await.expect("analysis should succeed");
1613 let source = results
1614 .iter()
1615 .find(|result| result.document.uri().path().contains("source.wdl"))
1616 .expect("should find source result");
1617 let errors = source
1618 .document
1619 .diagnostics()
1620 .filter(|diagnostic| diagnostic.severity() == Severity::Error)
1621 .map(|diagnostic| diagnostic.message())
1622 .collect::<Vec<_>>();
1623 assert_eq!(
1624 errors,
1625 ["import of `run` conflicts with an existing definition"]
1626 );
1627 }
1628
1629 #[tokio::test]
1630 #[test_log::test]
1631 async fn symbolic_import_resolves_through_mock_resolver() {
1632 use wdl_modules::Manifest;
1633 use wdl_modules::lockfile::ResolvedSource;
1634 use wdl_modules::resolver::MaterializedFile;
1635 use wdl_modules::resolver::ResolvedTree;
1636 use wdl_modules::resolver::ResolverError;
1637
1638 #[derive(Debug)]
1639 struct MockResolver {
1640 dep_path: PathBuf,
1641 }
1642
1643 #[async_trait::async_trait]
1644 impl wdl_modules::Resolver for MockResolver {
1645 async fn materialize(
1646 &self,
1647 _consumer: &wdl_modules::module::Module,
1648 path: &wdl_modules::symbolic_path::SymbolicPath,
1649 ) -> Result<MaterializedFile, ResolverError> {
1650 let rel = match path.sub_path() {
1651 Some(sub) => {
1652 let mut p = sub.to_path_buf();
1653 p.set_extension("wdl");
1654 p
1655 }
1656 None => std::path::PathBuf::from("index.wdl"),
1657 };
1658 let file_path = self.dep_path.join(rel);
1659 let manifest_bytes = fs::read(self.dep_path.join("module.json")).unwrap();
1660 let manifest = Manifest::parse(&manifest_bytes).unwrap();
1661 Ok(MaterializedFile {
1662 path: file_path,
1663 module_root: self.dep_path.clone(),
1664 source: ResolvedSource::Path {
1665 path: self.dep_path.clone(),
1666 },
1667 manifest: Arc::new(manifest),
1668 })
1669 }
1670
1671 async fn resolve_tree(
1672 &self,
1673 _consumer: &wdl_modules::module::Module,
1674 ) -> Result<ResolvedTree, ResolverError> {
1675 Ok(ResolvedTree::default())
1676 }
1677
1678 async fn discover_versions(
1679 &self,
1680 _name: &wdl_modules::dependency::DependencyName,
1681 _source: &wdl_modules::dependency::DependencySource,
1682 _scope: wdl_modules::resolver::DependencyScope,
1683 ) -> Result<Vec<semver::Version>, ResolverError> {
1684 Ok(Vec::new())
1685 }
1686 }
1687
1688 let dir = TempDir::new().expect("failed to create temporary directory");
1689
1690 let dep_dir = dir.path().join("dep");
1691 fs::create_dir_all(&dep_dir).unwrap();
1692 fs::write(
1693 dep_dir.join("module.json"),
1694 r#"{"name":"dep","version":"1.0.0","license":"MIT"}"#,
1695 )
1696 .unwrap();
1697 fs::write(
1698 dep_dir.join("index.wdl"),
1699 "version 1.4\n\ntask hello {\n command <<<>>>\n}\n",
1700 )
1701 .unwrap();
1702
1703 let consumer_dir = dir.path().join("consumer");
1704 fs::create_dir_all(&consumer_dir).unwrap();
1705 let dep_path_json = dep_dir.display().to_string().replace('\\', "/");
1706 fs::write(
1707 consumer_dir.join("module.json"),
1708 format!(
1709 r#"{{"name":"consumer","version":"0.1.0","license":"MIT","dependencies":{{"dep":{{"path":"{dep_path_json}"}}}}}}"#
1710 ),
1711 )
1712 .unwrap();
1713 fs::write(
1714 consumer_dir.join("source.wdl"),
1715 "version 1.4\n\nimport dep\nimport \"lib.wdl\"\n\nworkflow main {}\n",
1716 )
1717 .unwrap();
1718 fs::write(
1719 consumer_dir.join("lib.wdl"),
1720 "version 1.4\n\nimport dep\n\ntask lib {\n command <<<>>>\n}\n",
1721 )
1722 .unwrap();
1723
1724 let config = Config::default()
1725 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1726 let resolver: Arc<dyn wdl_modules::Resolver> = Arc::new(MockResolver {
1727 dep_path: dep_dir.clone(),
1728 });
1729 let consumer_module = wdl_modules::module::Module::load_from_path(&consumer_dir)
1730 .expect("test consumer module should load");
1731 let resolution = ResolutionContext::enabled(resolver, consumer_module);
1732 let analyzer = Analyzer::new_with_resolution(config, resolution, |(), _, _, _| async {});
1733 analyzer
1734 .add_document(path_to_uri(consumer_dir.join("source.wdl")).expect("should convert"))
1735 .await
1736 .expect("should add document");
1737
1738 let results = analyzer.analyze(()).await.unwrap();
1739 assert!(!results.is_empty(), "should have analysis results");
1740 let consumer_result = results
1741 .iter()
1742 .find(|r| r.document.uri().path().contains("source.wdl"))
1743 .expect("should find consumer result");
1744 let errors: Vec<_> = consumer_result
1745 .document
1746 .diagnostics()
1747 .filter(|d| d.severity() == Severity::Error)
1748 .collect();
1749 assert!(
1750 errors.is_empty(),
1751 "consumer should have no errors, got: {:?}",
1752 errors.iter().map(|d| d.message()).collect::<Vec<_>>()
1753 );
1754 let lib_result = results
1755 .iter()
1756 .find(|r| r.document.uri().path().contains("lib.wdl"))
1757 .expect("should find uri import result");
1758 let errors: Vec<_> = lib_result
1759 .document
1760 .diagnostics()
1761 .filter(|d| d.severity() == Severity::Error)
1762 .collect();
1763 assert!(
1764 errors.is_empty(),
1765 "uri import should have no errors, got: {:?}",
1766 errors.iter().map(|d| d.message()).collect::<Vec<_>>()
1767 );
1768 }
1769
1770 #[tokio::test]
1771 #[test_log::test]
1772 async fn concurrent_symbolic_imports_faster_than_serial() {
1773 use std::sync::atomic::AtomicUsize;
1774 use std::sync::atomic::Ordering;
1775 use std::time::Duration;
1776
1777 use wdl_modules::Manifest;
1778 use wdl_modules::lockfile::ResolvedSource;
1779 use wdl_modules::resolver::MaterializedFile;
1780 use wdl_modules::resolver::ResolvedTree;
1781 use wdl_modules::resolver::ResolverError;
1782
1783 #[derive(Debug)]
1785 struct SlowMockResolver {
1786 dep_path: PathBuf,
1787 delay: Duration,
1788 active: AtomicUsize,
1789 max_active: AtomicUsize,
1790 }
1791
1792 #[async_trait::async_trait]
1793 impl wdl_modules::Resolver for SlowMockResolver {
1794 async fn materialize(
1795 &self,
1796 _consumer: &wdl_modules::module::Module,
1797 path: &wdl_modules::symbolic_path::SymbolicPath,
1798 ) -> Result<MaterializedFile, ResolverError> {
1799 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
1800 self.max_active.fetch_max(active, Ordering::SeqCst);
1801 tokio::time::sleep(self.delay).await;
1802 self.active.fetch_sub(1, Ordering::SeqCst);
1803 let rel = match path.sub_path() {
1804 Some(sub) => {
1805 let mut p = sub.to_path_buf();
1806 p.set_extension("wdl");
1807 p
1808 }
1809 None => std::path::PathBuf::from("index.wdl"),
1810 };
1811 let file_path = self.dep_path.join(rel);
1812 let manifest_bytes = fs::read(self.dep_path.join("module.json")).unwrap();
1813 let manifest = Manifest::parse(&manifest_bytes).unwrap();
1814 Ok(MaterializedFile {
1815 path: file_path,
1816 module_root: self.dep_path.clone(),
1817 source: ResolvedSource::Path {
1818 path: self.dep_path.clone(),
1819 },
1820 manifest: Arc::new(manifest),
1821 })
1822 }
1823
1824 async fn resolve_tree(
1825 &self,
1826 _consumer: &wdl_modules::module::Module,
1827 ) -> Result<ResolvedTree, ResolverError> {
1828 Ok(ResolvedTree::default())
1829 }
1830
1831 async fn discover_versions(
1832 &self,
1833 _name: &wdl_modules::dependency::DependencyName,
1834 _source: &wdl_modules::dependency::DependencySource,
1835 _scope: wdl_modules::resolver::DependencyScope,
1836 ) -> Result<Vec<semver::Version>, ResolverError> {
1837 Ok(Vec::new())
1838 }
1839 }
1840
1841 const IMPORT_COUNT: usize = 8;
1842 const DELAY_MS: u64 = 200;
1843
1844 let dir = TempDir::new().expect("failed to create temporary directory");
1845
1846 let dep_dir = dir.path().join("slowdep");
1847 fs::create_dir_all(&dep_dir).unwrap();
1848 fs::write(
1849 dep_dir.join("module.json"),
1850 r#"{"name":"slowdep","version":"1.0.0","license":"MIT"}"#,
1851 )
1852 .unwrap();
1853 for i in 0..IMPORT_COUNT {
1854 fs::write(
1855 dep_dir.join(format!("sub{i}.wdl")),
1856 "version 1.4\n\ntask noop {\n command <<<>>>\n}\n",
1857 )
1858 .unwrap();
1859 }
1860 fs::write(
1861 dep_dir.join("index.wdl"),
1862 "version 1.4\n\ntask noop {\n command <<<>>>\n}\n",
1863 )
1864 .unwrap();
1865
1866 let consumer_dir = dir.path().join("slowconsumer");
1867 fs::create_dir_all(&consumer_dir).unwrap();
1868 let dep_path_json = dep_dir.display().to_string().replace('\\', "/");
1869 fs::write(
1870 consumer_dir.join("module.json"),
1871 format!(
1872 r#"{{"name":"slowconsumer","version":"0.1.0","license":"MIT","dependencies":{{"slowdep":{{"path":"{dep_path_json}"}}}}}}"#
1873 ),
1874 )
1875 .unwrap();
1876
1877 let mut source = "version 1.4\n\n".to_string();
1878 for i in 0..IMPORT_COUNT {
1879 source.push_str(&format!("import slowdep/sub{i}\n"));
1880 }
1881 source.push_str("\nworkflow main {}\n");
1882 fs::write(consumer_dir.join("source.wdl"), &source).unwrap();
1883
1884 let config = Config::default()
1885 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1886 let resolver = Arc::new(SlowMockResolver {
1887 dep_path: dep_dir.clone(),
1888 delay: Duration::from_millis(DELAY_MS),
1889 active: AtomicUsize::new(0),
1890 max_active: AtomicUsize::new(0),
1891 });
1892 let resolver_trait: Arc<dyn wdl_modules::Resolver> = resolver.clone();
1893 let consumer_module = wdl_modules::module::Module::load_from_path(&consumer_dir)
1894 .expect("test consumer module should load");
1895 let resolution = ResolutionContext::enabled(resolver_trait, consumer_module);
1896 let analyzer = Analyzer::new_with_resolution(config, resolution, |(), _, _, _| async {});
1897 analyzer
1898 .add_document(path_to_uri(consumer_dir.join("source.wdl")).expect("should convert"))
1899 .await
1900 .expect("should add document");
1901
1902 let results = analyzer.analyze(()).await.unwrap();
1903
1904 assert!(!results.is_empty(), "should have analysis results");
1905 assert!(
1906 resolver.max_active.load(Ordering::SeqCst) > 1,
1907 "symbolic imports should materialize concurrently"
1908 );
1909 }
1910
1911 #[tokio::test]
1912 #[test_log::test]
1913 async fn it_deletes_documents() {
1914 let dir = TempDir::new().expect("failed to create temporary directory");
1915 let foo = dir.path().join("foo.wdl");
1916 fs::write(
1917 &foo,
1918 r#"version 1.1
1919import "bar.wdl"
1920
1921workflow test {
1922 call bar.test
1923}
1924"#,
1925 )
1926 .expect("failed to create test file");
1927
1928 let bar = dir.path().join("bar.wdl");
1929 fs::write(
1930 &bar,
1931 r#"version 1.1
1932workflow test {}
1933"#,
1934 )
1935 .expect("failed to create test file");
1936
1937 let analyzer = Analyzer::default();
1939 analyzer
1940 .add_directory(dir.path())
1941 .await
1942 .expect("should add documents");
1943
1944 let results = analyzer.analyze(()).await.unwrap();
1946 assert_eq!(results.len(), 2);
1947 assert!(results[0].document.diagnostics().next().is_none());
1948 assert!(results[1].document.diagnostics().next().is_none());
1949
1950 fs::remove_file(&bar).expect("should delete file");
1955 analyzer
1956 .delete_documents(vec![path_to_uri(&bar).expect("should convert to URI")])
1957 .await
1958 .unwrap();
1959
1960 let results = analyzer.analyze(()).await.unwrap();
1962 assert_eq!(results.len(), 1);
1963
1964 let has_import_failed_diagnostic = results[0]
1965 .document
1966 .diagnostics()
1967 .any(|d| d.message().contains("failed to import `bar.wdl`"));
1968 assert!(has_import_failed_diagnostic);
1969 }
1970}