Skip to main content

wdl_analysis/
document.rs

1//! Representation of analyzed WDL documents.
2
3use std::borrow::Cow;
4use std::collections::HashMap;
5use std::collections::HashSet;
6use std::collections::hash_map::Entry;
7use std::path::Path;
8use std::sync::Arc;
9
10use arrayvec::ArrayString;
11use indexmap::IndexMap;
12use petgraph::graph::NodeIndex;
13use rowan::GreenNode;
14use rowan::TextRange;
15use rowan::TextSize;
16use url::Url;
17use uuid::Uuid;
18use wdl_ast::Ast;
19use wdl_ast::AstNode;
20use wdl_ast::Diagnostic;
21use wdl_ast::Severity;
22use wdl_ast::Span;
23use wdl_ast::SupportedVersion;
24use wdl_ast::SyntaxNode;
25
26use crate::config::Config;
27use crate::diagnostics::Context;
28use crate::diagnostics::no_common_type;
29use crate::diagnostics::unused_import;
30use crate::graph::DocumentGraph;
31use crate::graph::ParseState;
32use crate::types::CallType;
33use crate::types::Optional;
34use crate::types::Type;
35
36pub mod v1;
37
38/// The `task` variable name available in task command sections and outputs in
39/// WDL 1.2.
40pub const TASK_VAR_NAME: &str = "task";
41
42/// Represents a namespace introduced by an import.
43#[derive(Debug)]
44pub struct Namespace {
45    /// The span of the import that introduced the namespace.
46    span: Span,
47    /// The URI of the imported document that introduced the namespace.
48    source: Arc<Url>,
49    /// The namespace's document.
50    document: Document,
51    /// Whether or not the namespace is used (i.e. referenced) in the document.
52    used: bool,
53    /// Whether or not the namespace is excepted from the "unused import"
54    /// diagnostic.
55    excepted: bool,
56}
57
58impl Namespace {
59    /// Gets the span of the import that introduced the namespace.
60    pub fn span(&self) -> Span {
61        self.span
62    }
63
64    /// Gets the URI of the imported document that introduced the namespace.
65    pub fn source(&self) -> &Arc<Url> {
66        &self.source
67    }
68
69    /// Gets the imported document.
70    pub fn document(&self) -> &Document {
71        &self.document
72    }
73}
74
75/// Represents a struct in a document.
76#[derive(Debug, Clone)]
77pub struct Struct {
78    /// The name of the struct.
79    name: String,
80    /// The span that introduced the struct.
81    ///
82    /// This is either the name of a struct definition (local) or an import's
83    /// URI or alias (imported).
84    name_span: Span,
85    /// The offset of the CST node from the start of the document.
86    ///
87    /// This is used to adjust diagnostics resulting from traversing the struct
88    /// node as if it were the root of the CST.
89    offset: usize,
90    /// Stores the CST node of the struct.
91    ///
92    /// This is used to calculate type equivalence for imports.
93    node: rowan::GreenNode,
94    /// The namespace that defines the struct.
95    ///
96    /// This is `Some` only for imported structs.
97    namespace: Option<String>,
98    /// The type of the struct.
99    ///
100    /// Initially this is `None` until a type check occurs.
101    ty: Option<Type>,
102}
103
104impl Struct {
105    /// Gets the name of the struct.
106    pub fn name(&self) -> &str {
107        &self.name
108    }
109
110    /// Gets the span of the name.
111    pub fn name_span(&self) -> Span {
112        self.name_span
113    }
114
115    /// Gets the offset of the struct
116    pub fn offset(&self) -> usize {
117        self.offset
118    }
119
120    /// Gets the node of the struct.
121    pub fn node(&self) -> &rowan::GreenNode {
122        &self.node
123    }
124
125    /// Gets the namespace that defines this struct.
126    ///
127    /// Returns `None` for structs defined in the containing document or `Some`
128    /// for a struct introduced by an import.
129    pub fn namespace(&self) -> Option<&str> {
130        self.namespace.as_deref()
131    }
132
133    /// Gets the type of the struct.
134    ///
135    /// A value of `None` indicates that the type could not be determined for
136    /// the struct; this may happen if the struct definition is recursive.
137    pub fn ty(&self) -> Option<&Type> {
138        self.ty.as_ref()
139    }
140}
141
142/// Represents an enum in a document.
143#[derive(Debug, Clone)]
144pub struct Enum {
145    /// The name of the enum.
146    name: String,
147    /// The span that introduced the enum.
148    ///
149    /// This is either the name of an enum definition (local) or an import's
150    /// URI or alias (imported).
151    name_span: Span,
152    /// The offset of the CST node from the start of the document.
153    ///
154    /// This is used to adjust diagnostics resulting from traversing the enum
155    /// node as if it were the root of the CST.
156    offset: usize,
157    /// Stores the CST node of the enum.
158    ///
159    /// This is used to calculate type equivalence for imports and can be
160    /// reconstructed into an AST node to access choice expressions.
161    node: rowan::GreenNode,
162    /// The namespace that defines the enum.
163    ///
164    /// This is `Some` only for imported enums.
165    namespace: Option<String>,
166    /// The type of the enum.
167    ///
168    /// Initially this is `None` until a type check/coercion occurs.
169    ty: Option<Type>,
170}
171
172impl Enum {
173    /// Gets the name of the enum.
174    pub fn name(&self) -> &str {
175        &self.name
176    }
177
178    /// Gets the span of the name.
179    pub fn name_span(&self) -> Span {
180        self.name_span
181    }
182
183    /// Gets the offset of the enum.
184    pub fn offset(&self) -> usize {
185        self.offset
186    }
187
188    /// Gets the green node of the enum.
189    pub fn node(&self) -> &rowan::GreenNode {
190        &self.node
191    }
192
193    /// Reconstructs the AST definition from the stored green node.
194    ///
195    /// This provides access to choice expressions and other AST details.
196    pub fn definition(&self) -> wdl_ast::v1::EnumDefinition {
197        wdl_ast::v1::EnumDefinition::cast(wdl_ast::SyntaxNode::new_root(self.node.clone()))
198            .expect("stored node should be a valid enum definition")
199    }
200
201    /// Gets the namespace that defines this enum.
202    pub fn namespace(&self) -> Option<&str> {
203        self.namespace.as_deref()
204    }
205
206    /// Gets the type of the enum.
207    pub fn ty(&self) -> Option<&Type> {
208        self.ty.as_ref()
209    }
210}
211
212/// Represents information about a name in a scope.
213#[derive(Debug, Clone)]
214pub struct Name {
215    /// The span of the name.
216    span: Span,
217    /// The type of the name.
218    ty: Type,
219}
220
221impl Name {
222    /// Gets the span of the name.
223    pub fn span(&self) -> Span {
224        self.span
225    }
226
227    /// Gets the type of the name.
228    pub fn ty(&self) -> &Type {
229        &self.ty
230    }
231}
232
233/// Represents an index of a scope in a collection of scopes.
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
235pub struct ScopeIndex(usize);
236
237/// Represents a scope in a WDL document.
238#[derive(Debug)]
239pub struct Scope {
240    /// The index of the parent scope.
241    ///
242    /// This is `None` for task and workflow scopes.
243    parent: Option<ScopeIndex>,
244    /// The span in the document where the names of the scope are visible.
245    span: Span,
246    /// The map of names in scope to their span and types.
247    names: IndexMap<String, Name>,
248}
249
250impl Scope {
251    /// Creates a new scope given the parent scope and span.
252    fn new(parent: Option<ScopeIndex>, span: Span) -> Self {
253        Self {
254            parent,
255            span,
256            names: Default::default(),
257        }
258    }
259
260    /// Inserts a name into the scope.
261    pub fn insert(&mut self, name: impl Into<String>, span: Span, ty: Type) {
262        self.names.insert(name.into(), Name { span, ty });
263    }
264}
265
266/// Represents a reference to a scope.
267#[derive(Debug, Clone, Copy)]
268pub struct ScopeRef<'a> {
269    /// The reference to the scopes collection.
270    scopes: &'a [Scope],
271    /// The index of the scope in the collection.
272    index: ScopeIndex,
273}
274
275impl<'a> ScopeRef<'a> {
276    /// Creates a new scope reference given the scope index.
277    fn new(scopes: &'a [Scope], index: ScopeIndex) -> Self {
278        Self { scopes, index }
279    }
280
281    /// Gets the span of the scope.
282    pub fn span(&self) -> Span {
283        self.scopes[self.index.0].span
284    }
285
286    /// Gets the parent scope.
287    ///
288    /// Returns `None` if there is no parent scope.
289    pub fn parent(&self) -> Option<Self> {
290        self.scopes[self.index.0].parent.map(|p| Self {
291            scopes: self.scopes,
292            index: p,
293        })
294    }
295
296    /// Gets all of the names available at this scope.
297    pub fn names(&self) -> impl Iterator<Item = (&str, &Name)> + use<'_> {
298        self.scopes[self.index.0]
299            .names
300            .iter()
301            .map(|(name, n)| (name.as_str(), n))
302    }
303
304    /// Gets a name local to this scope.
305    ///
306    /// Returns `None` if a name local to this scope was not found.
307    pub fn local(&self, name: &str) -> Option<&Name> {
308        self.scopes[self.index.0].names.get(name)
309    }
310
311    /// Lookups a name in the scope.
312    ///
313    /// Returns `None` if the name is not available in the scope.
314    pub fn lookup(&self, name: &str) -> Option<&Name> {
315        let mut current = Some(self.index);
316
317        while let Some(index) = current {
318            if let Some(name) = self.scopes[index.0].names.get(name) {
319                return Some(name);
320            }
321
322            current = self.scopes[index.0].parent;
323        }
324
325        None
326    }
327}
328
329/// Represents a mutable reference to a scope.
330#[derive(Debug)]
331struct ScopeRefMut<'a> {
332    /// The reference to all scopes.
333    scopes: &'a mut [Scope],
334    /// The index to the scope.
335    index: ScopeIndex,
336}
337
338impl<'a> ScopeRefMut<'a> {
339    /// Creates a new mutable scope reference given the scope index.
340    fn new(scopes: &'a mut [Scope], index: ScopeIndex) -> Self {
341        Self { scopes, index }
342    }
343
344    /// Lookups a name in the scope.
345    ///
346    /// Returns `None` if the name is not available in the scope.
347    pub fn lookup(&self, name: &str) -> Option<&Name> {
348        let mut current = Some(self.index);
349
350        while let Some(index) = current {
351            if let Some(name) = self.scopes[index.0].names.get(name) {
352                return Some(name);
353            }
354
355            current = self.scopes[index.0].parent;
356        }
357
358        None
359    }
360
361    /// Inserts a name into the scope.
362    pub fn insert(&mut self, name: impl Into<String>, span: Span, ty: Type) {
363        self.scopes[self.index.0]
364            .names
365            .insert(name.into(), Name { span, ty });
366    }
367
368    /// Converts the mutable scope reference to an immutable scope reference.
369    pub fn as_scope_ref(&'a self) -> ScopeRef<'a> {
370        ScopeRef {
371            scopes: self.scopes,
372            index: self.index,
373        }
374    }
375}
376
377/// A scope union takes the union of names within a number of given scopes and
378/// computes a set of common output names for a presumed parent scope. This is
379/// useful when calculating common elements from, for example, an `if`
380/// statement within a workflow.
381#[derive(Debug)]
382pub struct ScopeUnion<'a> {
383    /// The scope references to process.
384    scope_refs: Vec<(ScopeRef<'a>, bool)>,
385}
386
387impl<'a> ScopeUnion<'a> {
388    /// Creates a new scope union.
389    pub fn new() -> Self {
390        Self {
391            scope_refs: Vec::new(),
392        }
393    }
394
395    /// Adds a scope to the union.
396    pub fn insert(&mut self, scope_ref: ScopeRef<'a>, exhaustive: bool) {
397        self.scope_refs.push((scope_ref, exhaustive));
398    }
399
400    /// Resolves the scope union to names and types that should be accessible
401    /// from the parent scope.
402    ///
403    /// Returns an error if any issues are encountered during resolving.
404    pub fn resolve(self) -> Result<HashMap<String, Name>, Vec<Diagnostic>> {
405        let mut errors = Vec::new();
406        let mut ignored: HashSet<String> = HashSet::new();
407
408        // Gather all declaration names and reconcile types
409        let mut names: HashMap<String, Name> = HashMap::new();
410        for (scope_ref, _) in &self.scope_refs {
411            for (name, info) in scope_ref.names() {
412                if ignored.contains(name) {
413                    continue;
414                }
415
416                match names.entry(name.to_string()) {
417                    Entry::Vacant(entry) => {
418                        entry.insert(info.clone());
419                    }
420                    Entry::Occupied(mut entry) => {
421                        let Some(ty) = entry.get().ty.common_type(&info.ty) else {
422                            errors.push(no_common_type(
423                                &entry.get().ty,
424                                entry.get().span,
425                                &info.ty,
426                                info.span,
427                            ));
428                            names.remove(name);
429                            ignored.insert(name.to_string());
430                            continue;
431                        };
432
433                        entry.get_mut().ty = ty;
434                    }
435                }
436            }
437        }
438
439        // Mark types as optional if not present in all clauses
440        for (scope_ref, _) in &self.scope_refs {
441            for (name, info) in &mut names {
442                if ignored.contains(name) {
443                    continue;
444                }
445
446                // If this name is not in the current clause's scope, mark as optional
447                if scope_ref.local(name).is_none() {
448                    info.ty = info.ty.optional();
449                }
450            }
451        }
452
453        // If there's no `else` clause, mark all types as optional
454        let has_exhaustive = self.scope_refs.iter().any(|(_, exhaustive)| *exhaustive);
455        if !has_exhaustive {
456            for info in names.values_mut() {
457                info.ty = info.ty.optional();
458            }
459        }
460
461        if !errors.is_empty() {
462            return Err(errors);
463        }
464
465        Ok(names)
466    }
467}
468
469/// Represents a task or workflow input.
470#[derive(Debug, Clone, PartialEq, Eq)]
471pub struct Input {
472    /// The type of the input.
473    ty: Type,
474    /// Whether or not the input is required.
475    ///
476    /// A required input is one that has a non-optional type and no default
477    /// expression.
478    required: bool,
479}
480
481impl Input {
482    /// Gets the type of the input.
483    pub fn ty(&self) -> &Type {
484        &self.ty
485    }
486
487    /// Whether or not the input is required.
488    pub fn required(&self) -> bool {
489        self.required
490    }
491}
492
493/// Represents a task or workflow output.
494#[derive(Debug, Clone, PartialEq, Eq)]
495pub struct Output {
496    /// The type of the output.
497    ty: Type,
498    /// The span of the output name.
499    name_span: Span,
500}
501
502impl Output {
503    /// Creates a new output with the given type.
504    pub(crate) fn new(ty: Type, name_span: Span) -> Self {
505        Self { ty, name_span }
506    }
507
508    /// Gets the type of the output.
509    pub fn ty(&self) -> &Type {
510        &self.ty
511    }
512
513    /// Gets the span of output's name.
514    pub fn name_span(&self) -> Span {
515        self.name_span
516    }
517}
518
519/// Represents a task in a document.
520#[derive(Debug)]
521pub struct Task {
522    /// The span of the task name.
523    name_span: Span,
524    /// The name of the task.
525    name: String,
526    /// The span of the task definition.
527    span: Span,
528    /// The scopes contained in the task.
529    ///
530    /// The first scope will always be the task's scope.
531    ///
532    /// The scopes will be in sorted order by span start.
533    scopes: Vec<Scope>,
534    /// The inputs of the task.
535    inputs: Arc<IndexMap<String, Input>>,
536    /// The outputs of the task.
537    outputs: Arc<IndexMap<String, Output>>,
538}
539
540impl Task {
541    /// Gets the name of the task.
542    pub fn name(&self) -> &str {
543        &self.name
544    }
545
546    /// Gets the span of the name.
547    pub fn name_span(&self) -> Span {
548        self.name_span
549    }
550
551    /// Gets the span of the workflow definition.
552    pub fn span(&self) -> Span {
553        self.span
554    }
555
556    /// Gets the scope of the task.
557    pub fn scope(&self) -> ScopeRef<'_> {
558        ScopeRef::new(&self.scopes, ScopeIndex(0))
559    }
560
561    /// Gets the inputs of the task.
562    pub fn inputs(&self) -> &IndexMap<String, Input> {
563        &self.inputs
564    }
565
566    /// Gets the outputs of the task.
567    pub fn outputs(&self) -> &IndexMap<String, Output> {
568        &self.outputs
569    }
570}
571
572/// Represents a workflow in a document.
573#[derive(Debug)]
574pub struct Workflow {
575    /// The span of the workflow name.
576    name_span: Span,
577    /// The name of the workflow.
578    name: String,
579    /// The span of the workflow definition.
580    span: Span,
581    /// The scopes contained in the workflow.
582    ///
583    /// The first scope will always be the workflow's scope.
584    ///
585    /// The scopes will be in sorted order by span start.
586    scopes: Vec<Scope>,
587    /// The inputs of the workflow.
588    inputs: Arc<IndexMap<String, Input>>,
589    /// The outputs of the workflow.
590    outputs: Arc<IndexMap<String, Output>>,
591    /// The calls made by the workflow.
592    calls: HashMap<String, CallType>,
593    /// Whether or not nested inputs are allowed for the workflow.
594    allows_nested_inputs: bool,
595}
596
597impl Workflow {
598    /// Gets the name of the workflow.
599    pub fn name(&self) -> &str {
600        &self.name
601    }
602
603    /// Gets the span of the name.
604    pub fn name_span(&self) -> Span {
605        self.name_span
606    }
607
608    /// Gets the span of the workflow definition.
609    pub fn span(&self) -> Span {
610        self.span
611    }
612
613    /// Gets the scope of the workflow.
614    pub fn scope(&self) -> ScopeRef<'_> {
615        ScopeRef::new(&self.scopes, ScopeIndex(0))
616    }
617
618    /// Gets the inputs of the workflow.
619    pub fn inputs(&self) -> &IndexMap<String, Input> {
620        &self.inputs
621    }
622
623    /// Gets the outputs of the workflow.
624    pub fn outputs(&self) -> &IndexMap<String, Output> {
625        &self.outputs
626    }
627
628    /// Gets the calls made by the workflow.
629    pub fn calls(&self) -> &HashMap<String, CallType> {
630        &self.calls
631    }
632
633    /// Determines if the workflow allows nested inputs.
634    pub fn allows_nested_inputs(&self) -> bool {
635        self.allows_nested_inputs
636    }
637}
638
639/// A callable item.
640#[derive(Debug)]
641pub enum Callable<'a> {
642    /// A workflow.
643    Workflow(&'a Workflow),
644    /// A task.
645    Task(&'a Task),
646}
647
648impl Callable<'_> {
649    /// Get the name of this callable.
650    pub fn name(&self) -> &str {
651        match self {
652            Callable::Workflow(w) => w.name(),
653            Callable::Task(t) => t.name(),
654        }
655    }
656
657    /// Get the [`Span`] of the callable's name.
658    pub fn name_span(&self) -> Span {
659        match self {
660            Callable::Workflow(w) => w.name_span(),
661            Callable::Task(t) => t.name_span(),
662        }
663    }
664
665    /// Get the [`Span`] of the callable's full definition.
666    pub fn span(&self) -> Span {
667        match self {
668            Callable::Workflow(w) => w.span(),
669            Callable::Task(t) => t.span(),
670        }
671    }
672}
673
674/// Represents analysis data about a WDL document.
675#[derive(Debug)]
676pub(crate) struct DocumentData {
677    /// The configuration under which this document was analyzed.
678    config: Config,
679    /// The root CST node of the document.
680    ///
681    /// This is `None` when the document could not be parsed.
682    root: Option<GreenNode>,
683    /// The document identifier.
684    ///
685    /// The identifier changes every time the document is analyzed.
686    id: Arc<String>,
687    /// The URI of the analyzed document.
688    uri: Arc<Url>,
689    /// The version of the document.
690    version: Option<SupportedVersion>,
691    /// The namespaces in the document.
692    namespaces: IndexMap<String, Namespace>,
693    /// The tasks in the document.
694    tasks: IndexMap<String, Task>,
695    /// The singular workflow in the document.
696    workflow: Option<Workflow>,
697    /// The structs in the document.
698    structs: IndexMap<String, Struct>,
699    /// The enums in the document.
700    enums: IndexMap<String, Enum>,
701    /// The diagnostics from parsing.
702    parse_diagnostics: Vec<Diagnostic>,
703    /// The diagnostics from analysis.
704    analysis_diagnostics: Vec<Diagnostic>,
705}
706
707impl DocumentData {
708    /// Constructs a new analysis document data.
709    fn new(
710        config: Config,
711        uri: Arc<Url>,
712        root: Option<GreenNode>,
713        version: Option<SupportedVersion>,
714        diagnostics: Vec<Diagnostic>,
715    ) -> Self {
716        Self {
717            config,
718            root,
719            id: Uuid::new_v4().to_string().into(),
720            uri,
721            version,
722            namespaces: Default::default(),
723            tasks: Default::default(),
724            workflow: Default::default(),
725            structs: Default::default(),
726            enums: Default::default(),
727            parse_diagnostics: diagnostics,
728            analysis_diagnostics: Default::default(),
729        }
730    }
731
732    /// Gets the context of the given name.
733    ///
734    /// The name may be for a namespace, task, workflow, struct, or enum.
735    ///
736    /// Returns `None` if there is no context for the given name.
737    pub fn context(&self, name: &str) -> Option<Context> {
738        // Look through the various data structures for the name
739        if let Some(ns) = self.namespaces.get(name) {
740            Some(Context::Namespace(ns.span()))
741        } else if let Some(task) = self.tasks.get(name) {
742            Some(Context::Task(task.name_span()))
743        } else if let Some(wf) = &self.workflow
744            && wf.name == name
745        {
746            Some(Context::Workflow(wf.name_span()))
747        } else if let Some(s) = self.structs.get(name) {
748            Some(Context::Struct(s.name_span()))
749        } else {
750            // Finally, check the enums and failing that return `None`
751            self.enums.get(name).map(|e| Context::Enum(e.name_span()))
752        }
753    }
754}
755
756/// Represents an analyzed WDL document.
757///
758/// This type is cheaply cloned.
759#[derive(Debug, Clone)]
760pub struct Document {
761    /// The document data for the document.
762    data: Arc<DocumentData>,
763}
764
765impl Document {
766    /// Creates a new default document from a URI.
767    pub(crate) fn default_from_uri(uri: Arc<Url>) -> Self {
768        Self {
769            data: Arc::new(DocumentData::new(
770                Default::default(),
771                uri,
772                None,
773                None,
774                Default::default(),
775            )),
776        }
777    }
778
779    /// Creates a new analyzed document from a document graph node.
780    pub(crate) fn from_graph_node(
781        config: &Config,
782        graph: &DocumentGraph,
783        index: NodeIndex,
784    ) -> Self {
785        let node = graph.get(index);
786
787        let (wdl_version, diagnostics) = match node.parse_state() {
788            ParseState::NotParsed => panic!("node should have been parsed"),
789            ParseState::Error(_) => return Self::default_from_uri(node.uri().clone()),
790            ParseState::Parsed {
791                wdl_version,
792                diagnostics,
793                ..
794            } => (wdl_version, diagnostics),
795        };
796
797        let root = node.root().expect("node should have been parsed");
798        let (config, wdl_version) = match (root.version_statement(), wdl_version) {
799            (Some(stmt), Some(wdl_version)) => (
800                config.with_diagnostics_config(
801                    config.diagnostics_config().excepted_for_node(stmt.inner()),
802                ),
803                *wdl_version,
804            ),
805            _ => {
806                // Don't process a document with a missing version statement or an unsupported
807                // version unless a fallback version is configured
808                return Self {
809                    data: Arc::new(DocumentData::new(
810                        config.clone(),
811                        node.uri().clone(),
812                        Some(root.inner().green().into()),
813                        None,
814                        diagnostics.to_vec(),
815                    )),
816                };
817            }
818        };
819
820        let mut data = DocumentData::new(
821            config.clone(),
822            node.uri().clone(),
823            Some(root.inner().green().into()),
824            Some(wdl_version),
825            diagnostics.to_vec(),
826        );
827        match root.ast_with_version_fallback(config.fallback_version()) {
828            Ast::Unsupported => {}
829            Ast::V1(ast) => v1::populate_document(&mut data, &config, graph, index, &ast),
830        }
831
832        // Check for unused imports
833        if let Some(severity) = config.diagnostics_config().unused_import {
834            let DocumentData {
835                namespaces,
836                analysis_diagnostics,
837                ..
838            } = &mut data;
839
840            analysis_diagnostics.extend(
841                namespaces
842                    .iter()
843                    .filter(|(_, ns)| !ns.used && !ns.excepted)
844                    .map(|(name, ns)| unused_import(name, ns.span()).with_severity(severity)),
845            );
846        }
847
848        Self {
849            data: Arc::new(data),
850        }
851    }
852
853    /// Gets the analysis configuration.
854    pub fn config(&self) -> &Config {
855        &self.data.config
856    }
857
858    /// Gets the root AST document node.
859    ///
860    /// # Panics
861    ///
862    /// Panics if the document was not parsed.
863    pub fn root(&self) -> wdl_ast::Document {
864        wdl_ast::Document::cast(SyntaxNode::new_root(
865            self.data.root.clone().expect("should have a root"),
866        ))
867        .expect("should cast")
868    }
869
870    /// Gets the identifier of the document.
871    ///
872    /// This value changes when a document is reanalyzed.
873    pub fn id(&self) -> &Arc<String> {
874        &self.data.id
875    }
876
877    /// Gets the URI of the document.
878    pub fn uri(&self) -> &Arc<Url> {
879        &self.data.uri
880    }
881
882    /// Gets the path to the document.
883    ///
884    /// If the scheme of the document's URI is not `file`, this will return the
885    /// URI as a string. Otherwise, this will attempt to return the path
886    /// relative to the current working directory, or the absolute path
887    /// failing that.
888    pub fn path(&self) -> Cow<'_, str> {
889        if let Ok(path) = self.data.uri.to_file_path() {
890            if let Some(path) = std::env::current_dir()
891                .ok()
892                .and_then(|cwd| path.strip_prefix(cwd).ok().and_then(Path::to_str))
893            {
894                return path.to_string().into();
895            }
896
897            if let Ok(path) = path.into_os_string().into_string() {
898                return path.into();
899            }
900        }
901
902        self.data.uri.as_str().into()
903    }
904
905    /// Computes the `blake3` hash of the document's source text over the
906    /// given span and returns the hex form.
907    ///
908    /// Uses `rowan::SyntaxText::for_each_chunk` so the span's text is never
909    /// materialized as a `String`.
910    ///
911    /// Returns `None` if `span` falls outside the document's source text.
912    pub fn hash_span(&self, span: Span) -> Option<ArrayString<64>> {
913        let text = self.root().inner().text();
914        let text_len = usize::from(text.len());
915        if span.end() > text_len {
916            return None;
917        }
918        let range = TextRange::new(
919            TextSize::new(span.start() as u32),
920            TextSize::new(span.end() as u32),
921        );
922        let slice = text.slice(range);
923        let mut hasher = blake3::Hasher::new();
924        slice.for_each_chunk(|chunk| {
925            hasher.update(chunk.as_bytes());
926        });
927        Some(hasher.finalize().to_hex())
928    }
929
930    /// Gets the supported version of the document.
931    ///
932    /// Returns `None` if the document could not be parsed or contains an
933    /// unsupported version.
934    pub fn version(&self) -> Option<SupportedVersion> {
935        self.data.version
936    }
937
938    /// Gets the namespaces in the document.
939    pub fn namespaces(&self) -> impl Iterator<Item = (&str, &Namespace)> {
940        self.data.namespaces.iter().map(|(n, ns)| (n.as_str(), ns))
941    }
942
943    /// Gets a namespace in the document by name.
944    pub fn namespace(&self, name: &str) -> Option<&Namespace> {
945        self.data.namespaces.get(name)
946    }
947
948    /// Gets the tasks in the document.
949    pub fn tasks(&self) -> impl Iterator<Item = &Task> {
950        self.data.tasks.iter().map(|(_, t)| t)
951    }
952
953    /// Gets a task in the document by name.
954    pub fn task_by_name(&self, name: &str) -> Option<&Task> {
955        self.data.tasks.get(name)
956    }
957
958    /// Gets a workflow in the document.
959    ///
960    /// Returns `None` if the document did not contain a workflow.
961    pub fn workflow(&self) -> Option<&Workflow> {
962        self.data.workflow.as_ref()
963    }
964
965    /// Gets a [`Callable`] in the document by name.
966    ///
967    /// Returns `None` if the document did not contain a callable definition
968    /// with the given name.
969    pub fn callable_by_name(&self, name: &str) -> Option<Callable<'_>> {
970        if let Some(workflow) = self.workflow()
971            && workflow.name() == name
972        {
973            return Some(Callable::Workflow(workflow));
974        }
975
976        if let Some(task) = self.task_by_name(name) {
977            return Some(Callable::Task(task));
978        }
979
980        None
981    }
982
983    /// Gets the structs in the document.
984    pub fn structs(&self) -> impl Iterator<Item = (&str, &Struct)> {
985        self.data.structs.iter().map(|(n, s)| (n.as_str(), s))
986    }
987
988    /// Gets a struct in the document by name.
989    pub fn struct_by_name(&self, name: &str) -> Option<&Struct> {
990        self.data.structs.get(name)
991    }
992
993    /// Gets the enums in the document.
994    pub fn enums(&self) -> impl Iterator<Item = (&str, &Enum)> {
995        self.data.enums.iter().map(|(n, e)| (n.as_str(), e))
996    }
997
998    /// Gets an enum in the document by name.
999    pub fn enum_by_name(&self, name: &str) -> Option<&Enum> {
1000        self.data.enums.get(name)
1001    }
1002
1003    /// Gets the custom type by name.
1004    pub fn get_custom_type(&self, name: &str) -> Option<Type> {
1005        if let Some(s) = self.struct_by_name(name) {
1006            return s.ty().cloned();
1007        }
1008
1009        if let Some(s) = self.enum_by_name(name) {
1010            return s.ty().cloned();
1011        }
1012
1013        None
1014    }
1015
1016    /// Gets a cache key for an enum choice lookup.
1017    pub fn get_choice_cache_key(
1018        &self,
1019        name: &str,
1020        choice: &str,
1021    ) -> Option<crate::types::EnumChoiceCacheKey> {
1022        let (enum_index, _, r#enum) = self.data.enums.get_full(name)?;
1023        let enum_ty = r#enum.ty()?.as_enum()?;
1024        let choice_index = enum_ty.choices().iter().position(|v| v == choice)?;
1025        Some(crate::types::EnumChoiceCacheKey::new(
1026            enum_index,
1027            choice_index,
1028        ))
1029    }
1030
1031    /// Gets the parse diagnostics for the document.
1032    pub fn parse_diagnostics(&self) -> &[Diagnostic] {
1033        &self.data.parse_diagnostics
1034    }
1035
1036    /// Gets the analysis diagnostics for the document.
1037    pub fn analysis_diagnostics(&self) -> &[Diagnostic] {
1038        &self.data.analysis_diagnostics
1039    }
1040
1041    /// Gets all diagnostics for the document (both from parsing and analysis).
1042    pub fn diagnostics(&self) -> impl Iterator<Item = &Diagnostic> {
1043        self.data
1044            .parse_diagnostics
1045            .iter()
1046            .chain(self.data.analysis_diagnostics.iter())
1047    }
1048
1049    /// Sorts the diagnostics for the document.
1050    ///
1051    /// # Panics
1052    ///
1053    /// Panics if there is more than one reference to the document.
1054    pub fn sort_diagnostics(&mut self) -> Self {
1055        let data = &mut self.data;
1056        let inner = Arc::get_mut(data).expect("should only have one reference");
1057        inner.parse_diagnostics.sort();
1058        inner.analysis_diagnostics.sort();
1059        Self { data: data.clone() }
1060    }
1061
1062    /// Extends the analysis diagnostics for the document.
1063    ///
1064    /// # Panics
1065    ///
1066    /// Panics if there is more than one reference to the document.
1067    pub fn extend_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) -> Self {
1068        let data = &mut self.data;
1069        let inner = Arc::get_mut(data).expect("should only have one reference");
1070        inner.analysis_diagnostics.extend(diagnostics);
1071        Self { data: data.clone() }
1072    }
1073
1074    /// Finds a scope based on a position within the document.
1075    pub fn find_scope_by_position(&self, position: usize) -> Option<ScopeRef<'_>> {
1076        /// Finds a scope within a collection of sorted scopes by position.
1077        fn find_scope(scopes: &[Scope], position: usize) -> Option<ScopeRef<'_>> {
1078            let mut index = match scopes.binary_search_by_key(&position, |s| s.span.start()) {
1079                Ok(index) => index,
1080                Err(index) => {
1081                    // This indicates that we couldn't find a match and the match would go _before_
1082                    // the first scope, so there is no containing scope.
1083                    if index == 0 {
1084                        return None;
1085                    }
1086
1087                    index - 1
1088                }
1089            };
1090
1091            // We now have the index to start looking up the list of scopes
1092            // We walk up the list to try to find a span that contains the position
1093            loop {
1094                let scope = &scopes[index];
1095                if scope.span.contains(position) {
1096                    return Some(ScopeRef::new(scopes, ScopeIndex(index)));
1097                }
1098
1099                if index == 0 {
1100                    return None;
1101                }
1102
1103                index -= 1;
1104            }
1105        }
1106
1107        // Check to see if the position is contained in the workflow
1108        if let Some(workflow) = &self.data.workflow
1109            && workflow.scope().span().contains(position)
1110        {
1111            return find_scope(&workflow.scopes, position);
1112        }
1113
1114        // Search for a task that might contain the position
1115        let task = match self
1116            .data
1117            .tasks
1118            .binary_search_by_key(&position, |_, t| t.scope().span().start())
1119        {
1120            Ok(index) => &self.data.tasks[index],
1121            Err(index) => {
1122                // This indicates that we couldn't find a match and the match would go _before_
1123                // the first task, so there is no containing task.
1124                if index == 0 {
1125                    return None;
1126                }
1127
1128                &self.data.tasks[index - 1]
1129            }
1130        };
1131
1132        if task.scope().span().contains(position) {
1133            return find_scope(&task.scopes, position);
1134        }
1135
1136        None
1137    }
1138
1139    /// Determines if the document, or any documents transitively imported by
1140    /// this document, has errors.
1141    ///
1142    /// Returns `true` if the document, or one of its transitive imports, has at
1143    /// least one error diagnostic.
1144    ///
1145    /// Returns `false` if the document, and all of its transitive imports, have
1146    /// no error diagnostics.
1147    pub fn has_errors(&self) -> bool {
1148        // Check this document for errors
1149        if self.diagnostics().any(|d| d.severity() == Severity::Error) {
1150            return true;
1151        }
1152
1153        // Check every imported document for errors
1154        for (_, ns) in self.namespaces() {
1155            if ns.document.has_errors() {
1156                return true;
1157            }
1158        }
1159
1160        false
1161    }
1162
1163    /// Visits the document with a pre-order traversal using the provided
1164    /// visitor to visit each element in the document.
1165    pub fn visit<V: crate::Visitor>(&self, diagnostics: &mut crate::Diagnostics, visitor: &mut V) {
1166        crate::visit(self, diagnostics, visitor)
1167    }
1168}