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::absolute;
10use std::sync::Arc;
11use std::thread::JoinHandle;
12
13use anyhow::Context;
14use anyhow::Error;
15use anyhow::Result;
16use anyhow::anyhow;
17use anyhow::bail;
18use ignore::WalkBuilder;
19use indexmap::IndexSet;
20use line_index::LineCol;
21use line_index::LineIndex;
22use line_index::WideEncoding;
23use line_index::WideLineCol;
24use lsp_types::CallHierarchyIncomingCall;
25use lsp_types::CallHierarchyItem;
26use lsp_types::CallHierarchyOutgoingCall;
27use lsp_types::CompletionResponse;
28use lsp_types::DocumentSymbolResponse;
29use lsp_types::FoldingRange;
30use lsp_types::GotoDefinitionResponse;
31use lsp_types::Hover;
32use lsp_types::InlayHint;
33use lsp_types::Location;
34use lsp_types::SemanticTokensResult;
35use lsp_types::SignatureHelp;
36use lsp_types::SymbolInformation;
37use lsp_types::WorkspaceEdit;
38use path_clean::PathClean;
39use tokio::runtime::Handle;
40use tokio::sync::mpsc;
41use tokio::sync::oneshot;
42use url::Url;
43
44use crate::config::Config;
45use crate::document::Document;
46use crate::graph::DocumentGraphNode;
47use crate::graph::ParseState;
48use crate::queue::AddRequest;
49use crate::queue::AnalysisQueue;
50use crate::queue::AnalyzeRequest;
51use crate::queue::CallHierarchyRequest;
52use crate::queue::CompletionRequest;
53use crate::queue::DocumentSymbolRequest;
54use crate::queue::FindAllReferencesRequest;
55use crate::queue::FoldingRangeRequest;
56use crate::queue::FormatRequest;
57use crate::queue::GotoDefinitionRequest;
58use crate::queue::HoverRequest;
59use crate::queue::IncomingCallsRequest;
60use crate::queue::InlayHintsRequest;
61use crate::queue::NotifyChangeRequest;
62use crate::queue::NotifyIncrementalChangeRequest;
63use crate::queue::OutgoingCallsRequest;
64use crate::queue::RemoveRequest;
65use crate::queue::RenameRequest;
66use crate::queue::Request;
67use crate::queue::SemanticTokenRequest;
68use crate::queue::SignatureHelpRequest;
69use crate::queue::WorkspaceSymbolRequest;
70use crate::rayon::RayonHandle;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ProgressKind {
75 Parsing,
77 Analyzing,
79}
80
81impl fmt::Display for ProgressKind {
82 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83 match self {
84 Self::Parsing => write!(f, "parsing"),
85 Self::Analyzing => write!(f, "analyzing"),
86 }
87 }
88}
89
90pub fn path_to_uri(path: impl AsRef<Path>) -> Option<Url> {
92 Url::from_file_path(absolute(path).ok()?.clean()).ok()
93}
94
95#[derive(Debug, Clone)]
99pub struct AnalysisResult {
100 error: Option<Arc<Error>>,
103 version: Option<i32>,
109 lines: Option<Arc<LineIndex>>,
111 document: Document,
113}
114
115impl AnalysisResult {
116 pub(crate) fn new(node: &DocumentGraphNode) -> Self {
118 if let Some(error) = node.analysis_error() {
119 return Self {
120 error: Some(error.clone()),
121 version: node.parse_state().version(),
122 lines: node.parse_state().lines().cloned(),
123 document: Document::default_from_uri(node.uri().clone()),
124 };
125 }
126
127 let (error, version, lines) = match node.parse_state() {
128 ParseState::NotParsed => unreachable!("document should have been parsed"),
129 ParseState::Error(e) => (Some(e), None, None),
130 ParseState::Parsed { version, lines, .. } => (None, *version, Some(lines)),
131 };
132
133 Self {
134 error: error.cloned(),
135 version,
136 lines: lines.cloned(),
137 document: node
138 .document()
139 .expect("analysis should have completed")
140 .clone(),
141 }
142 }
143
144 pub fn error(&self) -> Option<&Arc<Error>> {
150 self.error.as_ref()
151 }
152
153 pub fn version(&self) -> Option<i32> {
158 self.version
159 }
160
161 pub fn lines(&self) -> Option<&Arc<LineIndex>> {
165 self.lines.as_ref()
166 }
167
168 pub fn document(&self) -> &Document {
170 &self.document
171 }
172}
173
174#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default)]
176pub struct SourcePosition {
177 pub line: u32,
180 pub character: u32,
183}
184
185impl SourcePosition {
186 pub fn new(line: u32, character: u32) -> Self {
188 Self { line, character }
189 }
190}
191
192#[derive(Debug, Eq, PartialEq, Copy, Clone)]
194pub enum SourcePositionEncoding {
195 UTF8,
199 UTF16,
203}
204
205#[derive(Debug, Clone)]
207pub struct SourceEdit {
208 range: Range<SourcePosition>,
212 encoding: SourcePositionEncoding,
214 text: String,
216}
217
218impl SourceEdit {
219 pub fn new(
221 range: Range<SourcePosition>,
222 encoding: SourcePositionEncoding,
223 text: impl Into<String>,
224 ) -> Self {
225 Self {
226 range,
227 encoding,
228 text: text.into(),
229 }
230 }
231
232 pub(crate) fn range(&self) -> Range<SourcePosition> {
234 self.range.start..self.range.end
235 }
236
237 pub(crate) fn apply(&self, source: &mut String, lines: &LineIndex) -> Result<()> {
239 let (start, end) = match self.encoding {
240 SourcePositionEncoding::UTF8 => (
241 LineCol {
242 line: self.range.start.line,
243 col: self.range.start.character,
244 },
245 LineCol {
246 line: self.range.end.line,
247 col: self.range.end.character,
248 },
249 ),
250 SourcePositionEncoding::UTF16 => (
251 lines
252 .to_utf8(
253 WideEncoding::Utf16,
254 WideLineCol {
255 line: self.range.start.line,
256 col: self.range.start.character,
257 },
258 )
259 .context("invalid edit start position")?,
260 lines
261 .to_utf8(
262 WideEncoding::Utf16,
263 WideLineCol {
264 line: self.range.end.line,
265 col: self.range.end.character,
266 },
267 )
268 .context("invalid edit end position")?,
269 ),
270 };
271
272 let range: Range<usize> = lines
273 .offset(start)
274 .context("invalid edit start position")?
275 .into()
276 ..lines
277 .offset(end)
278 .context("invalid edit end position")?
279 .into();
280
281 if !source.is_char_boundary(range.start) {
282 bail!("edit start position is not at a character boundary");
283 }
284
285 if !source.is_char_boundary(range.end) {
286 bail!("edit end position is not at a character boundary");
287 }
288
289 source.replace_range(range, &self.text);
290 Ok(())
291 }
292}
293
294#[derive(Clone, Debug)]
296pub struct IncrementalChange {
297 pub version: i32,
301 pub start: Option<String>,
309 pub edits: Vec<SourceEdit>,
311}
312
313#[derive(Debug)]
327pub struct Analyzer<Context> {
328 sender: ManuallyDrop<mpsc::UnboundedSender<Request<Context>>>,
330 handle: Option<JoinHandle<()>>,
332 config: Config,
334}
335
336impl<Context> Analyzer<Context>
337where
338 Context: Send + Clone + 'static,
339{
340 pub fn new<Progress, Return>(config: Config, progress: Progress) -> Self
348 where
349 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
350 Return: Future<Output = ()>,
351 {
352 Self::new_with_validator(config, progress, crate::Validator::default)
353 }
354
355 pub fn new_with_validator<Progress, Return, Validator>(
364 config: Config,
365 progress: Progress,
366 validator: Validator,
367 ) -> Self
368 where
369 Progress: Fn(Context, ProgressKind, usize, usize) -> Return + Send + 'static,
370 Return: Future<Output = ()>,
371 Validator: Fn() -> crate::Validator + Send + Sync + 'static,
372 {
373 let (tx, rx) = mpsc::unbounded_channel();
374 let tokio = Handle::current();
375 let inner_config = config.clone();
376 let handle = std::thread::spawn(move || {
377 let queue = AnalysisQueue::new(inner_config, tokio, progress, validator);
378 queue.run(rx);
379 });
380
381 Self {
382 sender: ManuallyDrop::new(tx),
383 handle: Some(handle),
384 config,
385 }
386 }
387
388 pub async fn add_document(&self, uri: Url) -> Result<()> {
392 let mut documents = IndexSet::new();
393 documents.insert(uri);
394
395 let (tx, rx) = oneshot::channel();
396 self.sender
397 .send(Request::Add(AddRequest {
398 documents,
399 completed: tx,
400 }))
401 .map_err(|_| {
402 anyhow!("failed to send request to analysis queue because the channel has closed")
403 })?;
404
405 rx.await.map_err(|_| {
406 anyhow!("failed to receive response from analysis queue because the channel has closed")
407 })?;
408
409 Ok(())
410 }
411
412 pub async fn add_directory(&self, path: impl AsRef<Path>) -> Result<()> {
418 let path = path.as_ref().to_path_buf();
419 let config = self.config.clone();
420 let documents = RayonHandle::spawn(move || -> Result<IndexSet<Url>> {
422 let mut documents = IndexSet::new();
423
424 let metadata = path.metadata().with_context(|| {
425 format!(
426 "failed to read metadata for `{path}`",
427 path = path.display()
428 )
429 })?;
430
431 if metadata.is_file() {
432 bail!("`{path}` is a file, not a directory", path = path.display());
433 }
434
435 let mut walker = WalkBuilder::new(&path);
436 if let Some(ignore_filename) = config.ignore_filename() {
437 walker.add_custom_ignore_filename(ignore_filename);
438 }
439 let walker = walker
440 .standard_filters(false)
441 .parents(true)
442 .follow_links(true)
443 .build();
444
445 for result in walker {
446 let entry = result.with_context(|| {
447 format!("failed to read directory `{path}`", path = path.display())
448 })?;
449
450 let Some(file_type) = entry.file_type() else {
452 continue;
453 };
454 if !file_type.is_file() {
456 continue;
457 }
458 if entry.path().extension() != Some(OsStr::new("wdl")) {
460 continue;
461 }
462
463 documents.insert(path_to_uri(entry.path()).with_context(|| {
464 format!(
465 "failed to convert path `{path}` to a URI",
466 path = entry.path().display()
467 )
468 })?);
469 }
470
471 Ok(documents)
472 })
473 .await?;
474
475 if documents.is_empty() {
476 return Ok(());
477 }
478
479 let (tx, rx) = oneshot::channel();
481 self.sender
482 .send(Request::Add(AddRequest {
483 documents,
484 completed: tx,
485 }))
486 .map_err(|_| {
487 anyhow!("failed to send request to analysis queue because the channel has closed")
488 })?;
489
490 rx.await.map_err(|_| {
491 anyhow!("failed to receive response from analysis queue because the channel has closed")
492 })?;
493
494 Ok(())
495 }
496
497 pub async fn remove_documents(&self, documents: Vec<Url>) -> Result<()> {
504 let (tx, rx) = oneshot::channel();
506 self.sender
507 .send(Request::Remove(RemoveRequest {
508 documents,
509 completed: tx,
510 }))
511 .map_err(|_| {
512 anyhow!("failed to send request to analysis queue because the channel has closed")
513 })?;
514
515 rx.await.map_err(|_| {
516 anyhow!("failed to receive response from analysis queue because the channel has closed")
517 })?;
518
519 Ok(())
520 }
521
522 pub fn notify_incremental_change(
526 &self,
527 document: Url,
528 change: IncrementalChange,
529 ) -> Result<()> {
530 self.sender
531 .send(Request::NotifyIncrementalChange(
532 NotifyIncrementalChangeRequest { document, change },
533 ))
534 .map_err(|_| {
535 anyhow!("failed to send request to analysis queue because the channel has closed")
536 })
537 }
538
539 pub fn notify_change(&self, document: Url, discard_pending: bool) -> Result<()> {
548 self.sender
549 .send(Request::NotifyChange(NotifyChangeRequest {
550 document,
551 discard_pending,
552 }))
553 .map_err(|_| {
554 anyhow!("failed to send request to analysis queue because the channel has closed")
555 })
556 }
557
558 pub async fn analyze_document(
567 &self,
568 context: Context,
569 document: Url,
570 ) -> Result<Vec<AnalysisResult>> {
571 let (tx, rx) = oneshot::channel();
573 self.sender
574 .send(Request::Analyze(AnalyzeRequest {
575 document: Some(document),
576 context,
577 completed: tx,
578 }))
579 .map_err(|_| {
580 anyhow!("failed to send request to analysis queue because the channel has closed")
581 })?;
582
583 rx.await.map_err(|_| {
584 anyhow!("failed to receive response from analysis queue because the channel has closed")
585 })?
586 }
587
588 pub async fn analyze(&self, context: Context) -> Result<Vec<AnalysisResult>> {
597 let (tx, rx) = oneshot::channel();
599 self.sender
600 .send(Request::Analyze(AnalyzeRequest {
601 document: None, context,
603 completed: tx,
604 }))
605 .map_err(|_| {
606 anyhow!("failed to send request to analysis queue because the channel has closed")
607 })?;
608
609 rx.await.map_err(|_| {
610 anyhow!("failed to receive response from analysis queue because the channel has closed")
611 })?
612 }
613
614 pub async fn call_hierarchy(
616 &self,
617 document: Url,
618 position: SourcePosition,
619 encoding: SourcePositionEncoding,
620 ) -> Result<Option<Vec<CallHierarchyItem>>> {
621 let (tx, rx) = oneshot::channel();
622 self.sender
623 .send(Request::CallHierarchy(CallHierarchyRequest {
624 document,
625 position,
626 encoding,
627 completed: tx,
628 }))
629 .map_err(|_| {
630 anyhow!(
631 "failed to send call hierarchy request to analysis queue because the channel \
632 has closed"
633 )
634 })?;
635
636 rx.await.map_err(|_| {
637 anyhow!(
638 "failed to receive call hierarchy response from analysis queue because the \
639 channel has closed"
640 )
641 })
642 }
643
644 pub async fn format_document(&self, document: Url) -> Result<Option<(u32, u32, String)>> {
646 let (tx, rx) = oneshot::channel();
647 self.sender
648 .send(Request::Format(FormatRequest {
649 document,
650 completed: tx,
651 }))
652 .map_err(|_| {
653 anyhow!("failed to send format request to the queue because the channel has closed")
654 })?;
655
656 rx.await.map_err(|_| {
657 anyhow!("failed to send format request to the queue because the channel has closed")
658 })
659 }
660
661 pub async fn folding_range(&self, document: Url) -> Result<Option<Vec<FoldingRange>>> {
663 let (tx, rx) = oneshot::channel();
664 self.sender
665 .send(Request::FoldingRange(FoldingRangeRequest {
666 document,
667 completed: tx,
668 }))
669 .map_err(|_| {
670 anyhow!(
671 "failed to send folding range request to the queue because the channel has \
672 closed"
673 )
674 })?;
675
676 rx.await.map_err(|_| {
677 anyhow!(
678 "failed to receive folding range response from analysis queue because the channel \
679 has closed"
680 )
681 })
682 }
683
684 pub async fn goto_definition(
686 &self,
687 document: Url,
688 position: SourcePosition,
689 encoding: SourcePositionEncoding,
690 ) -> Result<Option<GotoDefinitionResponse>> {
691 let (tx, rx) = oneshot::channel();
692 self.sender
693 .send(Request::GotoDefinition(GotoDefinitionRequest {
694 document,
695 position,
696 encoding,
697 completed: tx,
698 }))
699 .map_err(|_| {
700 anyhow!(
701 "failed to send goto definition request to analysis queue because the channel \
702 has closed"
703 )
704 })?;
705
706 rx.await.map_err(|_| {
707 anyhow!(
708 "failed to receive goto definition response from analysis queue because the \
709 channel has closed"
710 )
711 })
712 }
713
714 pub async fn find_all_references(
716 &self,
717 document: Url,
718 position: SourcePosition,
719 encoding: SourcePositionEncoding,
720 include_declaration: bool,
721 ) -> Result<Vec<Location>> {
722 let (tx, rx) = oneshot::channel();
723 self.sender
724 .send(Request::FindAllReferences(FindAllReferencesRequest {
725 document,
726 position,
727 encoding,
728 include_declaration,
729 completed: tx,
730 }))
731 .map_err(|_| {
732 anyhow!(
733 "failed to send find all references request to analysis queue because the \
734 channel has closed"
735 )
736 })?;
737
738 rx.await.map_err(|_| {
739 anyhow!(
740 "failed to receive find all references response from analysis queue because the \
741 client channel has closed"
742 )
743 })
744 }
745
746 pub async fn completion(
748 &self,
749 context: Context,
750 document: Url,
751 position: SourcePosition,
752 encoding: SourcePositionEncoding,
753 ) -> Result<Option<CompletionResponse>> {
754 let (tx, rx) = oneshot::channel();
755 self.sender
756 .send(Request::Completion(CompletionRequest {
757 document,
758 position,
759 encoding,
760 context,
761 completed: tx,
762 }))
763 .map_err(|_| {
764 anyhow!(
765 "failed to send completion request to analysis queue because the channel has \
766 closed"
767 )
768 })?;
769
770 rx.await.map_err(|_| {
771 anyhow!(
772 "failed to send completion request to analysis queue because the channel has \
773 closed"
774 )
775 })
776 }
777
778 pub async fn hover(
780 &self,
781 document: Url,
782 position: SourcePosition,
783 encoding: SourcePositionEncoding,
784 ) -> Result<Option<Hover>> {
785 let (tx, rx) = oneshot::channel();
786 self.sender
787 .send(Request::Hover(HoverRequest {
788 document,
789 position,
790 encoding,
791 completed: tx,
792 }))
793 .map_err(|_| {
794 anyhow!(
795 "failed to send hover request to analysis queue because the channel has closed"
796 )
797 })?;
798
799 rx.await.map_err(|_| {
800 anyhow!("failed to send hover request to analysis queue because the channel has closed")
801 })
802 }
803
804 pub async fn rename(
806 &self,
807 document: Url,
808 position: SourcePosition,
809 encoding: SourcePositionEncoding,
810 new_name: String,
811 ) -> Result<Option<WorkspaceEdit>> {
812 let (tx, rx) = oneshot::channel();
813 self.sender
814 .send(Request::Rename(RenameRequest {
815 document,
816 position,
817 encoding,
818 new_name,
819 completed: tx,
820 }))
821 .map_err(|_| {
822 anyhow!(
823 "failed to send rename request to analysis queue because the channel has \
824 closed"
825 )
826 })?;
827
828 rx.await.map_err(|_| {
829 anyhow!(
830 "failed to receive rename response from analysis queue because the channel has \
831 closed"
832 )
833 })
834 }
835
836 pub async fn semantic_tokens(&self, document: Url) -> Result<Option<SemanticTokensResult>> {
838 let (tx, rx) = oneshot::channel();
839 self.sender
840 .send(Request::SemanticTokens(SemanticTokenRequest {
841 document,
842 completed: tx,
843 }))
844 .map_err(|_| {
845 anyhow!(
846 "failed to send semantic tokens request to analysis queue because the channel \
847 has closed"
848 )
849 })?;
850
851 rx.await.map_err(|_| {
852 anyhow!(
853 "failed to receive semantic tokens response from analysis queue because the \
854 channel has closed"
855 )
856 })
857 }
858
859 pub async fn document_symbol(&self, document: Url) -> Result<Option<DocumentSymbolResponse>> {
861 let (tx, rx) = oneshot::channel();
862 self.sender
863 .send(Request::DocumentSymbol(DocumentSymbolRequest {
864 document,
865 completed: tx,
866 }))
867 .map_err(|_| {
868 anyhow!(
869 "failed to send document symbol request to analysis queue because the channel \
870 has closed"
871 )
872 })?;
873
874 rx.await.map_err(|_| {
875 anyhow!(
876 "failed to receive document symbol request to analysis queue because the channel \
877 has closed"
878 )
879 })
880 }
881
882 pub async fn workspace_symbol(&self, query: String) -> Result<Option<Vec<SymbolInformation>>> {
884 let (tx, rx) = oneshot::channel();
885 self.sender
886 .send(Request::WorkspaceSymbol(WorkspaceSymbolRequest {
887 query,
888 completed: tx,
889 }))
890 .map_err(|_| {
891 anyhow!(
892 "failed to send workspace symbol request to analysis queue because the \
893 channel has closed"
894 )
895 })?;
896
897 rx.await.map_err(|_| {
898 anyhow!(
899 "failed to receive workspace symbol response from analysis queue because the \
900 channel has closed"
901 )
902 })
903 }
904
905 pub async fn incoming_calls(
907 &self,
908 document: Url,
909 position: SourcePosition,
910 encoding: SourcePositionEncoding,
911 ) -> Result<Option<Vec<CallHierarchyIncomingCall>>> {
912 let (tx, rx) = oneshot::channel();
913 self.sender
914 .send(Request::IncomingCalls(IncomingCallsRequest {
915 document,
916 position,
917 encoding,
918 completed: tx,
919 }))
920 .map_err(|_| {
921 anyhow!(
922 "failed to send incoming calls request to analysis queue because the channel \
923 has closed"
924 )
925 })?;
926
927 rx.await.map_err(|_| {
928 anyhow!(
929 "failed to receive incoming calls response from analysis queue because the \
930 channel has closed"
931 )
932 })
933 }
934
935 pub async fn outgoing_calls(
937 &self,
938 document: Url,
939 position: SourcePosition,
940 encoding: SourcePositionEncoding,
941 ) -> Result<Option<Vec<CallHierarchyOutgoingCall>>> {
942 let (tx, rx) = oneshot::channel();
943 self.sender
944 .send(Request::OutgoingCalls(OutgoingCallsRequest {
945 document,
946 position,
947 encoding,
948 completed: tx,
949 }))
950 .map_err(|_| {
951 anyhow!(
952 "failed to send outgoing calls request to analysis queue because the channel \
953 has closed"
954 )
955 })?;
956
957 rx.await.map_err(|_| {
958 anyhow!(
959 "failed to receive outgoing calls response from analysis queue because the \
960 channel has closed"
961 )
962 })
963 }
964
965 pub async fn signature_help(
967 &self,
968 document: Url,
969 position: SourcePosition,
970 encoding: SourcePositionEncoding,
971 ) -> Result<Option<SignatureHelp>> {
972 let (tx, rx) = oneshot::channel();
973 self.sender
974 .send(Request::SignatureHelp(SignatureHelpRequest {
975 document,
976 position,
977 encoding,
978 completed: tx,
979 }))
980 .map_err(|_| {
981 anyhow!(
982 "failed to send signature help request to analysis queue because the channel \
983 has closed"
984 )
985 })?;
986
987 rx.await.map_err(|_| {
988 anyhow!(
989 "failed to receive signature help response from analysis queue because the \
990 channel has closed"
991 )
992 })
993 }
994
995 pub async fn inlay_hints(
997 &self,
998 document: Url,
999 range: lsp_types::Range,
1000 ) -> Result<Option<Vec<InlayHint>>> {
1001 let (tx, rx) = oneshot::channel();
1002 self.sender
1003 .send(Request::InlayHints(InlayHintsRequest {
1004 document,
1005 range,
1006 completed: tx,
1007 }))
1008 .map_err(|_| {
1009 anyhow!(
1010 "failed to send inlay hints request to analysis queue because the channel has \
1011 closed"
1012 )
1013 })?;
1014
1015 rx.await.map_err(|_| {
1016 anyhow!(
1017 "failed to receive inlay hints response from analysis queue because the channel \
1018 has closed"
1019 )
1020 })
1021 }
1022}
1023
1024impl Default for Analyzer<()> {
1025 fn default() -> Self {
1026 Self::new(Default::default(), |_, _, _, _| async {})
1027 }
1028}
1029
1030impl<C> Drop for Analyzer<C> {
1031 fn drop(&mut self) {
1032 unsafe { ManuallyDrop::drop(&mut self.sender) };
1033 if let Some(handle) = self.handle.take() {
1034 handle.join().unwrap();
1035 }
1036 }
1037}
1038
1039const _: () = {
1042 const fn _assert<T: Send + Sync>() {}
1044 _assert::<Analyzer<()>>();
1045};
1046
1047#[cfg(test)]
1048mod test {
1049 use std::fs;
1050
1051 use tempfile::TempDir;
1052 use wdl_ast::Severity;
1053
1054 use super::*;
1055
1056 #[tokio::test]
1057 async fn it_returns_empty_results() {
1058 let analyzer = Analyzer::default();
1059 let results = analyzer.analyze(()).await.unwrap();
1060 assert!(results.is_empty());
1061 }
1062
1063 #[tokio::test]
1064 async fn it_analyzes_a_document() {
1065 let dir = TempDir::new().expect("failed to create temporary directory");
1066 let path = dir.path().join("foo.wdl");
1067 fs::write(
1068 &path,
1069 r#"version 1.1
1070
1071task test {
1072 command <<<>>>
1073}
1074
1075workflow test {
1076}
1077"#,
1078 )
1079 .expect("failed to create test file");
1080
1081 let analyzer = Analyzer::default();
1083 analyzer
1084 .add_document(path_to_uri(&path).expect("should convert to URI"))
1085 .await
1086 .expect("should add document");
1087
1088 let results = analyzer.analyze(()).await.unwrap();
1089 assert_eq!(results.len(), 1);
1090 assert_eq!(results[0].document.diagnostics().count(), 1);
1091 assert_eq!(
1092 results[0].document.diagnostics().next().unwrap().rule(),
1093 None
1094 );
1095 assert_eq!(
1096 results[0].document.diagnostics().next().unwrap().severity(),
1097 Severity::Error
1098 );
1099 assert_eq!(
1100 results[0].document.diagnostics().next().unwrap().message(),
1101 "conflicting workflow name `test`"
1102 );
1103
1104 let id = results[0].document.id().clone();
1106 let results = analyzer.analyze(()).await.unwrap();
1107 assert_eq!(results.len(), 1);
1108 assert_eq!(results[0].document.id().as_ref(), id.as_ref());
1109 assert_eq!(results[0].document.diagnostics().count(), 1);
1110 assert_eq!(
1111 results[0].document.diagnostics().next().unwrap().rule(),
1112 None
1113 );
1114 assert_eq!(
1115 results[0].document.diagnostics().next().unwrap().severity(),
1116 Severity::Error
1117 );
1118 assert_eq!(
1119 results[0].document.diagnostics().next().unwrap().message(),
1120 "conflicting workflow name `test`"
1121 );
1122 }
1123
1124 #[tokio::test]
1125 async fn it_reanalyzes_a_document_on_change() {
1126 let dir = TempDir::new().expect("failed to create temporary directory");
1127 let path = dir.path().join("foo.wdl");
1128 fs::write(
1129 &path,
1130 r#"version 1.1
1131
1132task test {
1133 command <<<>>>
1134}
1135
1136workflow test {
1137}
1138"#,
1139 )
1140 .expect("failed to create test file");
1141
1142 let analyzer = Analyzer::default();
1144 analyzer
1145 .add_document(path_to_uri(&path).expect("should convert to URI"))
1146 .await
1147 .expect("should add document");
1148
1149 let results = analyzer.analyze(()).await.unwrap();
1150 assert_eq!(results.len(), 1);
1151 assert_eq!(results[0].document.diagnostics().count(), 1);
1152 assert_eq!(
1153 results[0].document.diagnostics().next().unwrap().rule(),
1154 None
1155 );
1156 assert_eq!(
1157 results[0].document.diagnostics().next().unwrap().severity(),
1158 Severity::Error
1159 );
1160 assert_eq!(
1161 results[0].document.diagnostics().next().unwrap().message(),
1162 "conflicting workflow name `test`"
1163 );
1164
1165 fs::write(
1167 &path,
1168 r#"version 1.1
1169
1170task test {
1171 command <<<>>>
1172}
1173
1174workflow something_else {
1175}
1176"#,
1177 )
1178 .expect("failed to create test file");
1179
1180 let uri = path_to_uri(&path).expect("should convert to URI");
1181 analyzer.notify_change(uri.clone(), false).unwrap();
1182
1183 let id = results[0].document.id().clone();
1186 let results = analyzer.analyze(()).await.unwrap();
1187 assert_eq!(results.len(), 1);
1188 assert!(results[0].document.id().as_ref() != id.as_ref());
1189 assert_eq!(results[0].document.diagnostics().count(), 0);
1190
1191 let id = results[0].document.id().clone();
1193 let results = analyzer.analyze_document((), uri).await.unwrap();
1194 assert_eq!(results.len(), 1);
1195 assert!(results[0].document.id().as_ref() == id.as_ref());
1196 assert_eq!(results[0].document.diagnostics().count(), 0);
1197 }
1198
1199 #[tokio::test]
1200 async fn it_reanalyzes_a_document_on_incremental_change() {
1201 let dir = TempDir::new().expect("failed to create temporary directory");
1202 let path = dir.path().join("foo.wdl");
1203 fs::write(
1204 &path,
1205 r#"version 1.1
1206
1207task test {
1208 command <<<>>>
1209}
1210
1211workflow test {
1212}
1213"#,
1214 )
1215 .expect("failed to create test file");
1216
1217 let analyzer = Analyzer::default();
1219 analyzer
1220 .add_document(path_to_uri(&path).expect("should convert to URI"))
1221 .await
1222 .expect("should add document");
1223
1224 let results = analyzer.analyze(()).await.unwrap();
1225 assert_eq!(results.len(), 1);
1226 assert_eq!(results[0].document.diagnostics().count(), 1);
1227 assert_eq!(
1228 results[0].document.diagnostics().next().unwrap().rule(),
1229 None
1230 );
1231 assert_eq!(
1232 results[0].document.diagnostics().next().unwrap().severity(),
1233 Severity::Error
1234 );
1235 assert_eq!(
1236 results[0].document.diagnostics().next().unwrap().message(),
1237 "conflicting workflow name `test`"
1238 );
1239
1240 let uri = path_to_uri(&path).expect("should convert to URI");
1242 analyzer
1243 .notify_incremental_change(
1244 uri.clone(),
1245 IncrementalChange {
1246 version: 2,
1247 start: None,
1248 edits: vec![SourceEdit {
1249 range: SourcePosition::new(6, 9)..SourcePosition::new(6, 13),
1250 encoding: SourcePositionEncoding::UTF8,
1251 text: "something_else".to_string(),
1252 }],
1253 },
1254 )
1255 .unwrap();
1256
1257 let id = results[0].document.id().clone();
1260 let results = analyzer.analyze_document((), uri).await.unwrap();
1261 assert_eq!(results.len(), 1);
1262 assert!(results[0].document.id().as_ref() != id.as_ref());
1263 assert_eq!(results[0].document.diagnostics().count(), 0);
1264 }
1265
1266 #[tokio::test]
1267 async fn it_removes_documents() {
1268 let dir = TempDir::new().expect("failed to create temporary directory");
1269 let foo = dir.path().join("foo.wdl");
1270 fs::write(
1271 &foo,
1272 r#"version 1.1
1273workflow test {
1274}
1275"#,
1276 )
1277 .expect("failed to create test file");
1278
1279 let bar = dir.path().join("bar.wdl");
1280 fs::write(
1281 &bar,
1282 r#"version 1.1
1283workflow test {
1284}
1285"#,
1286 )
1287 .expect("failed to create test file");
1288
1289 let baz = dir.path().join("baz.wdl");
1290 fs::write(
1291 &baz,
1292 r#"version 1.1
1293workflow test {
1294}
1295"#,
1296 )
1297 .expect("failed to create test file");
1298
1299 let analyzer = Analyzer::default();
1301 analyzer
1302 .add_directory(dir.path())
1303 .await
1304 .expect("should add documents");
1305
1306 let results = analyzer.analyze(()).await.unwrap();
1308 assert_eq!(results.len(), 3);
1309 assert!(results[0].document.diagnostics().next().is_none());
1310 assert!(results[1].document.diagnostics().next().is_none());
1311 assert!(results[2].document.diagnostics().next().is_none());
1312
1313 let results = analyzer.analyze(()).await.unwrap();
1315 assert_eq!(results.len(), 3);
1316
1317 analyzer
1319 .remove_documents(vec![
1320 path_to_uri(dir.path()).expect("should convert to URI"),
1321 ])
1322 .await
1323 .unwrap();
1324 let results = analyzer.analyze(()).await.unwrap();
1325 assert!(results.is_empty());
1326 }
1327}