Skip to main content

wdl_analysis/
analyzer.rs

1//! Implementation of the analyzer.
2
3use 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/// Represents the kind of analysis progress being reported.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ProgressKind {
75    /// The progress is for parsing documents.
76    Parsing,
77    /// The progress is for analyzing documents.
78    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
90/// Converts a local file path to a file schemed URI.
91pub fn path_to_uri(path: impl AsRef<Path>) -> Option<Url> {
92    Url::from_file_path(absolute(path).ok()?.clean()).ok()
93}
94
95/// Represents the result of an analysis.
96///
97/// Analysis results are cheap to clone.
98#[derive(Debug, Clone)]
99pub struct AnalysisResult {
100    /// The error that occurred when attempting to parse the file (e.g. the file
101    /// could not be opened).
102    error: Option<Arc<Error>>,
103    /// The monotonic version of the document that was parsed.
104    ///
105    /// This value comes from incremental changes to the file.
106    ///
107    /// If `None`, the parsed version had no incremental changes.
108    version: Option<i32>,
109    /// The lines indexed for the parsed file.
110    lines: Option<Arc<LineIndex>>,
111    /// The analyzed document.
112    document: Document,
113}
114
115impl AnalysisResult {
116    /// Constructs a new analysis result for the given graph node.
117    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    /// Gets the error that occurred when attempting to parse the document.
145    ///
146    /// An example error would be if the file could not be opened.
147    ///
148    /// Returns `None` if the document was parsed successfully.
149    pub fn error(&self) -> Option<&Arc<Error>> {
150        self.error.as_ref()
151    }
152
153    /// Gets the incremental version of the parsed document.
154    ///
155    /// Returns `None` if there was an error parsing the document or if the
156    /// parsed document had no incremental changes.
157    pub fn version(&self) -> Option<i32> {
158        self.version
159    }
160
161    /// Gets the line index of the parsed document.
162    ///
163    /// Returns `None` if there was an error parsing the document.
164    pub fn lines(&self) -> Option<&Arc<LineIndex>> {
165        self.lines.as_ref()
166    }
167
168    /// Gets the analyzed document.
169    pub fn document(&self) -> &Document {
170        &self.document
171    }
172}
173
174/// Represents a position in a document's source.
175#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default)]
176pub struct SourcePosition {
177    /// Line position in a document (zero-based).
178    // NOTE: this field must come before `character` to maintain a correct sort order.
179    pub line: u32,
180    /// Character offset on a line in a document (zero-based). The meaning of
181    /// this offset is determined by the position encoding.
182    pub character: u32,
183}
184
185impl SourcePosition {
186    /// Constructs a new source position from a line and character offset.
187    pub fn new(line: u32, character: u32) -> Self {
188        Self { line, character }
189    }
190}
191
192/// Represents the encoding of a source position.
193#[derive(Debug, Eq, PartialEq, Copy, Clone)]
194pub enum SourcePositionEncoding {
195    /// The position is UTF8 encoded.
196    ///
197    /// A position's character is the UTF-8 offset from the start of the line.
198    UTF8,
199    /// The position is UTF16 encoded.
200    ///
201    /// A position's character is the UTF-16 offset from the start of the line.
202    UTF16,
203}
204
205/// Represents an edit to a document's source.
206#[derive(Debug, Clone)]
207pub struct SourceEdit {
208    /// The range of the edit.
209    ///
210    /// Note that invalid ranges will cause the edit to be ignored.
211    range: Range<SourcePosition>,
212    /// The encoding of the edit positions.
213    encoding: SourcePositionEncoding,
214    /// The replacement text.
215    text: String,
216}
217
218impl SourceEdit {
219    /// Creates a new source edit for the given range and replacement text.
220    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    /// Gets the range of the edit.
233    pub(crate) fn range(&self) -> Range<SourcePosition> {
234        self.range.start..self.range.end
235    }
236
237    /// Applies the edit to the given string if it's in range.
238    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/// Represents an incremental change to a document.
295#[derive(Clone, Debug)]
296pub struct IncrementalChange {
297    /// The monotonic version of the document.
298    ///
299    /// This is expected to increase for each incremental change.
300    pub version: i32,
301    /// The source to start from for applying edits.
302    ///
303    /// If this is `Some`, a full reparse will occur after applying edits to
304    /// this string.
305    ///
306    /// If this is `None`, edits will be applied to the existing CST and an
307    /// attempt will be made to incrementally parse the file.
308    pub start: Option<String>,
309    /// The source edits to apply.
310    pub edits: Vec<SourceEdit>,
311}
312
313/// Represents a Workflow Description Language (WDL) document analyzer.
314///
315/// By default, analysis parses documents, performs validation checks, resolves
316/// imports, and performs type checking.
317///
318/// Each analysis operation is processed in order of request; however, the
319/// individual parsing, resolution, and analysis of documents is performed
320/// across a thread pool.
321///
322/// Note that dropping the analyzer is a blocking operation as it will wait for
323/// the queue thread to join.
324///
325/// The type parameter is the context type passed to the progress callback.
326#[derive(Debug)]
327pub struct Analyzer<Context> {
328    /// The sender for sending analysis requests to the queue.
329    sender: ManuallyDrop<mpsc::UnboundedSender<Request<Context>>>,
330    /// The join handle for the queue task.
331    handle: Option<JoinHandle<()>>,
332    /// The config to use during analysis.
333    config: Config,
334}
335
336impl<Context> Analyzer<Context>
337where
338    Context: Send + Clone + 'static,
339{
340    /// Constructs a new analyzer with the given config.
341    ///
342    /// The provided progress callback will be invoked during analysis.
343    ///
344    /// The analyzer will use a default validator for validation.
345    ///
346    /// The analyzer must be constructed from the context of a Tokio runtime.
347    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    /// Constructs a new analyzer with the given config and validator function.
356    ///
357    /// The provided progress callback will be invoked during analysis.
358    ///
359    /// This validator function will be called once per worker thread to
360    /// initialize a thread-local validator.
361    ///
362    /// The analyzer must be constructed from the context of a Tokio runtime.
363    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    /// Adds a document to the analyzer. Document can be a local file or a URL.
389    ///
390    /// Returns an error if the document could not be added.
391    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    /// Adds a directory to the analyzer. It will recursively search for WDL
413    /// documents in the supplied directory.
414    ///
415    /// Returns an error if there was a problem discovering documents for the
416    /// specified path.
417    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        // Start by searching for documents
421        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                // Skip entries without a file type
451                let Some(file_type) = entry.file_type() else {
452                    continue;
453                };
454                // Skip non-files
455                if !file_type.is_file() {
456                    continue;
457                }
458                // Skip files without a `.wdl` extension
459                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        // Send the add request to the queue
480        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    /// Removes the specified documents from the analyzer.
498    ///
499    /// If a specified URI is a prefix (i.e. directory) of documents known to
500    /// the analyzer, those documents will be removed.
501    ///
502    /// Documents are only removed when not referenced from importing documents.
503    pub async fn remove_documents(&self, documents: Vec<Url>) -> Result<()> {
504        // Send the remove request to the queue
505        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    /// Notifies the analyzer that a document has an incremental change.
523    ///
524    /// Changes to documents that aren't known to the analyzer are ignored.
525    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    /// Notifies the analyzer that a document has fully changed and should be
540    /// fetched again.
541    ///
542    /// Changes to documents that aren't known to the analyzer are ignored.
543    ///
544    /// If `discard_pending` is true, then any pending incremental changes are
545    /// discarded; otherwise, the full change is ignored if there are pending
546    /// incremental changes.
547    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    /// Analyzes a specific document.
559    ///
560    /// The provided context is passed to the progress callback.
561    ///
562    /// If the document is up-to-date and was previously analyzed, the current
563    /// analysis result is returned.
564    ///
565    /// Returns an analysis result for each document that was analyzed.
566    pub async fn analyze_document(
567        &self,
568        context: Context,
569        document: Url,
570    ) -> Result<Vec<AnalysisResult>> {
571        // Send the analyze request to the queue
572        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    /// Performs analysis of all documents.
589    ///
590    /// The provided context is passed to the progress callback.
591    ///
592    /// If a document is up-to-date and was previously analyzed, the current
593    /// analysis result is returned.
594    ///
595    /// Returns an analysis result for each document that was analyzed.
596    pub async fn analyze(&self, context: Context) -> Result<Vec<AnalysisResult>> {
597        // Send the analyze request to the queue
598        let (tx, rx) = oneshot::channel();
599        self.sender
600            .send(Request::Analyze(AnalyzeRequest {
601                document: None, // analyze all documents
602                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    /// Get the call hierarchy for the symbol at the current position.
615    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    /// Formats a document.
645    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    /// Get all folding ranges in a document.
662    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    /// Performs a "goto definition" for a symbol at the current position.
685    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    /// Performs a `find references` for a symbol across all the documents.
715    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    /// Performs a `auto-completion` for a symbol.
747    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    /// Performs a `hover` for a symbol at a given position in a document.
779    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    /// Renames a symbol at a given position across the workspace.
805    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    /// Gets semantic tokens for a document
837    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    /// Gets document symbols for a document.
860    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    /// Gets document symbols for the workspace.
883    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    /// Get the incoming calls for the symbol at the current position.
906    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    /// Get the outgoing calls for the symbol at the current position.
936    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    /// Gets signature help for a function call at a given position.
966    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    /// Requests inlay hints for a document.
996    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
1039/// Constant that asserts `Analyzer` is `Send + Sync`; if not, it fails to
1040/// compile.
1041const _: () = {
1042    /// Helper that will fail to compile if T is not `Send + Sync`.
1043    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        // Analyze the file and check the resulting diagnostic
1082        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        // Analyze again and ensure the analysis result id is unchanged
1105        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        // Analyze the file and check the resulting diagnostic
1143        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        // Rewrite the file to correct the issue
1166        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        // Analyze again and ensure the analysis result id is changed and the issue
1184        // fixed
1185        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        // Analyze again and ensure the analysis result id is unchanged
1192        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        // Analyze the file and check the resulting diagnostic
1218        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        // Edit the file to correct the issue
1241        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        // Analyze again and ensure the analysis result id is changed and the issue was
1258        // fixed
1259        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        // Add all three documents to the analyzer
1300        let analyzer = Analyzer::default();
1301        analyzer
1302            .add_directory(dir.path())
1303            .await
1304            .expect("should add documents");
1305
1306        // Analyze the documents
1307        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        // Analyze the documents again
1314        let results = analyzer.analyze(()).await.unwrap();
1315        assert_eq!(results.len(), 3);
1316
1317        // Remove the documents by directory
1318        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}