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