Skip to main content

wdl_analysis/document/
cache.rs

1//! Caching layer for WDL document analysis.
2
3mod hash;
4#[cfg(test)]
5mod tests;
6
7use std::cmp::Ordering;
8use std::hash::Hash;
9use std::sync::Arc;
10
11use indexmap::IndexMap;
12use petgraph::prelude::DiGraphMap;
13use sha2::Digest;
14use sha2::Sha256;
15use url::Url;
16use wdl_ast::TreeNode;
17use wdl_ast::v1::Ast;
18use wdl_ast::v1::DocumentItem;
19use wdl_ast::v1::EnumDefinition;
20use wdl_ast::v1::ImportStatement;
21use wdl_ast::v1::StructDefinition;
22use wdl_grammar::Diagnostic;
23use wdl_grammar::Span;
24use wdl_grammar::SyntaxKind;
25
26use crate::AppliedEdit;
27use crate::Diagnostics;
28use crate::Exceptable;
29use crate::document::Enum;
30use crate::document::ImportedEnum;
31use crate::document::ImportedStruct;
32use crate::document::ImportedTask;
33use crate::document::ImportedWorkflow;
34use crate::document::Input;
35use crate::document::Namespace;
36use crate::document::Output;
37use crate::document::Struct;
38use crate::document::Task;
39use crate::document::Workflow;
40use crate::document::cache::hash::HashableCallable;
41use crate::document::cache::hash::HashableItem;
42use crate::types::Type;
43
44/// The kind of an [`LocalItem`].
45#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
46#[repr(u8)]
47pub enum ItemKind {
48    /// A struct.
49    Struct,
50    /// An enum.
51    Enum,
52    /// A task.
53    Task,
54    /// A workflow.
55    Workflow,
56    /// An import.
57    Import,
58}
59
60/// The import merges its contents directly into the document's scope.
61#[derive(Debug, Clone, Default, PartialEq)]
62pub(crate) struct MergingImport {
63    /// Tasks imported via wildcard or selected import.
64    pub(in crate::document) imported_tasks: IndexMap<String, ImportedTask>,
65    /// Workflows imported via wildcard or selected import.
66    pub(in crate::document) imported_workflows: IndexMap<String, ImportedWorkflow>,
67    /// Structs imported via wildcard or selected import.
68    ///
69    /// NOTE: While this is separated from the [`Document`], imported
70    /// structs/enums are copied into the document's scope and should
71    /// be treated as though they were defined in the document.
72    pub(in crate::document) imported_structs: IndexMap<String, ImportedStruct>,
73    /// Enums imported via wildcard or selected import.
74    pub(in crate::document) imported_enums: IndexMap<String, ImportedEnum>,
75}
76
77impl MergingImport {
78    /// Gets all of the items that this import brings into scope.
79    pub(crate) fn items(&self) -> impl Iterator<Item = ImportedItem<'_>> {
80        self.imported_tasks
81            .values()
82            .map(ImportedItem::Task)
83            .chain(self.imported_workflows.values().map(ImportedItem::Workflow))
84            .chain(self.imported_structs.values().map(ImportedItem::Struct))
85            .chain(self.imported_enums.values().map(ImportedItem::Enum))
86    }
87}
88
89/// An import in a document.
90#[derive(Debug, Clone, PartialEq)]
91pub(crate) enum Import {
92    /// The import introduces a new namespace in the document.
93    Namespace(Namespace),
94    /// The import merges its contents directly into the document's scope.
95    Merging(MergingImport),
96}
97
98impl Import {
99    /// Get the [`MergingImport`] contents of the import if it's a merging
100    /// import.
101    pub(crate) fn merging(&self) -> Option<&MergingImport> {
102        match self {
103            Import::Merging(i) => Some(i),
104            _ => None,
105        }
106    }
107
108    /// Get the namespace of the import, if it has one.
109    pub(crate) fn namespace(&self) -> Option<&Namespace> {
110        match self {
111            Import::Namespace(n) => Some(n),
112            _ => None,
113        }
114    }
115
116    /// Get a mutable reference to the namespace of the import, if it has one.
117    fn namespace_mut(&mut self) -> Option<&mut Namespace> {
118        match self {
119            Import::Namespace(n) => Some(n),
120            _ => None,
121        }
122    }
123
124    /// Gets all of the structs introduced by this import.
125    fn structs(&self) -> impl Iterator<Item = &ImportedStruct> {
126        match self {
127            Import::Namespace(n) => n.imported_structs.values(),
128            Import::Merging(m) => m.imported_structs.values(),
129        }
130    }
131
132    /// Add a struct to this import.
133    pub(in crate::document) fn add_struct(&mut self, s: ImportedStruct) {
134        let _ = match self {
135            Import::Namespace(n) => n.imported_structs.insert(s.local_name.clone(), s),
136            Import::Merging(m) => m.imported_structs.insert(s.local_name.clone(), s),
137        };
138    }
139
140    /// Gets all of the enums introduced by this import.
141    fn enums(&self) -> impl Iterator<Item = &ImportedEnum> {
142        match self {
143            Import::Namespace(n) => n.imported_enums.values(),
144            Import::Merging(m) => m.imported_enums.values(),
145        }
146    }
147
148    /// Add an enum to this import.
149    pub(in crate::document) fn add_enum(&mut self, e: ImportedEnum) {
150        let _ = match self {
151            Import::Namespace(n) => n.imported_enums.insert(e.local_name.clone(), e),
152            Import::Merging(m) => m.imported_enums.insert(e.local_name.clone(), e),
153        };
154    }
155}
156
157/// A reference to an externally defined item.
158pub(crate) enum ImportedItem<'a> {
159    /// A struct.
160    Struct(&'a ImportedStruct),
161    /// An enum.
162    Enum(&'a ImportedEnum),
163    /// A task.
164    Task(&'a ImportedTask),
165    /// A workflow.
166    Workflow(&'a ImportedWorkflow),
167}
168
169impl<'a> ImportedItem<'a> {
170    /// Gets the aliased name of the imported item.
171    fn aliased_name(&self) -> &'a str {
172        match self {
173            ImportedItem::Struct(s) => &s.local_name,
174            ImportedItem::Enum(e) => &e.local_name,
175            ImportedItem::Task(t) => &t.local_name,
176            ImportedItem::Workflow(w) => &w.local_name,
177        }
178    }
179}
180
181/// An item in the current document's scope.
182#[derive(Copy, Clone, Debug)]
183pub enum MaybeImported<Local, Imported> {
184    /// The item is locally defined.
185    Local(Local),
186    /// The item was imported from another document.
187    Imported(Imported),
188}
189
190impl<L, I> MaybeImported<L, I> {
191    /// Returns true if the item was imported.
192    pub fn is_imported(&self) -> bool {
193        matches!(self, Self::Imported(_))
194    }
195
196    /// Returns the imported item.
197    ///
198    /// # Panics
199    ///
200    /// This will panic if the item was locally defined.
201    pub fn expect_imported(self) -> I {
202        match self {
203            Self::Imported(i) => i,
204            Self::Local(_) => panic!("expected an imported item"),
205        }
206    }
207
208    /// Returns the contained local item.
209    ///
210    /// # Panics
211    ///
212    /// This will panic if the item was imported.
213    pub fn expect_local(self) -> L {
214        match self {
215            Self::Local(l) => l,
216            Self::Imported(_) => panic!("expected a locally defined item"),
217        }
218    }
219}
220
221/// A reference to an item in the document's scope.
222pub(in crate::document) type Item<'a> = MaybeImported<CachedItemRef<'a>, ImportedItem<'a>>;
223
224/// A reference to a workflow in the document's scope.
225pub type WorkflowRef<'a> = MaybeImported<&'a Workflow, &'a ImportedWorkflow>;
226impl<'a> WorkflowRef<'a> {
227    /// Gets the name of the workflow.
228    pub fn name(&self) -> &'a str {
229        match self {
230            WorkflowRef::Local(w) => w.name(),
231            WorkflowRef::Imported(i) => &i.local_name,
232        }
233    }
234
235    /// Gets the span of the name.
236    pub fn name_span(&self) -> Span {
237        match self {
238            WorkflowRef::Local(w) => w.name_span(),
239            WorkflowRef::Imported(i) => i.span,
240        }
241    }
242
243    /// The inputs of the workflow.
244    pub fn inputs(&self) -> Arc<IndexMap<String, Input>> {
245        match self {
246            WorkflowRef::Local(w) => Arc::clone(&w.inputs),
247            WorkflowRef::Imported(i) => Arc::clone(&i.inputs),
248        }
249    }
250
251    /// The outputs of the workflow.
252    pub fn outputs(&self) -> Arc<IndexMap<String, Output>> {
253        match self {
254            WorkflowRef::Local(w) => Arc::clone(&w.outputs),
255            WorkflowRef::Imported(i) => Arc::clone(&i.outputs),
256        }
257    }
258
259    /// Gets the source of the workflow, if it was imported.
260    pub fn source(&self) -> Option<Arc<Url>> {
261        match self {
262            WorkflowRef::Local(_) => None,
263            WorkflowRef::Imported(i) => Some(i.source()),
264        }
265    }
266}
267
268/// A reference to a task in the document's scope.
269pub type TaskRef<'a> = MaybeImported<&'a Task, &'a ImportedTask>;
270impl<'a> TaskRef<'a> {
271    /// Gets the name of the task.
272    pub fn name(&self) -> &'a str {
273        match self {
274            TaskRef::Local(t) => t.name(),
275            TaskRef::Imported(i) => &i.local_name,
276        }
277    }
278
279    /// Gets the span of the name.
280    pub fn name_span(&self) -> Span {
281        match self {
282            TaskRef::Local(t) => t.name_span(),
283            TaskRef::Imported(i) => i.span,
284        }
285    }
286
287    /// The inputs of the task.
288    pub fn inputs(&self) -> Arc<IndexMap<String, Input>> {
289        match self {
290            TaskRef::Local(t) => Arc::clone(&t.inputs),
291            TaskRef::Imported(i) => Arc::clone(&i.inputs),
292        }
293    }
294
295    /// The outputs of the task.
296    pub fn outputs(&self) -> Arc<IndexMap<String, Output>> {
297        match self {
298            TaskRef::Local(t) => Arc::clone(&t.outputs),
299            TaskRef::Imported(i) => Arc::clone(&i.outputs),
300        }
301    }
302
303    /// Gets the source of the task, if it was imported.
304    pub fn source(&self) -> Option<Arc<Url>> {
305        match self {
306            TaskRef::Local(_) => None,
307            TaskRef::Imported(i) => Some(i.source()),
308        }
309    }
310}
311
312/// A reference to a struct in the document's scope.
313pub type StructRef<'a> = MaybeImported<&'a Struct, &'a ImportedStruct>;
314impl<'a> StructRef<'a> {
315    /// Gets the name of the struct.
316    pub fn name(&self) -> &'a str {
317        match self {
318            StructRef::Local(s) => s.name(),
319            StructRef::Imported(i) => &i.local_name,
320        }
321    }
322
323    /// Gets the span of the name.
324    pub fn name_span(&self) -> Span {
325        match self {
326            StructRef::Local(s) => s.name_span(),
327            StructRef::Imported(i) => i.span,
328        }
329    }
330
331    /// Gets the type of the struct, if it was computed.
332    pub fn ty(&self) -> Option<&'a Type> {
333        match self {
334            StructRef::Local(s) => s.ty(),
335            StructRef::Imported(i) => i.ty(),
336        }
337    }
338
339    /// Gets the source of the struct, if it was imported.
340    pub fn source(&self) -> Option<Arc<Url>> {
341        match self {
342            StructRef::Local(_) => None,
343            StructRef::Imported(i) => Some(i.source()),
344        }
345    }
346
347    /// Reconstructs the AST definition from the stored green node.
348    pub fn definition(&self) -> StructDefinition {
349        match self {
350            StructRef::Local(s) => s.definition(),
351            StructRef::Imported(i) => i.definition(),
352        }
353    }
354
355    /// Gets the offset of the struct in the source document's CST.
356    pub fn offset(&self) -> usize {
357        match self {
358            StructRef::Local(s) => s.offset(),
359            StructRef::Imported(i) => i.offset(),
360        }
361    }
362
363    /// Gets the node of the struct.
364    pub fn node(&self) -> &'a rowan::GreenNode {
365        match self {
366            StructRef::Local(s) => s.node(),
367            StructRef::Imported(i) => i.node(),
368        }
369    }
370}
371
372/// A reference to an enum in the document's scope.
373pub type EnumRef<'a> = MaybeImported<&'a Enum, &'a ImportedEnum>;
374impl<'a> EnumRef<'a> {
375    /// Gets the name of the enum.
376    pub fn name(&self) -> &'a str {
377        match self {
378            EnumRef::Local(e) => e.name(),
379            EnumRef::Imported(i) => &i.local_name,
380        }
381    }
382
383    /// Gets the span of the name.
384    pub fn name_span(&self) -> Span {
385        match self {
386            EnumRef::Local(e) => e.name_span(),
387            EnumRef::Imported(i) => i.span,
388        }
389    }
390
391    /// Gets the type of the enum, if it was computed.
392    pub fn ty(&self) -> Option<&'a Type> {
393        match self {
394            EnumRef::Local(e) => e.ty(),
395            EnumRef::Imported(i) => i.ty(),
396        }
397    }
398
399    /// Gets the source of the enum if it was imported.
400    pub fn source(&self) -> Option<Arc<Url>> {
401        match self {
402            EnumRef::Local(_) => None,
403            EnumRef::Imported(i) => Some(i.source()),
404        }
405    }
406
407    /// Reconstructs the AST definition from the stored green node.
408    pub fn definition(&self) -> EnumDefinition {
409        match self {
410            EnumRef::Local(e) => e.definition(),
411            EnumRef::Imported(i) => i.definition(),
412        }
413    }
414
415    /// Gets the offset of the enum.
416    pub fn offset(&self) -> usize {
417        match self {
418            EnumRef::Local(e) => e.offset(),
419            EnumRef::Imported(i) => i.offset(),
420        }
421    }
422
423    /// Gets the node of the enum.
424    pub fn node(&self) -> &'a rowan::GreenNode {
425        match self {
426            EnumRef::Local(e) => e.node(),
427            EnumRef::Imported(i) => i.node(),
428        }
429    }
430}
431
432impl<'a> Item<'a> {
433    /// Get the name of the item, if it introduces one.
434    fn name(&self) -> Option<&'a str> {
435        match self {
436            Item::Local(i) => i.name(),
437            Item::Imported(i) => Some(i.aliased_name()),
438        }
439    }
440
441    /// Get the [`SignatureHash`] of the item, if it was locally defined.
442    pub fn signature_hash(&self) -> Option<SignatureHash> {
443        match self {
444            Item::Local(i) => Some(i.signature_hash()),
445            Item::Imported(_) => None,
446        }
447    }
448}
449
450/// A hash of an item's signature.
451///
452/// Any changes to the signature of an item will invalidate it and all of its
453/// dependents.
454pub(in crate::document) type SignatureHash = [u8; 32];
455/// A hash of an item's body.
456///
457/// Any change to the body of an item will only invalidate itself.
458pub(in crate::document) type BodyHash = [u8; 32];
459
460/// An analyzed item with an associated [`BodyHash`].
461#[derive(Debug, Clone, Default, PartialEq, Eq)]
462pub struct WithBodyHash<T> {
463    /// The hash of the item's body.
464    pub body_hash: BodyHash,
465    /// The analyzed item.
466    pub item: T,
467}
468
469/// A cached, analyzed document item.
470#[derive(Debug, Clone, PartialEq)]
471pub struct CachedItem<T> {
472    /// The hash for this item's signature.
473    signature_hash: SignatureHash,
474    /// The offset of the item in the document's CST.
475    offset: usize,
476    /// The analyzed item.
477    item: T,
478    /// Diagnostics produced during the analysis of this item.
479    diagnostics: Diagnostics,
480}
481
482impl<T> CachedItem<T> {
483    /// Create a new cached item.
484    pub(in crate::document) fn new(
485        signature_hash: SignatureHash,
486        offset: usize,
487        item: T,
488        diagnostics: Diagnostics,
489    ) -> Self {
490        Self {
491            signature_hash,
492            offset,
493            item,
494            diagnostics,
495        }
496    }
497
498    /// Get the item that this cached item represents.
499    pub fn item(&self) -> &T {
500        &self.item
501    }
502
503    /// Get a mutable reference to the item that this cached item represents.
504    pub fn item_mut(&mut self) -> &mut T {
505        &mut self.item
506    }
507
508    /// Overwrite the diagnostics for this cached item.
509    ///
510    /// NOTE: This expects diagnostics with absolute offsets.
511    pub fn set_diagnostics(&mut self, diagnostics: Diagnostics) {
512        self.diagnostics = diagnostics;
513        self.shift_diagnostic_offsets();
514    }
515
516    /// Adds a diagnostic to this item.
517    ///
518    /// NOTE: This expects diagnostics with absolute offsets.
519    pub(in crate::document) fn add_diagnostic(&mut self, mut diagnostic: Diagnostic) {
520        diagnostic.offset(-(self.offset as isize));
521        self.diagnostics.add(diagnostic);
522    }
523
524    /// See [`Diagnostics::exceptable_add()`]
525    ///
526    /// NOTE: This expects diagnostics with absolute offsets.
527    pub(in crate::document) fn exceptable_add<N: TreeNode + Exceptable>(
528        &mut self,
529        mut diagnostic: Diagnostic,
530        element: &N,
531        exceptable_nodes: &Option<&'static [SyntaxKind]>,
532    ) {
533        diagnostic.offset(-(self.offset as isize));
534        self.diagnostics
535            .exceptable_add(diagnostic, element, exceptable_nodes);
536    }
537
538    /// Reposition the item's diagnostics to be relative to the item's offset,
539    /// rather than the item's absolute offset in the document.
540    fn shift_diagnostic_offsets(&mut self) {
541        for diagnostic in &mut self.diagnostics.diagnostics {
542            diagnostic.offset(-(self.offset as isize))
543        }
544    }
545}
546
547impl CachedItem<Struct> {
548    /// Get the item this `CachedItem` wraps.
549    fn target(&self) -> &Struct {
550        &self.item
551    }
552}
553
554impl CachedItem<Enum> {
555    /// Get the item this `CachedItem` wraps.
556    fn target(&self) -> &Enum {
557        &self.item
558    }
559}
560
561impl<T> CachedItem<WithBodyHash<T>> {
562    /// Get the item this `CachedItem` wraps.
563    fn target(&self) -> &T {
564        &self.item.item
565    }
566}
567
568/// A mutable reference to an item in the cache.
569#[derive(Debug)]
570pub(in crate::document) enum CachedItemRefMut<'a> {
571    /// An analyzed struct.
572    Struct(&'a mut CachedItem<Struct>),
573    /// An analyzed enum.
574    Enum(&'a mut CachedItem<Enum>),
575    /// An analyzed task.
576    Task(&'a mut CachedItem<WithBodyHash<Task>>),
577    /// An analyzed workflow.
578    Workflow(&'a mut CachedItem<WithBodyHash<Workflow>>),
579    /// An analyzed import.
580    Import(&'a mut CachedItem<WithBodyHash<Import>>),
581}
582
583impl CachedItemRefMut<'_> {
584    /// Gets the current CST offset of the item.
585    fn offset(&self) -> usize {
586        match self {
587            Self::Struct(s) => s.offset,
588            Self::Enum(e) => e.offset,
589            Self::Task(t) => t.offset,
590            Self::Workflow(w) => w.offset,
591            Self::Import(i) => i.offset,
592        }
593    }
594
595    /// Gets a mutable reference to the diagnostics of the item.
596    fn diagnostics_mut(&mut self) -> &mut Vec<Diagnostic> {
597        match self {
598            Self::Struct(s) => &mut s.diagnostics.diagnostics,
599            Self::Enum(e) => &mut e.diagnostics.diagnostics,
600            Self::Task(t) => &mut t.diagnostics.diagnostics,
601            Self::Workflow(w) => &mut w.diagnostics.diagnostics,
602            Self::Import(i) => &mut i.diagnostics.diagnostics,
603        }
604    }
605
606    /// Shift the item's diagnostics based on the newly applied edits and the
607    /// new item offset.
608    ///
609    /// When we first store an item in the cache, we shift all of its diagnostic
610    /// spans to be relative to its position in the document. However, edits
611    /// can occur that shift the item around without invalidating it (e.g.,
612    /// adding comments/whitespace).
613    ///
614    /// For example, in the following document:
615    ///
616    /// ```wdl
617    /// version 1.3
618    ///
619    /// task foo {
620    ///     input {
621    ///         String unused_input
622    ///     }
623    ///
624    ///     command <<<>>>
625    /// }
626    /// ```
627    ///
628    /// If we make edits like:
629    ///
630    /// ```wdl
631    /// version 1.3
632    ///
633    /// # Here's a comment that shifts the entire task down
634    /// task foo {
635    ///     # Woah! Here's a bunch of comments and whitespace
636    ///
637    ///     # This should shift the diagnostics around a lot!
638    ///     input {
639    ///         String unused_input
640    ///     }
641    ///
642    ///     command <<<>>>
643    /// }
644    /// ```
645    ///
646    /// `foo` doesn't get invalidated. Instead, we're able to recalculate the
647    /// new positions of the diagnostics based on the newly applied edits.
648    fn shift_existing_diagnostics(&mut self, edits: &[AppliedEdit], new_item_offset: usize) {
649        /// Shift an absolutely position span based on the given `edits`.
650        fn shift_absolute_span(span: Span, edits: &[AppliedEdit]) -> Span {
651            let mut start = span.start();
652            let mut end = span.end();
653            for edit in edits {
654                let edit_start = edit.range.start;
655                let edit_end = edit.range.end;
656                let replacement_end = edit_start + edit.replacement_length;
657                let edit_diff = edit.replacement_length as isize - edit.range.len() as isize;
658
659                start = if start < edit_start {
660                    start
661                } else if start <= edit_end {
662                    replacement_end
663                } else {
664                    start.saturating_add_signed(edit_diff)
665                };
666
667                end = if end < edit_start {
668                    end
669                } else if end <= edit_end {
670                    replacement_end
671                } else {
672                    end.saturating_add_signed(edit_diff)
673                };
674            }
675            Span::new(start, end - start)
676        }
677
678        if edits.is_empty() {
679            // Nothing to do, might be from a full source replacement
680            return;
681        }
682
683        let original_item_offset = self.offset();
684        for diagnostic in self.diagnostics_mut() {
685            for label in diagnostic.labels_mut() {
686                let start_absolute = original_item_offset + label.span().start();
687                let end_absolute = original_item_offset + label.span().end();
688                let new_span = shift_absolute_span(
689                    Span::new(start_absolute, end_absolute - start_absolute),
690                    edits,
691                );
692
693                // Shrink it back to be relative to the item's offset
694                let new_relative_start = new_span.start().saturating_sub(new_item_offset);
695                label.set_span(Span::new(new_relative_start, new_span.len()));
696            }
697        }
698
699        // Shift the spans of the items themselves
700        match self {
701            Self::Struct(s) => {
702                let Struct {
703                    name: _,
704                    name_span,
705                    offset: _,
706                    node: _,
707                    ty: _,
708                } = &mut s.item;
709
710                *name_span = shift_absolute_span(*name_span, edits)
711            }
712            Self::Enum(e) => {
713                let Enum {
714                    name: _,
715                    name_span,
716                    offset: _,
717                    node: _,
718                    ty: _,
719                } = &mut e.item;
720
721                *name_span = shift_absolute_span(*name_span, edits)
722            }
723            Self::Task(t) => {
724                let Task {
725                    name: _,
726                    name_span,
727                    span,
728                    scopes,
729                    inputs: _,
730                    outputs,
731                } = &mut t.item.item;
732
733                *name_span = shift_absolute_span(*name_span, edits);
734                *span = shift_absolute_span(*span, edits);
735                for scope in scopes {
736                    scope.span = shift_absolute_span(scope.span, edits);
737                    for name in scope.names.values_mut() {
738                        name.span = shift_absolute_span(name.span, edits);
739                    }
740                }
741                for output in Arc::make_mut(outputs).values_mut() {
742                    output.name_span = shift_absolute_span(output.name_span, edits);
743                }
744            }
745            Self::Workflow(wf) => {
746                let Workflow {
747                    name: _,
748                    name_span,
749                    span,
750                    scopes,
751                    inputs: _,
752                    outputs,
753                    allows_nested_inputs: _,
754                    calls: _,
755                } = &mut wf.item.item;
756
757                *name_span = shift_absolute_span(*name_span, edits);
758                *span = shift_absolute_span(*span, edits);
759                for scope in scopes {
760                    scope.span = shift_absolute_span(scope.span, edits);
761                    for name in scope.names.values_mut() {
762                        name.span = shift_absolute_span(name.span, edits);
763                    }
764                }
765                for output in Arc::make_mut(outputs).values_mut() {
766                    output.name_span = shift_absolute_span(output.name_span, edits);
767                }
768            }
769            Self::Import(i) => match &mut i.item.item {
770                Import::Namespace(n) => {
771                    let Namespace {
772                        name: _,
773                        span,
774                        source: _,
775                        document: _,
776                        used: _,
777                        imported_structs,
778                        imported_enums,
779                    } = n;
780
781                    *span = shift_absolute_span(*span, edits);
782                    for s in imported_structs.values_mut() {
783                        s.span = shift_absolute_span(s.span, edits);
784                    }
785                    for e in imported_enums.values_mut() {
786                        e.span = shift_absolute_span(e.span, edits);
787                    }
788                }
789                Import::Merging(m) => {
790                    let MergingImport {
791                        imported_tasks,
792                        imported_workflows,
793                        imported_structs,
794                        imported_enums,
795                    } = m;
796
797                    for t in imported_tasks.values_mut() {
798                        t.span = shift_absolute_span(t.span, edits);
799                    }
800                    for w in imported_workflows.values_mut() {
801                        w.span = shift_absolute_span(w.span, edits);
802                    }
803                    for s in imported_structs.values_mut() {
804                        s.span = shift_absolute_span(s.span, edits);
805                    }
806                    for e in imported_enums.values_mut() {
807                        e.span = shift_absolute_span(e.span, edits);
808                    }
809                }
810            },
811        }
812    }
813
814    /// Change the item's CST offset.
815    fn swap_offset(&mut self, offset: usize) {
816        match self {
817            Self::Struct(s) => s.offset = offset,
818            Self::Enum(e) => e.offset = offset,
819            Self::Task(t) => t.offset = offset,
820            Self::Workflow(w) => w.offset = offset,
821            Self::Import(i) => i.offset = offset,
822        }
823    }
824}
825
826/// A reference to an item in the cache.
827#[derive(Copy, Clone, Debug, PartialEq)]
828pub(in crate::document) enum CachedItemRef<'a> {
829    /// An analyzed struct.
830    Struct(&'a CachedItem<Struct>),
831    /// An analyzed enum.
832    Enum(&'a CachedItem<Enum>),
833    /// An analyzed task.
834    Task(&'a CachedItem<WithBodyHash<Task>>),
835    /// An analyzed workflow.
836    Workflow(&'a CachedItem<WithBodyHash<Workflow>>),
837    /// An analyzed import.
838    Import(&'a CachedItem<WithBodyHash<Import>>),
839}
840
841impl<'a> CachedItemRef<'a> {
842    /// Get the name of the item, if it has one.
843    pub fn name(&self) -> Option<&'a str> {
844        match self {
845            Self::Struct(s) => Some(s.item.name()),
846            Self::Enum(e) => Some(e.item.name()),
847            Self::Task(t) => Some(t.item.item.name()),
848            Self::Workflow(w) => Some(w.item.item.name()),
849            Self::Import(i) => match &i.item.item {
850                Import::Merging { .. } => None,
851                Import::Namespace(ns) => Some(ns.name()),
852            },
853        }
854    }
855
856    /// Get the diagnostics produced for this item.
857    pub fn diagnostics(&self) -> impl Iterator<Item = Diagnostic> + use<'a> {
858        let (offset, diagnostics) = match self {
859            Self::Struct(s) => (s.offset, s.diagnostics.iter()),
860            Self::Enum(e) => (e.offset, e.diagnostics.iter()),
861            Self::Task(t) => (t.offset, t.diagnostics.iter()),
862            Self::Workflow(w) => (w.offset, w.diagnostics.iter()),
863            Self::Import(i) => (i.offset, i.diagnostics.iter()),
864        };
865
866        // We need to shift the diagnostics back to their absolute positions
867        // within the document. `CachedItemRef` stores diagnostics
868        // relative to the start of the item.
869        diagnostics.cloned().map(move |mut d| {
870            d.offset(offset as isize);
871            d
872        })
873    }
874
875    /// Get the [`SignatureHash`] of the item.
876    pub fn signature_hash(&self) -> SignatureHash {
877        match self {
878            Self::Struct(s) => s.signature_hash,
879            Self::Enum(e) => e.signature_hash,
880            Self::Task(t) => t.signature_hash,
881            Self::Workflow(w) => w.signature_hash,
882            Self::Import(i) => i.signature_hash,
883        }
884    }
885
886    /// Get the [`BodyHash`] of the item, if it has one.
887    pub fn body_hash(&self) -> Option<BodyHash> {
888        match self {
889            Self::Import(i) => Some(i.item.body_hash),
890            Self::Task(t) => Some(t.item.body_hash),
891            Self::Workflow(w) => Some(w.item.body_hash),
892            _ => None,
893        }
894    }
895}
896
897/// Extra data retained during test analysis runs.
898#[cfg(test)]
899#[derive(Debug, Clone, Default)]
900struct TestCache {
901    /// The list of items whose signatures were invalidated in the last pass.
902    invalidated_signatures: Vec<SignatureHash>,
903    /// The list of items whose bodies were invalidated in the last pass.
904    invalidated_bodies: Vec<SignatureHash>,
905}
906
907/// A cache of a document's analyzed items.
908#[derive(Debug, Clone, Default)]
909pub(crate) struct AnalysisCache {
910    /// Map of struct hashes to their cached analysis results.
911    pub structs: IndexMap<SignatureHash, CachedItem<Struct>>,
912    /// Map of enum hashes to their cached analysis results.
913    pub enums: IndexMap<SignatureHash, CachedItem<Enum>>,
914    /// Map of task hashes to their cached analysis results.
915    pub tasks: IndexMap<SignatureHash, CachedItem<WithBodyHash<Task>>>,
916    /// The workflow in the document.
917    pub workflow: Option<CachedItem<WithBodyHash<Workflow>>>,
918    /// Map of import hashes to their cached analysis results.
919    pub imports: IndexMap<SignatureHash, CachedItem<WithBodyHash<Import>>>,
920    /// Analysis item dependency graph.
921    dependencies: DiGraphMap<SignatureHash, ()>,
922    /// Extra data used for tests.
923    #[cfg(test)]
924    tests: TestCache,
925}
926
927impl PartialEq for AnalysisCache {
928    fn eq(&self, other: &Self) -> bool {
929        self.structs == other.structs
930            && self.enums == other.enums
931            && self.tasks == other.tasks
932            && self.workflow == other.workflow
933            && self.imports == other.imports
934            && self
935                .dependencies
936                .all_edges()
937                .all(|(a, b, _)| other.dependencies.contains_edge(a, b))
938    }
939}
940
941/// Generates the common methods for local and imported items.
942macro_rules! item_getters {
943    (
944        $(
945            (
946                $item_ty:ident, $cache_field:ident, $all_by_name:ident, $local_fn:ident, $local_fn_by_name:ident, $import_fn:ident, $import_fn_by_name:ident
947            ) => ($ty:ty, $ref_ty:ident, $imported_ty:ty)
948        ),+ $(,)+
949    ) => {
950        $(
951        paste::paste! {
952            #[doc = "Gets the " $item_ty "s locally defined in the document."]
953            ///
954            /// Returns `(index, hash, item)` tuples, where:
955            ///
956            /// * `index` - The position of the item in the cache. See
957            #[doc = "[`Self::" $item_ty "_by_index()`]."]
958            pub(crate) fn $local_fn(&self) -> impl Iterator<Item = (usize, SignatureHash, &$ty)> {
959                self.$cache_field
960                    .iter()
961                    .enumerate()
962                    .map(|(idx, (hash, i))| (idx, *hash, i.target()))
963            }
964
965            #[doc = "Gets a locally defined " $item_ty " in the document by name."]
966            ///
967            #[doc = "See: [`Self::" $local_fn "`]"]
968            pub(crate) fn $local_fn_by_name(&self, name: &str) -> Option<(usize, SignatureHash, &$ty)> {
969                self.$local_fn().find(|(_idx, _hash, i)| i.name() == name)
970            }
971
972            #[doc = "Gets the " $item_ty "s in the document."]
973            ///
974            /// NOTE: This includes both locally defined and imported items.
975            ///
976            #[doc = "See: [`Self::" $local_fn "`], [`Self::" $import_fn "`]."]
977            pub(crate) fn $cache_field(&self) -> impl Iterator<Item = $ref_ty<'_>> {
978                self.$local_fn()
979                    .map(|(_idx, _hash, t)| $ref_ty::Local(t))
980                    .chain(self.$import_fn().map(|(_hash, t)| $ref_ty::Imported(t)))
981            }
982
983            #[doc = "Gets a " $item_ty " in the document by name."]
984            ///
985            #[doc = "See: [`Self::" $local_fn_by_name "`], [`Self::" $import_fn_by_name "`]."]
986            pub(crate) fn $all_by_name(&self, name: &str) -> Option<(SignatureHash, $ref_ty<'_>)> {
987                self.$local_fn_by_name(name)
988                    .map(|(_idx, hash, t)| (hash, $ref_ty::Local(t)))
989                    .or_else(|| {
990                        self.$import_fn_by_name(name)
991                            .map(|(hash, t)| (hash, $ref_ty::Imported(t)))
992                    })
993            }
994
995            #[doc = "Gets an imported " $item_ty " in the document by local name."]
996            ///
997            /// NOTE: This only includes imports in the current document's scope. Namespaced imports
998            ///       are available through [`Self::namespaces()`].
999            pub(crate) fn $import_fn_by_name(
1000                &self,
1001                name: &str,
1002            ) -> Option<(SignatureHash, &$imported_ty)> {
1003                self.$import_fn()
1004                    .find(|(_hash, t)| t.local_name == name)
1005            }
1006        }
1007        )+
1008    }
1009}
1010
1011// Public getters
1012impl AnalysisCache {
1013    item_getters!(
1014        (task, tasks, task_by_name, local_tasks, local_task_by_name, imported_tasks, imported_task_by_name) => (Task, TaskRef, ImportedTask),
1015        (struct, structs, struct_by_name, local_structs, local_struct_by_name, imported_structs, imported_struct_by_name) => (Struct, StructRef, ImportedStruct),
1016        (enum, enums, enum_by_name, local_enums, local_enum_by_name, imported_enums, imported_enum_by_name) => (Enum, EnumRef, ImportedEnum),
1017    );
1018
1019    /// Returns the number of items in the cache.
1020    pub fn len(&self) -> usize {
1021        let Self {
1022            structs,
1023            enums,
1024            tasks,
1025            workflow,
1026            imports,
1027            dependencies: _,
1028            #[cfg(test)]
1029                tests: _,
1030        } = self;
1031
1032        structs.len() + enums.len() + tasks.len() + workflow.is_some() as usize + imports.len()
1033    }
1034
1035    /// Returns whether the cache is empty.
1036    #[cfg(test)]
1037    pub fn is_empty(&self) -> bool {
1038        self.len() == 0
1039    }
1040
1041    /// Gets all imported structs in the document.
1042    ///
1043    /// NOTE: This includes structs from all import forms.
1044    pub(crate) fn imported_structs(
1045        &self,
1046    ) -> impl Iterator<Item = (SignatureHash, &ImportedStruct)> {
1047        self.imports()
1048            .flat_map(|(_idx, hash, i)| i.structs().map(move |s| (hash, s)))
1049    }
1050
1051    /// Gets all imported enums in the document.
1052    ///
1053    /// NOTE: This includes enums from all import forms.
1054    pub(crate) fn imported_enums(&self) -> impl Iterator<Item = (SignatureHash, &ImportedEnum)> {
1055        self.imports()
1056            .flat_map(|(_idx, hash, i)| i.enums().map(move |e| (hash, e)))
1057    }
1058
1059    /// Gets all imported tasks in the document.
1060    ///
1061    /// NOTE: This only includes tasks in the current document's scope (e.g.,
1062    /// those from select/wildcard imports).
1063    pub(crate) fn imported_tasks(&self) -> impl Iterator<Item = (SignatureHash, &ImportedTask)> {
1064        self.imports()
1065            .filter_map(|(_idx, hash, i)| i.merging().map(|m| (hash, m)))
1066            .flat_map(|(hash, i)| i.imported_tasks.values().map(move |t| (hash, t)))
1067    }
1068
1069    /// Gets the import statements in the document.
1070    pub(crate) fn imports(&self) -> impl Iterator<Item = (usize, SignatureHash, &Import)> {
1071        self.imports
1072            .iter()
1073            .enumerate()
1074            .map(|(idx, (hash, i))| (idx, *hash, &i.item.item))
1075    }
1076
1077    /// Gets the namespaces in the document.
1078    pub(crate) fn namespaces(&self) -> impl Iterator<Item = (SignatureHash, &Namespace)> {
1079        self.imports()
1080            .filter_map(|(_idx, hash, i)| i.namespace().map(|ns| (hash, ns)))
1081    }
1082
1083    /// Gets a successfully resolved namespace in the document by name.
1084    pub fn namespace_by_name(&self, name: &str) -> Option<(SignatureHash, &Namespace)> {
1085        self.namespaces().find(|(_, ns)| ns.name == name)
1086    }
1087
1088    /// Gets the workflow in the document.
1089    ///
1090    /// Returns `None` if the document did not contain a workflow.
1091    pub(crate) fn workflow(&self) -> Option<&Workflow> {
1092        self.workflow.as_ref().map(|i| &i.item.item)
1093    }
1094
1095    /// Gets an imported workflow in the document by local name.
1096    ///
1097    /// NOTE: This only includes workflows in the current document's scope
1098    /// (e.g., those from select/wildcard imports).
1099    pub(crate) fn imported_workflow_by_name(
1100        &self,
1101        name: &str,
1102    ) -> Option<(SignatureHash, &ImportedWorkflow)> {
1103        self.imports()
1104            .filter_map(|(_idx, hash, i)| i.merging().map(|m| (hash, m)))
1105            .find_map(|(hash, i)| i.imported_workflows.get(name).map(|w| (hash, w)))
1106    }
1107
1108    /// Gets all imported workflows in the document.
1109    ///
1110    /// NOTE: This only includes workflows in the current document's scope
1111    /// (e.g., those from select/wildcard imports).
1112    pub(crate) fn imported_workflows(
1113        &self,
1114    ) -> impl Iterator<Item = (SignatureHash, &ImportedWorkflow)> {
1115        self.imports()
1116            .filter_map(|(_idx, hash, i)| i.merging().map(|m| (hash, m)))
1117            .flat_map(|(hash, i)| i.imported_workflows.values().map(move |w| (hash, w)))
1118    }
1119
1120    /// Gets a task in the document by name.
1121    ///
1122    /// See: [`Self::imported_workflow_by_name()`].
1123    pub(crate) fn workflow_by_name(&self, name: &str) -> Option<(SignatureHash, WorkflowRef<'_>)> {
1124        self.workflow
1125            .as_ref()
1126            .filter(|wf| wf.item.item.name == name)
1127            .map(|item| (item.signature_hash, WorkflowRef::Local(&item.item.item)))
1128            .or_else(|| {
1129                self.imported_workflow_by_name(name)
1130                    .map(|(hash, wf)| (hash, WorkflowRef::Imported(wf)))
1131            })
1132    }
1133}
1134
1135// Private getters (only ever used in `populate_document`)
1136impl AnalysisCache {
1137    /// Returns an iterator over dirty items in `current_ast` that are missing
1138    /// from the cache.
1139    ///
1140    /// The iterator is ordered for the `populate_document` passes:
1141    ///
1142    /// 1. `import`
1143    /// 2. `struct`, `enum`
1144    /// 3. `task`, `workflow`
1145    pub(in crate::document) fn dirty<'a>(
1146        &self,
1147        current_ast: &'a AstItems,
1148    ) -> impl Iterator<Item = (SignatureHash, Option<BodyHash>, &'a DocumentItem)> {
1149        current_ast.items.iter().filter_map(move |ast_item| {
1150            match self.get(&ast_item.signature_hash) {
1151                Some(cache_item) => {
1152                    let Some(expected_body_hash) = cache_item.body_hash() else {
1153                        return None; // signature comparison is enough, not dirty
1154                    };
1155
1156                    let Some(new_body_hash) = ast_item.body_hash else {
1157                        // This is only ever the case for imports. The
1158                        // `BodyHash` of imports is
1159                        // calculated separately in `Self::intersect()`. If it's
1160                        // still in the cache at this
1161                        // point, it isn't dirty.
1162                        return None;
1163                    };
1164
1165                    if expected_body_hash == new_body_hash {
1166                        return None; // body unchanged, not dirty
1167                    }
1168
1169                    Some((ast_item.signature_hash, ast_item.body_hash, &ast_item.item))
1170                }
1171                // Either a new or changed item
1172                None => Some((ast_item.signature_hash, ast_item.body_hash, &ast_item.item)),
1173            }
1174        })
1175    }
1176
1177    /// Gets all of the items in the cache.
1178    fn items(&self) -> impl Iterator<Item = CachedItemRef<'_>> {
1179        self.structs
1180            .values()
1181            .map(CachedItemRef::Struct)
1182            .chain(self.enums.values().map(CachedItemRef::Enum))
1183            .chain(self.tasks.values().map(CachedItemRef::Task))
1184            .chain(
1185                self.workflow
1186                    .as_ref()
1187                    .into_iter()
1188                    .map(CachedItemRef::Workflow),
1189            )
1190            .chain(self.imports.values().map(CachedItemRef::Import))
1191    }
1192
1193    /// Gets a mutable reference all of the items in the cache.
1194    pub(in crate::document) fn items_mut(&mut self) -> impl Iterator<Item = CachedItemRefMut<'_>> {
1195        self.structs
1196            .values_mut()
1197            .map(CachedItemRefMut::Struct)
1198            .chain(self.enums.values_mut().map(CachedItemRefMut::Enum))
1199            .chain(self.tasks.values_mut().map(CachedItemRefMut::Task))
1200            .chain(
1201                self.workflow
1202                    .as_mut()
1203                    .into_iter()
1204                    .map(CachedItemRefMut::Workflow),
1205            )
1206            .chain(self.imports.values_mut().map(CachedItemRefMut::Import))
1207    }
1208
1209    /// Gets all of the diagnostics in the cache.
1210    pub(in crate::document) fn diagnostics(&self) -> impl Iterator<Item = Diagnostic> + use<'_> {
1211        self.items().flat_map(|i| i.diagnostics())
1212    }
1213
1214    /// Looks up a cached item by hash.
1215    pub(in crate::document) fn get(&self, hash: &SignatureHash) -> Option<CachedItemRef<'_>> {
1216        self.structs
1217            .get(hash)
1218            .map(CachedItemRef::Struct)
1219            .or_else(|| self.enums.get(hash).map(CachedItemRef::Enum))
1220            .or_else(|| self.tasks.get(hash).map(CachedItemRef::Task))
1221            .or_else(|| {
1222                if self.workflow.as_ref().map(|w| &w.signature_hash) == Some(hash) {
1223                    self.workflow.as_ref().map(CachedItemRef::Workflow)
1224                } else {
1225                    None
1226                }
1227            })
1228            .or_else(|| self.imports.get(hash).map(CachedItemRef::Import))
1229    }
1230
1231    /// Looks up a cached item by hash.
1232    fn get_mut(&mut self, hash: &SignatureHash) -> Option<CachedItemRefMut<'_>> {
1233        self.structs
1234            .get_mut(hash)
1235            .map(CachedItemRefMut::Struct)
1236            .or_else(|| self.enums.get_mut(hash).map(CachedItemRefMut::Enum))
1237            .or_else(|| self.tasks.get_mut(hash).map(CachedItemRefMut::Task))
1238            .or_else(|| {
1239                if self.workflow.as_ref().map(|w| &w.signature_hash) == Some(hash) {
1240                    self.workflow.as_mut().map(CachedItemRefMut::Workflow)
1241                } else {
1242                    None
1243                }
1244            })
1245            .or_else(|| self.imports.get_mut(hash).map(CachedItemRefMut::Import))
1246    }
1247
1248    /// Get an item in the document's scope by name.
1249    pub(in crate::document) fn item_by_name(&self, name: &str) -> Option<Item<'_>> {
1250        self.items()
1251            .map(Item::Local)
1252            .chain(
1253                self.imports()
1254                    .filter_map(|(_idx, _hash, i)| i.merging())
1255                    .flat_map(|i| i.items().map(Item::Imported)),
1256            )
1257            .find(|i| i.name() == Some(name))
1258    }
1259
1260    /// Gets a struct in the document at the given cache index.
1261    pub(in crate::document) fn struct_by_index(&self, index: usize) -> Option<&Struct> {
1262        Some(&self.structs.get_index(index)?.1.item)
1263    }
1264
1265    /// Gets an enum in the document at the given cache index.
1266    pub(in crate::document) fn enum_by_index(&self, index: usize) -> Option<&Enum> {
1267        Some(&self.enums.get_index(index)?.1.item)
1268    }
1269
1270    /// Gets all of the signature hashes in the cache.
1271    ///
1272    /// NOTE: These are not guaranteed to be in stable order between
1273    /// invalidations.
1274    fn keys(&self) -> impl Iterator<Item = &SignatureHash> {
1275        self.structs
1276            .keys()
1277            .chain(self.enums.keys())
1278            .chain(self.tasks.keys())
1279            .chain(self.workflow.as_ref().map(|w| &w.signature_hash))
1280            .chain(self.imports.keys())
1281    }
1282
1283    /// Hash all of the exported symbols in the cache.
1284    pub(in crate::document) fn exports_hash(&self) -> BodyHash {
1285        let mut hasher = Sha256::default();
1286
1287        // Invalidation, on both the signature and body level, will shift around
1288        // the keys in the cache. We need to sort them here to keep the
1289        // hash stable.
1290        let mut keys: Vec<_> = self.keys().collect();
1291        keys.sort_unstable();
1292
1293        for signature in keys {
1294            hasher.update(signature);
1295        }
1296        hasher.finalize().into()
1297    }
1298}
1299
1300/// The method for invalidating an existing cache item.
1301#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1302pub(in crate::document) enum InvalidationStrategy {
1303    /// The item's signature needs to be invalidated.
1304    Signature,
1305    /// The item's body needs to be invalidated.
1306    Body,
1307}
1308
1309// Mutation (only ever used in `populate_document`)
1310impl AnalysisCache {
1311    /// Inserts an import into the cache.
1312    pub(in crate::document) fn insert_import(
1313        &mut self,
1314        mut item: CachedItem<WithBodyHash<Import>>,
1315    ) {
1316        let hash = item.signature_hash;
1317        item.shift_diagnostic_offsets();
1318
1319        self.imports.insert(item.signature_hash, item);
1320        self.dependencies.add_node(hash);
1321    }
1322
1323    /// Inserts an enum into the cache.
1324    pub(in crate::document) fn insert_enum(&mut self, mut item: CachedItem<Enum>) {
1325        let hash = item.signature_hash;
1326        item.shift_diagnostic_offsets();
1327
1328        self.enums.insert(item.signature_hash, item);
1329        self.dependencies.add_node(hash);
1330    }
1331
1332    /// Inserts a struct into the cache.
1333    pub(in crate::document) fn insert_struct(&mut self, mut item: CachedItem<Struct>) {
1334        let hash = item.signature_hash;
1335        item.shift_diagnostic_offsets();
1336
1337        self.structs.insert(item.signature_hash, item);
1338        self.dependencies.add_node(hash);
1339    }
1340
1341    /// Inserts a task into the cache.
1342    pub(in crate::document) fn insert_task(&mut self, mut item: CachedItem<WithBodyHash<Task>>) {
1343        let hash = item.signature_hash;
1344        item.shift_diagnostic_offsets();
1345
1346        self.tasks.insert(item.signature_hash, item);
1347        self.dependencies.add_node(hash);
1348    }
1349
1350    /// Inserts a workflow into the cache.
1351    pub(in crate::document) fn set_workflow(&mut self, item: CachedItem<WithBodyHash<Workflow>>) {
1352        // NOTE: We don't shift the diagnostics here. Workflow addition and
1353        // population are different steps. Diagnostics are shifted
1354        // *after* `populate_workflow()`.
1355        let hash = item.signature_hash;
1356        self.workflow = Some(item);
1357        self.dependencies.add_node(hash);
1358    }
1359
1360    /// Gets the namespaces in the document.
1361    pub(in crate::document) fn namespaces_mut(&mut self) -> impl Iterator<Item = &mut Namespace> {
1362        self.imports
1363            .values_mut()
1364            .flat_map(|i| i.item.item.namespace_mut())
1365    }
1366
1367    /// Gets a mutable reference to the cached item for the workflow in the
1368    /// document.
1369    pub(in crate::document) fn workflow_item_mut(
1370        &mut self,
1371    ) -> Option<&mut CachedItem<WithBodyHash<Workflow>>> {
1372        self.workflow.as_mut()
1373    }
1374
1375    /// Gets a mutable reference to a `struct` `CachedItem` at the given index.
1376    pub(in crate::document) fn struct_item_mut(
1377        &mut self,
1378        index: usize,
1379    ) -> Option<&mut CachedItem<Struct>> {
1380        Some(self.structs.get_index_mut(index)?.1)
1381    }
1382
1383    /// Remove an item by its [`SignatureHash`].
1384    ///
1385    /// NOTE: This does not invalidate the item's dependents.
1386    fn remove_item(&mut self, hash: &SignatureHash) {
1387        self.structs
1388            .shift_remove(hash)
1389            .map(|_| ())
1390            .or_else(|| self.enums.shift_remove(hash).map(|_| ()))
1391            .or_else(|| self.tasks.shift_remove(hash).map(|_| ()))
1392            .or_else(|| self.imports.shift_remove(hash).map(|_| ()));
1393
1394        if self.workflow.as_ref().map(|w| &w.signature_hash) == Some(hash) {
1395            self.workflow = None;
1396        }
1397    }
1398
1399    /// Invalidates the given items and all of their dependents from the cache.
1400    pub(in crate::document) fn invalidate(
1401        &mut self,
1402        ast_items: &AstItems,
1403        edits: &[AppliedEdit],
1404        hashes: impl IntoIterator<Item = (InvalidationStrategy, SignatureHash)>,
1405    ) {
1406        let mut dirty_set = std::collections::HashSet::new();
1407        for (strategy, hash) in hashes {
1408            match strategy {
1409                // Invalidate the item and every dependent.
1410                InvalidationStrategy::Signature => {
1411                    let mut stack = vec![hash];
1412                    while let Some(node) = stack.pop() {
1413                        if dirty_set.insert(node) {
1414                            #[cfg(test)]
1415                            self.tests.invalidated_signatures.push(node);
1416
1417                            for dependent in self
1418                                .dependencies
1419                                .neighbors_directed(node, petgraph::Direction::Incoming)
1420                            {
1421                                stack.push(dependent);
1422                            }
1423                        }
1424                    }
1425                }
1426                // Invalidate the item only.
1427                InvalidationStrategy::Body => {
1428                    #[cfg(test)]
1429                    self.tests.invalidated_bodies.push(hash);
1430
1431                    self.remove_item(&hash);
1432                    let outgoing: Vec<_> = self
1433                        .dependencies
1434                        .neighbors_directed(hash, petgraph::Direction::Outgoing)
1435                        .collect();
1436                    for dependency in outgoing {
1437                        self.dependencies.remove_edge(hash, dependency);
1438                    }
1439                }
1440            }
1441        }
1442
1443        for hash in dirty_set {
1444            self.remove_item(&hash);
1445            self.dependencies.remove_node(hash);
1446        }
1447
1448        for ast_item in &ast_items.items {
1449            let hash = ast_item.signature_hash;
1450            let Some(mut item) = self.get_mut(&hash) else {
1451                continue;
1452            };
1453
1454            // Existing diagnostics need to be shifted
1455            item.shift_existing_diagnostics(edits, ast_item.offset);
1456            item.swap_offset(ast_item.offset);
1457        }
1458    }
1459
1460    /// Drop items and their dependents from the cache that are not present in
1461    /// `current_ast` or whose body hash has changed.
1462    pub(in crate::document) fn intersect(
1463        &self,
1464        current_ast: &AstItems,
1465        mut resolve_import_body_hash: impl FnMut(&ImportStatement) -> Option<BodyHash>,
1466    ) -> Vec<(InvalidationStrategy, SignatureHash)> {
1467        let mut to_remove = Vec::new();
1468        for cache_item in self.items() {
1469            let signature_hash = cache_item.signature_hash();
1470            if !current_ast.contains(&signature_hash) {
1471                to_remove.push((InvalidationStrategy::Signature, signature_hash));
1472                continue;
1473            }
1474
1475            let new_body_hash = match cache_item {
1476                CachedItemRef::Import(_) => {
1477                    let import_stmt = current_ast
1478                        .imports()
1479                        .find(|(h, _)| **h == signature_hash)
1480                        .map(|(_, i)| i)
1481                        .expect("should exist because current_ast contains hash");
1482                    resolve_import_body_hash(import_stmt)
1483                }
1484                _ => current_ast.get_body_hash(&signature_hash),
1485            };
1486
1487            if cache_item.body_hash() != new_body_hash {
1488                if matches!(cache_item, CachedItemRef::Import(_)) {
1489                    to_remove.push((InvalidationStrategy::Signature, signature_hash));
1490                } else {
1491                    to_remove.push((InvalidationStrategy::Body, signature_hash));
1492                }
1493            }
1494        }
1495
1496        to_remove
1497    }
1498
1499    /// Gets a mutable reference to an `enum` `CachedItem` at the given index.
1500    pub(in crate::document) fn enum_item_mut(
1501        &mut self,
1502        index: usize,
1503    ) -> Option<&mut CachedItem<Enum>> {
1504        Some(self.enums.get_index_mut(index)?.1)
1505    }
1506
1507    /// Adds a dependency edge to the graph.
1508    pub(in crate::document) fn add_dependency(
1509        &mut self,
1510        dependent: SignatureHash,
1511        dependency: SignatureHash,
1512    ) {
1513        self.dependencies.add_edge(dependent, dependency, ());
1514    }
1515}
1516
1517/// Represents an item in the AST.
1518struct AstItem {
1519    /// The signature hash of the item.
1520    signature_hash: SignatureHash,
1521    /// The body hash of the item, if applicable.
1522    body_hash: Option<BodyHash>,
1523    /// The CST offset of the item.
1524    offset: usize,
1525    /// The item itself.
1526    item: DocumentItem,
1527}
1528
1529/// A collection of the items in the document's AST.
1530pub(in crate::document) struct AstItems {
1531    /// The items in the AST.
1532    items: Vec<AstItem>,
1533}
1534
1535impl AstItems {
1536    /// Checks if the AST contains an item with the given [`SignatureHash`].
1537    pub fn contains(&self, signature_hash: &SignatureHash) -> bool {
1538        self.items
1539            .iter()
1540            .any(|item| &item.signature_hash == signature_hash)
1541    }
1542
1543    /// Gets the body hash of an item by its [`SignatureHash`].
1544    pub fn get_body_hash(&self, signature_hash: &SignatureHash) -> Option<BodyHash> {
1545        self.items.iter().find_map(|item| {
1546            if &item.signature_hash == signature_hash {
1547                item.body_hash
1548            } else {
1549                None
1550            }
1551        })
1552    }
1553
1554    /// Get the number of items in the AST.
1555    pub fn len(&self) -> usize {
1556        self.items.len()
1557    }
1558
1559    /// Gets all of the import statements in the document.
1560    fn imports(&self) -> impl Iterator<Item = (&SignatureHash, &ImportStatement)> {
1561        self.items.iter().filter_map(|item| match &item.item {
1562            DocumentItem::Import(i) => Some((&item.signature_hash, i)),
1563            _ => None,
1564        })
1565    }
1566
1567    /// Create a new [`AstItems`] from the document's AST.
1568    pub fn new(ast: &Ast) -> Self {
1569        #[derive(PartialEq, Eq)]
1570        struct DocumentItemOrd<'a>(&'a DocumentItem);
1571
1572        impl DocumentItemOrd<'_> {
1573            fn ord(&self) -> u8 {
1574                match self.0 {
1575                    DocumentItem::Import(_) => 0,
1576                    DocumentItem::Struct(_) | DocumentItem::Enum(_) => 1,
1577                    DocumentItem::Task(_) | DocumentItem::Workflow(_) => 2,
1578                }
1579            }
1580        }
1581
1582        impl PartialOrd for DocumentItemOrd<'_> {
1583            fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
1584                Some(self.cmp(other))
1585            }
1586        }
1587
1588        impl Ord for DocumentItemOrd<'_> {
1589            fn cmp(&self, other: &Self) -> Ordering {
1590                self.ord().cmp(&other.ord())
1591            }
1592        }
1593
1594        let mut items = Vec::new();
1595
1596        for item in ast.items() {
1597            let offset = usize::from(item.inner().text_range().start());
1598            let (signature_hash, body_hash) = match &item {
1599                DocumentItem::Import(i) => (HashableItem::hash(i), None),
1600                DocumentItem::Struct(s) => (HashableItem::hash(s), None),
1601                DocumentItem::Enum(e) => (HashableItem::hash(e), None),
1602                DocumentItem::Task(t) => {
1603                    let (signature_hash, body_hash) = t.hash_callable();
1604                    (signature_hash, Some(body_hash))
1605                }
1606                DocumentItem::Workflow(w) => {
1607                    let (signature_hash, body_hash) = w.hash_callable();
1608                    (signature_hash, Some(body_hash))
1609                }
1610            };
1611
1612            items.push(AstItem {
1613                signature_hash,
1614                body_hash,
1615                offset,
1616                item,
1617            });
1618        }
1619
1620        items.sort_by(|a, b| DocumentItemOrd(&a.item).cmp(&DocumentItemOrd(&b.item)));
1621
1622        Self { items }
1623    }
1624}