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 async fn it_returns_empty_results() {
1306 let analyzer = Analyzer::default();
1307 let results = analyzer.analyze(()).await.unwrap();
1308 assert!(results.is_empty());
1309 }
1310
1311 #[tokio::test]
1312 async fn it_analyzes_a_document() {
1313 let dir = TempDir::new().expect("failed to create temporary directory");
1314 let path = dir.path().join("foo.wdl");
1315 fs::write(
1316 &path,
1317 r#"version 1.1
1318
1319task test {
1320 command <<<>>>
1321}
1322
1323workflow test {
1324}
1325"#,
1326 )
1327 .expect("failed to create test file");
1328
1329 let analyzer = Analyzer::default();
1331 analyzer
1332 .add_document(path_to_uri(&path).expect("should convert to URI"))
1333 .await
1334 .expect("should add document");
1335
1336 let results = analyzer.analyze(()).await.unwrap();
1337 assert_eq!(results.len(), 1);
1338 assert_eq!(results[0].document.diagnostics().count(), 1);
1339 assert_eq!(
1340 results[0].document.diagnostics().next().unwrap().rule(),
1341 None
1342 );
1343 assert_eq!(
1344 results[0].document.diagnostics().next().unwrap().severity(),
1345 Severity::Error
1346 );
1347 assert_eq!(
1348 results[0].document.diagnostics().next().unwrap().message(),
1349 "conflicting workflow name `test`"
1350 );
1351
1352 let id = results[0].document.id().clone();
1354 let results = analyzer.analyze(()).await.unwrap();
1355 assert_eq!(results.len(), 1);
1356 assert_eq!(results[0].document.id().as_ref(), id.as_ref());
1357 assert_eq!(results[0].document.diagnostics().count(), 1);
1358 assert_eq!(
1359 results[0].document.diagnostics().next().unwrap().rule(),
1360 None
1361 );
1362 assert_eq!(
1363 results[0].document.diagnostics().next().unwrap().severity(),
1364 Severity::Error
1365 );
1366 assert_eq!(
1367 results[0].document.diagnostics().next().unwrap().message(),
1368 "conflicting workflow name `test`"
1369 );
1370 }
1371
1372 #[tokio::test]
1373 async fn it_reanalyzes_a_document_on_change() {
1374 let dir = TempDir::new().expect("failed to create temporary directory");
1375 let path = dir.path().join("foo.wdl");
1376 fs::write(
1377 &path,
1378 r#"version 1.1
1379
1380task test {
1381 command <<<>>>
1382}
1383
1384workflow test {
1385}
1386"#,
1387 )
1388 .expect("failed to create test file");
1389
1390 let analyzer = Analyzer::default();
1392 analyzer
1393 .add_document(path_to_uri(&path).expect("should convert to URI"))
1394 .await
1395 .expect("should add document");
1396
1397 let results = analyzer.analyze(()).await.unwrap();
1398 assert_eq!(results.len(), 1);
1399 assert_eq!(results[0].document.diagnostics().count(), 1);
1400 assert_eq!(
1401 results[0].document.diagnostics().next().unwrap().rule(),
1402 None
1403 );
1404 assert_eq!(
1405 results[0].document.diagnostics().next().unwrap().severity(),
1406 Severity::Error
1407 );
1408 assert_eq!(
1409 results[0].document.diagnostics().next().unwrap().message(),
1410 "conflicting workflow name `test`"
1411 );
1412
1413 fs::write(
1415 &path,
1416 r#"version 1.1
1417
1418task test {
1419 command <<<>>>
1420}
1421
1422workflow something_else {
1423}
1424"#,
1425 )
1426 .expect("failed to create test file");
1427
1428 let uri = path_to_uri(&path).expect("should convert to URI");
1429 analyzer.notify_change(uri.clone(), false).unwrap();
1430
1431 let id = results[0].document.id().clone();
1434 let results = analyzer.analyze(()).await.unwrap();
1435 assert_eq!(results.len(), 1);
1436 assert_ne!(results[0].document.id().as_ref(), id.as_ref());
1437 assert_eq!(results[0].document.diagnostics().count(), 0);
1438
1439 let id = results[0].document.id().clone();
1441 let results = analyzer.analyze_document((), uri).await.unwrap();
1442 assert_eq!(results.len(), 1);
1443 assert_eq!(results[0].document.id().as_ref(), id.as_ref());
1444 assert_eq!(results[0].document.diagnostics().count(), 0);
1445 }
1446
1447 #[tokio::test]
1448 async fn it_reanalyzes_a_document_on_incremental_change() {
1449 let dir = TempDir::new().expect("failed to create temporary directory");
1450 let path = dir.path().join("foo.wdl");
1451 fs::write(
1452 &path,
1453 r#"version 1.1
1454
1455task test {
1456 command <<<>>>
1457}
1458
1459workflow test {
1460}
1461"#,
1462 )
1463 .expect("failed to create test file");
1464
1465 let analyzer = Analyzer::default();
1467 analyzer
1468 .add_document(path_to_uri(&path).expect("should convert to URI"))
1469 .await
1470 .expect("should add document");
1471
1472 let results = analyzer.analyze(()).await.unwrap();
1473 assert_eq!(results.len(), 1);
1474 assert_eq!(results[0].document.diagnostics().count(), 1);
1475 assert_eq!(
1476 results[0].document.diagnostics().next().unwrap().rule(),
1477 None
1478 );
1479 assert_eq!(
1480 results[0].document.diagnostics().next().unwrap().severity(),
1481 Severity::Error
1482 );
1483 assert_eq!(
1484 results[0].document.diagnostics().next().unwrap().message(),
1485 "conflicting workflow name `test`"
1486 );
1487
1488 let uri = path_to_uri(&path).expect("should convert to URI");
1490 analyzer
1491 .notify_incremental_change(
1492 uri.clone(),
1493 IncrementalChange {
1494 version: 2,
1495 start: None,
1496 edits: vec![SourceEdit {
1497 range: SourcePosition::new(6, 9)..SourcePosition::new(6, 13),
1498 encoding: SourcePositionEncoding::UTF8,
1499 text: "something_else".to_string(),
1500 }],
1501 },
1502 )
1503 .unwrap();
1504
1505 let id = results[0].document.id().clone();
1508 let results = analyzer.analyze_document((), uri).await.unwrap();
1509 assert_eq!(results.len(), 1);
1510 assert_ne!(results[0].document.id().as_ref(), id.as_ref());
1511 assert_eq!(results[0].document.diagnostics().count(), 0);
1512 }
1513
1514 #[tokio::test]
1515 async fn it_removes_documents() {
1516 let dir = TempDir::new().expect("failed to create temporary directory");
1517 let foo = dir.path().join("foo.wdl");
1518 fs::write(
1519 &foo,
1520 r#"version 1.1
1521workflow test {
1522}
1523"#,
1524 )
1525 .expect("failed to create test file");
1526
1527 let bar = dir.path().join("bar.wdl");
1528 fs::write(
1529 &bar,
1530 r#"version 1.1
1531workflow test {
1532}
1533"#,
1534 )
1535 .expect("failed to create test file");
1536
1537 let baz = dir.path().join("baz.wdl");
1538 fs::write(
1539 &baz,
1540 r#"version 1.1
1541workflow test {
1542}
1543"#,
1544 )
1545 .expect("failed to create test file");
1546
1547 let analyzer = Analyzer::default();
1549 analyzer
1550 .add_directory(dir.path())
1551 .await
1552 .expect("should add documents");
1553
1554 let results = analyzer.analyze(()).await.unwrap();
1556 assert_eq!(results.len(), 3);
1557 assert!(results[0].document.diagnostics().next().is_none());
1558 assert!(results[1].document.diagnostics().next().is_none());
1559 assert!(results[2].document.diagnostics().next().is_none());
1560
1561 let results = analyzer.analyze(()).await.unwrap();
1563 assert_eq!(results.len(), 3);
1564
1565 analyzer
1567 .unroot_documents(vec![
1568 path_to_uri(dir.path()).expect("should convert to URI"),
1569 ])
1570 .await
1571 .unwrap();
1572 let results = analyzer.analyze(()).await.unwrap();
1573 assert!(results.is_empty());
1574 }
1575
1576 #[tokio::test]
1577 async fn selected_imported_task_conflicts_with_local_workflow() {
1578 let dir = TempDir::new().expect("failed to create temporary directory");
1579 fs::write(
1580 dir.path().join("lib.wdl"),
1581 r#"version 1.4
1582task run {
1583 command <<<>>>
1584}
1585"#,
1586 )
1587 .expect("failed to create library document");
1588 fs::write(
1589 dir.path().join("source.wdl"),
1590 r#"version 1.4
1591import { run } from "lib.wdl"
1592workflow run {
1593}
1594"#,
1595 )
1596 .expect("failed to create source document");
1597
1598 let config = Config::default()
1599 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1600 let analyzer = Analyzer::new(config, |(), _, _, _| async {});
1601 analyzer
1602 .add_document(path_to_uri(dir.path().join("source.wdl")).expect("should convert"))
1603 .await
1604 .expect("should add document");
1605
1606 let results = analyzer.analyze(()).await.expect("analysis should succeed");
1607 let source = results
1608 .iter()
1609 .find(|result| result.document.uri().path().contains("source.wdl"))
1610 .expect("should find source result");
1611 let errors = source
1612 .document
1613 .diagnostics()
1614 .filter(|diagnostic| diagnostic.severity() == Severity::Error)
1615 .map(|diagnostic| diagnostic.message())
1616 .collect::<Vec<_>>();
1617 assert_eq!(
1618 errors,
1619 ["import of `run` conflicts with an existing definition"]
1620 );
1621 }
1622
1623 #[tokio::test]
1624 async fn symbolic_import_resolves_through_mock_resolver() {
1625 use wdl_modules::Manifest;
1626 use wdl_modules::lockfile::ResolvedSource;
1627 use wdl_modules::resolver::MaterializedFile;
1628 use wdl_modules::resolver::ResolvedTree;
1629 use wdl_modules::resolver::ResolverError;
1630
1631 #[derive(Debug)]
1632 struct MockResolver {
1633 dep_path: PathBuf,
1634 }
1635
1636 #[async_trait::async_trait]
1637 impl wdl_modules::Resolver for MockResolver {
1638 async fn materialize(
1639 &self,
1640 _consumer: &wdl_modules::module::Module,
1641 path: &wdl_modules::symbolic_path::SymbolicPath,
1642 ) -> Result<MaterializedFile, ResolverError> {
1643 let rel = match path.sub_path() {
1644 Some(sub) => {
1645 let mut p = sub.to_path_buf();
1646 p.set_extension("wdl");
1647 p
1648 }
1649 None => std::path::PathBuf::from("index.wdl"),
1650 };
1651 let file_path = self.dep_path.join(rel);
1652 let manifest_bytes = fs::read(self.dep_path.join("module.json")).unwrap();
1653 let manifest = Manifest::parse(&manifest_bytes).unwrap();
1654 Ok(MaterializedFile {
1655 path: file_path,
1656 module_root: self.dep_path.clone(),
1657 source: ResolvedSource::Path {
1658 path: self.dep_path.clone(),
1659 },
1660 manifest: Arc::new(manifest),
1661 })
1662 }
1663
1664 async fn resolve_tree(
1665 &self,
1666 _consumer: &wdl_modules::module::Module,
1667 ) -> Result<ResolvedTree, ResolverError> {
1668 Ok(ResolvedTree::default())
1669 }
1670
1671 async fn discover_versions(
1672 &self,
1673 _name: &wdl_modules::dependency::DependencyName,
1674 _source: &wdl_modules::dependency::DependencySource,
1675 _scope: wdl_modules::resolver::DependencyScope,
1676 ) -> Result<Vec<semver::Version>, ResolverError> {
1677 Ok(Vec::new())
1678 }
1679 }
1680
1681 let dir = TempDir::new().expect("failed to create temporary directory");
1682
1683 let dep_dir = dir.path().join("dep");
1684 fs::create_dir_all(&dep_dir).unwrap();
1685 fs::write(
1686 dep_dir.join("module.json"),
1687 r#"{"name":"dep","version":"1.0.0","license":"MIT"}"#,
1688 )
1689 .unwrap();
1690 fs::write(
1691 dep_dir.join("index.wdl"),
1692 "version 1.4\n\ntask hello {\n command <<<>>>\n}\n",
1693 )
1694 .unwrap();
1695
1696 let consumer_dir = dir.path().join("consumer");
1697 fs::create_dir_all(&consumer_dir).unwrap();
1698 let dep_path_json = dep_dir.display().to_string().replace('\\', "/");
1699 fs::write(
1700 consumer_dir.join("module.json"),
1701 format!(
1702 r#"{{"name":"consumer","version":"0.1.0","license":"MIT","dependencies":{{"dep":{{"path":"{dep_path_json}"}}}}}}"#
1703 ),
1704 )
1705 .unwrap();
1706 fs::write(
1707 consumer_dir.join("source.wdl"),
1708 "version 1.4\n\nimport dep\nimport \"lib.wdl\"\n\nworkflow main {}\n",
1709 )
1710 .unwrap();
1711 fs::write(
1712 consumer_dir.join("lib.wdl"),
1713 "version 1.4\n\nimport dep\n\ntask lib {\n command <<<>>>\n}\n",
1714 )
1715 .unwrap();
1716
1717 let config = Config::default()
1718 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1719 let resolver: Arc<dyn wdl_modules::Resolver> = Arc::new(MockResolver {
1720 dep_path: dep_dir.clone(),
1721 });
1722 let consumer_module = wdl_modules::module::Module::load_from_path(&consumer_dir)
1723 .expect("test consumer module should load");
1724 let resolution = ResolutionContext::enabled(resolver, consumer_module);
1725 let analyzer = Analyzer::new_with_resolution(config, resolution, |(), _, _, _| async {});
1726 analyzer
1727 .add_document(path_to_uri(consumer_dir.join("source.wdl")).expect("should convert"))
1728 .await
1729 .expect("should add document");
1730
1731 let results = analyzer.analyze(()).await.unwrap();
1732 assert!(!results.is_empty(), "should have analysis results");
1733 let consumer_result = results
1734 .iter()
1735 .find(|r| r.document.uri().path().contains("source.wdl"))
1736 .expect("should find consumer result");
1737 let errors: Vec<_> = consumer_result
1738 .document
1739 .diagnostics()
1740 .filter(|d| d.severity() == Severity::Error)
1741 .collect();
1742 assert!(
1743 errors.is_empty(),
1744 "consumer should have no errors, got: {:?}",
1745 errors.iter().map(|d| d.message()).collect::<Vec<_>>()
1746 );
1747 let lib_result = results
1748 .iter()
1749 .find(|r| r.document.uri().path().contains("lib.wdl"))
1750 .expect("should find uri import result");
1751 let errors: Vec<_> = lib_result
1752 .document
1753 .diagnostics()
1754 .filter(|d| d.severity() == Severity::Error)
1755 .collect();
1756 assert!(
1757 errors.is_empty(),
1758 "uri import should have no errors, got: {:?}",
1759 errors.iter().map(|d| d.message()).collect::<Vec<_>>()
1760 );
1761 }
1762
1763 #[tokio::test]
1764 async fn concurrent_symbolic_imports_faster_than_serial() {
1765 use std::sync::atomic::AtomicUsize;
1766 use std::sync::atomic::Ordering;
1767 use std::time::Duration;
1768
1769 use wdl_modules::Manifest;
1770 use wdl_modules::lockfile::ResolvedSource;
1771 use wdl_modules::resolver::MaterializedFile;
1772 use wdl_modules::resolver::ResolvedTree;
1773 use wdl_modules::resolver::ResolverError;
1774
1775 #[derive(Debug)]
1777 struct SlowMockResolver {
1778 dep_path: PathBuf,
1779 delay: Duration,
1780 active: AtomicUsize,
1781 max_active: AtomicUsize,
1782 }
1783
1784 #[async_trait::async_trait]
1785 impl wdl_modules::Resolver for SlowMockResolver {
1786 async fn materialize(
1787 &self,
1788 _consumer: &wdl_modules::module::Module,
1789 path: &wdl_modules::symbolic_path::SymbolicPath,
1790 ) -> Result<MaterializedFile, ResolverError> {
1791 let active = self.active.fetch_add(1, Ordering::SeqCst) + 1;
1792 self.max_active.fetch_max(active, Ordering::SeqCst);
1793 tokio::time::sleep(self.delay).await;
1794 self.active.fetch_sub(1, Ordering::SeqCst);
1795 let rel = match path.sub_path() {
1796 Some(sub) => {
1797 let mut p = sub.to_path_buf();
1798 p.set_extension("wdl");
1799 p
1800 }
1801 None => std::path::PathBuf::from("index.wdl"),
1802 };
1803 let file_path = self.dep_path.join(rel);
1804 let manifest_bytes = fs::read(self.dep_path.join("module.json")).unwrap();
1805 let manifest = Manifest::parse(&manifest_bytes).unwrap();
1806 Ok(MaterializedFile {
1807 path: file_path,
1808 module_root: self.dep_path.clone(),
1809 source: ResolvedSource::Path {
1810 path: self.dep_path.clone(),
1811 },
1812 manifest: Arc::new(manifest),
1813 })
1814 }
1815
1816 async fn resolve_tree(
1817 &self,
1818 _consumer: &wdl_modules::module::Module,
1819 ) -> Result<ResolvedTree, ResolverError> {
1820 Ok(ResolvedTree::default())
1821 }
1822
1823 async fn discover_versions(
1824 &self,
1825 _name: &wdl_modules::dependency::DependencyName,
1826 _source: &wdl_modules::dependency::DependencySource,
1827 _scope: wdl_modules::resolver::DependencyScope,
1828 ) -> Result<Vec<semver::Version>, ResolverError> {
1829 Ok(Vec::new())
1830 }
1831 }
1832
1833 const IMPORT_COUNT: usize = 8;
1834 const DELAY_MS: u64 = 200;
1835
1836 let dir = TempDir::new().expect("failed to create temporary directory");
1837
1838 let dep_dir = dir.path().join("slowdep");
1839 fs::create_dir_all(&dep_dir).unwrap();
1840 fs::write(
1841 dep_dir.join("module.json"),
1842 r#"{"name":"slowdep","version":"1.0.0","license":"MIT"}"#,
1843 )
1844 .unwrap();
1845 for i in 0..IMPORT_COUNT {
1846 fs::write(
1847 dep_dir.join(format!("sub{i}.wdl")),
1848 "version 1.4\n\ntask noop {\n command <<<>>>\n}\n",
1849 )
1850 .unwrap();
1851 }
1852 fs::write(
1853 dep_dir.join("index.wdl"),
1854 "version 1.4\n\ntask noop {\n command <<<>>>\n}\n",
1855 )
1856 .unwrap();
1857
1858 let consumer_dir = dir.path().join("slowconsumer");
1859 fs::create_dir_all(&consumer_dir).unwrap();
1860 let dep_path_json = dep_dir.display().to_string().replace('\\', "/");
1861 fs::write(
1862 consumer_dir.join("module.json"),
1863 format!(
1864 r#"{{"name":"slowconsumer","version":"0.1.0","license":"MIT","dependencies":{{"slowdep":{{"path":"{dep_path_json}"}}}}}}"#
1865 ),
1866 )
1867 .unwrap();
1868
1869 let mut source = "version 1.4\n\n".to_string();
1870 for i in 0..IMPORT_COUNT {
1871 source.push_str(&format!("import slowdep/sub{i}\n"));
1872 }
1873 source.push_str("\nworkflow main {}\n");
1874 fs::write(consumer_dir.join("source.wdl"), &source).unwrap();
1875
1876 let config = Config::default()
1877 .with_feature_flags(crate::config::FeatureFlags::default().with_wdl_1_4());
1878 let resolver = Arc::new(SlowMockResolver {
1879 dep_path: dep_dir.clone(),
1880 delay: Duration::from_millis(DELAY_MS),
1881 active: AtomicUsize::new(0),
1882 max_active: AtomicUsize::new(0),
1883 });
1884 let resolver_trait: Arc<dyn wdl_modules::Resolver> = resolver.clone();
1885 let consumer_module = wdl_modules::module::Module::load_from_path(&consumer_dir)
1886 .expect("test consumer module should load");
1887 let resolution = ResolutionContext::enabled(resolver_trait, consumer_module);
1888 let analyzer = Analyzer::new_with_resolution(config, resolution, |(), _, _, _| async {});
1889 analyzer
1890 .add_document(path_to_uri(consumer_dir.join("source.wdl")).expect("should convert"))
1891 .await
1892 .expect("should add document");
1893
1894 let results = analyzer.analyze(()).await.unwrap();
1895
1896 assert!(!results.is_empty(), "should have analysis results");
1897 assert!(
1898 resolver.max_active.load(Ordering::SeqCst) > 1,
1899 "symbolic imports should materialize concurrently"
1900 );
1901 }
1902
1903 #[tokio::test]
1904 async fn it_deletes_documents() {
1905 let dir = TempDir::new().expect("failed to create temporary directory");
1906 let foo = dir.path().join("foo.wdl");
1907 fs::write(
1908 &foo,
1909 r#"version 1.1
1910import "bar.wdl"
1911
1912workflow test {
1913 call bar.test
1914}
1915"#,
1916 )
1917 .expect("failed to create test file");
1918
1919 let bar = dir.path().join("bar.wdl");
1920 fs::write(
1921 &bar,
1922 r#"version 1.1
1923workflow test {}
1924"#,
1925 )
1926 .expect("failed to create test file");
1927
1928 let analyzer = Analyzer::default();
1930 analyzer
1931 .add_directory(dir.path())
1932 .await
1933 .expect("should add documents");
1934
1935 let results = analyzer.analyze(()).await.unwrap();
1937 assert_eq!(results.len(), 2);
1938 assert!(results[0].document.diagnostics().next().is_none());
1939 assert!(results[1].document.diagnostics().next().is_none());
1940
1941 fs::remove_file(&bar).expect("should delete file");
1946 analyzer
1947 .delete_documents(vec![path_to_uri(&bar).expect("should convert to URI")])
1948 .await
1949 .unwrap();
1950
1951 let results = analyzer.analyze(()).await.unwrap();
1953 assert_eq!(results.len(), 1);
1954
1955 let has_import_failed_diagnostic = results[0]
1956 .document
1957 .diagnostics()
1958 .any(|d| d.message().contains("failed to import `bar.wdl`"));
1959 assert!(has_import_failed_diagnostic);
1960 }
1961}