Skip to main content

wdl_ast/
lib.rs

1//! An abstract syntax tree for Workflow Description Language (WDL) documents.
2//!
3//! The AST implementation is effectively a facade over the concrete syntax tree
4//! (CST) implemented by [SyntaxTree] from `wdl-grammar`.
5//!
6//! An AST is cheap to construct and may be cheaply cloned at any level.
7//!
8//! However, an AST (and the underlying CST) are immutable; updating the tree
9//! requires replacing a node in the tree to produce a new tree. The unaffected
10//! nodes of the replacement are reused from the old tree to the new tree.
11//!
12//! # Examples
13//!
14//! An example of parsing a WDL document into an AST and validating it:
15//!
16//! ```rust
17//! # let source = "version 1.1\nworkflow test {}";
18//! use wdl_ast::Document;
19//!
20//! let (document, diagnostics) = Document::parse(source, None);
21//! if !diagnostics.is_empty() {
22//!     // Handle the failure to parse
23//! }
24//! ```
25
26#![warn(missing_docs)]
27#![warn(rust_2018_idioms)]
28#![warn(rust_2021_compatibility)]
29#![warn(missing_debug_implementations)]
30#![warn(clippy::missing_docs_in_private_items)]
31#![warn(rustdoc::broken_intra_doc_links)]
32
33use std::collections::HashSet;
34use std::fmt;
35use std::hash::Hash;
36use std::str::FromStr;
37
38pub use rowan::Direction;
39use rowan::NodeOrToken;
40use v1::CloseBrace;
41use v1::CloseHeredoc;
42use v1::OpenBrace;
43use v1::OpenHeredoc;
44pub use wdl_grammar::Diagnostic;
45pub use wdl_grammar::Label;
46pub use wdl_grammar::Severity;
47pub use wdl_grammar::Span;
48pub use wdl_grammar::SupportedVersion;
49pub use wdl_grammar::SyntaxElement;
50pub use wdl_grammar::SyntaxKind;
51pub use wdl_grammar::SyntaxNode;
52pub use wdl_grammar::SyntaxToken;
53pub use wdl_grammar::SyntaxTokenExt;
54pub use wdl_grammar::SyntaxTree;
55pub use wdl_grammar::WorkflowDescriptionLanguage;
56pub use wdl_grammar::lexer;
57pub use wdl_grammar::version;
58
59pub mod v1;
60
61mod element;
62
63pub use element::*;
64
65/// An [`AstNode`] that may have documentation comments attached to it.
66pub trait Documented<N: TreeNode>: AstNode<N> {
67    /// Get all comment nodes preceding this node that start with
68    /// [`DOC_COMMENT_PREFIX`].
69    ///
70    /// If doc comments don't apply to this node, `None` will be returned.
71    ///
72    /// The comments returned are ordered top to bottom.
73    fn doc_comments(&self) -> Option<Vec<Comment<N::Token>>>;
74}
75
76/// Shared doc comment extraction logic.
77pub fn doc_comments<N: TreeNode>(
78    preceding_trivia: impl IntoIterator<Item = N::Token>,
79) -> impl Iterator<Item = Comment<N::Token>> {
80    preceding_trivia
81        .into_iter()
82        .take_while(|token| {
83            token.kind() == SyntaxKind::Whitespace || token.kind() == SyntaxKind::Comment
84        })
85        .filter_map(|token| {
86            if token.kind() == SyntaxKind::Comment && token.text().starts_with(DOC_COMMENT_PREFIX) {
87                Some(Comment::<N::Token>::cast(token).expect("should be a comment"))
88            } else {
89                None
90            }
91        })
92}
93
94/// A trait that abstracts the underlying representation of a syntax tree node.
95///
96/// The default node type is `SyntaxNode` for all AST nodes.
97pub trait TreeNode: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
98    /// The associated token type for the tree node.
99    type Token: TreeToken;
100
101    /// Gets the parent node of the node.
102    ///
103    /// Returns `None` if the node is a root.
104    fn parent(&self) -> Option<Self>;
105
106    /// Gets the syntax kind of the node.
107    fn kind(&self) -> SyntaxKind;
108
109    /// Gets the text of the node.
110    ///
111    /// Node text is not contiguous, so the returned value implements `Display`.
112    fn text(&self) -> impl fmt::Display;
113
114    /// Gets the span of the node.
115    fn span(&self) -> Span;
116
117    /// Gets the children nodes of the node.
118    fn children(&self) -> impl Iterator<Item = Self>;
119
120    /// Gets all the children of the node, including tokens.
121    fn children_with_tokens(&self) -> impl Iterator<Item = NodeOrToken<Self, Self::Token>>;
122
123    /// Gets the first token of the node.
124    fn first_token(&self) -> Option<Self::Token>;
125
126    /// Gets the last token of the node.
127    fn last_token(&self) -> Option<Self::Token>;
128
129    /// Gets the node descendants of the node.
130    fn descendants(&self) -> impl Iterator<Item = Self>;
131
132    /// Gets the ancestors of the node.
133    fn ancestors(&self) -> impl Iterator<Item = Self>;
134}
135
136/// A trait that abstracts the underlying representation of a syntax token.
137pub trait TreeToken: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
138    /// The associated node type for the token.
139    type Node: TreeNode;
140
141    /// Gets the parent node of the token.
142    fn parent(&self) -> Self::Node;
143
144    /// Gets the syntax kind for the token.
145    fn kind(&self) -> SyntaxKind;
146
147    /// Gets the text of the token.
148    fn text(&self) -> &str;
149
150    /// Gets the span of the token.
151    fn span(&self) -> Span;
152}
153
154/// A trait implemented by AST nodes.
155pub trait AstNode<N: TreeNode>: Sized {
156    /// Determines if the kind can be cast to this representation.
157    fn can_cast(kind: SyntaxKind) -> bool;
158
159    /// Casts the given inner type to the this representation.
160    fn cast(inner: N) -> Option<Self>;
161
162    /// Gets the inner type from this representation.
163    fn inner(&self) -> &N;
164
165    /// Gets the syntax kind of the node.
166    fn kind(&self) -> SyntaxKind {
167        self.inner().kind()
168    }
169
170    /// Gets the text of the node.
171    ///
172    /// As node text is not contiguous, this returns a type that implements
173    /// `Display`.
174    fn text<'a>(&'a self) -> impl fmt::Display
175    where
176        N: 'a,
177    {
178        self.inner().text()
179    }
180
181    /// Gets the span of the node.
182    fn span(&self) -> Span {
183        self.inner().span()
184    }
185
186    /// Gets the first token child that can cast to an expected type.
187    fn token<C>(&self) -> Option<C>
188    where
189        C: AstToken<N::Token>,
190    {
191        self.inner()
192            .children_with_tokens()
193            .filter_map(|e| e.into_token())
194            .find_map(|t| C::cast(t))
195    }
196
197    /// Gets all the token children that can cast to an expected type.
198    fn tokens<'a, C>(&'a self) -> impl Iterator<Item = C>
199    where
200        C: AstToken<N::Token>,
201        N: 'a,
202    {
203        self.inner()
204            .children_with_tokens()
205            .filter_map(|e| e.into_token().and_then(C::cast))
206    }
207
208    /// Gets the last token of the node and attempts to cast it to an expected
209    /// type.
210    ///
211    /// Returns `None` if there is no last token or if it cannot be casted to
212    /// the expected type.
213    fn last_token<C>(&self) -> Option<C>
214    where
215        C: AstToken<N::Token>,
216    {
217        self.inner().last_token().and_then(C::cast)
218    }
219
220    /// Gets the first node child that can cast to an expected type.
221    fn child<C>(&self) -> Option<C>
222    where
223        C: AstNode<N>,
224    {
225        self.inner().children().find_map(C::cast)
226    }
227
228    /// Gets all node children that can cast to an expected type.
229    fn children<'a, C>(&'a self) -> impl Iterator<Item = C>
230    where
231        C: AstNode<N>,
232        N: 'a,
233    {
234        self.inner().children().filter_map(C::cast)
235    }
236
237    /// Gets the parent of the node if the underlying tree node has a parent.
238    ///
239    /// Returns `None` if the node has no parent or if the parent node is not of
240    /// the expected type.
241    fn parent<'a, P>(&self) -> Option<P>
242    where
243        P: AstNode<N>,
244        N: 'a,
245    {
246        P::cast(self.inner().parent()?)
247    }
248
249    /// Calculates the span of a scope given the node where the scope is
250    /// visible.
251    ///
252    /// Returns `None` if the node does not contain the open and close tokens as
253    /// children.
254    fn scope_span<O, C>(&self, include_braces: bool) -> Option<Span>
255    where
256        O: AstToken<N::Token>,
257        C: AstToken<N::Token>,
258    {
259        let open = self.token::<O>()?.span();
260        let close = self.last_token::<C>()?.span();
261
262        let start = if include_braces {
263            open.start()
264        } else {
265            open.end()
266        };
267        Some(Span::new(start, close.end() - start))
268    }
269
270    /// Gets the interior span of child opening and closing brace tokens for the
271    /// node.
272    ///
273    /// If `include_braces` is true, the returned [`Span`] will include both the
274    /// opening and closing braces. Otherwise, the span starts from
275    /// immediately after the opening brace token and ends immediately
276    /// before the closing brace token.
277    ///
278    /// Returns `None` if the node does not contain child brace tokens.
279    fn braced_scope_span(&self, include_braces: bool) -> Option<Span> {
280        self.scope_span::<OpenBrace<N::Token>, CloseBrace<N::Token>>(include_braces)
281    }
282
283    /// Gets the interior span of child opening and closing heredoc tokens for
284    /// the node.
285    ///
286    /// If `include_braces` is true, the returned [`Span`] will include both the
287    /// opening and closing braces. Otherwise, the span starts from
288    /// immediately after the opening brace token and ends immediately
289    /// before the closing brace token.
290    ///
291    /// Returns `None` if the node does not contain child heredoc tokens.
292    fn heredoc_scope_span(&self, include_braces: bool) -> Option<Span> {
293        self.scope_span::<OpenHeredoc<N::Token>, CloseHeredoc<N::Token>>(include_braces)
294    }
295
296    /// Gets the node descendants (including self) from this node that can be
297    /// cast to the expected type.
298    fn descendants<'a, D>(&'a self) -> impl Iterator<Item = D>
299    where
300        D: AstNode<N>,
301        N: 'a,
302    {
303        self.inner().descendants().filter_map(|d| D::cast(d))
304    }
305}
306
307/// A trait implemented by AST tokens.
308pub trait AstToken<T: TreeToken>: Sized {
309    /// Determines if the kind can be cast to this representation.
310    fn can_cast(kind: SyntaxKind) -> bool;
311
312    /// Casts the given inner type to the this representation.
313    fn cast(inner: T) -> Option<Self>;
314
315    /// Gets the inner type from this representation.
316    fn inner(&self) -> &T;
317
318    /// Gets the syntax kind of the token.
319    fn kind(&self) -> SyntaxKind {
320        self.inner().kind()
321    }
322
323    /// Gets the text of the token.
324    fn text<'a>(&'a self) -> &'a str
325    where
326        T: 'a,
327    {
328        self.inner().text()
329    }
330
331    /// Gets the span of the token.
332    fn span(&self) -> Span {
333        self.inner().span()
334    }
335
336    /// Gets the parent of the token.
337    ///
338    /// Returns `None` if the parent node cannot be cast to the expected type.
339    fn parent<'a, P>(&self) -> Option<P>
340    where
341        P: AstNode<T::Node>,
342        T: 'a,
343    {
344        P::cast(self.inner().parent())
345    }
346}
347
348/// Implemented by nodes that can create a new root from a different tree node
349/// type.
350pub trait NewRoot<N: TreeNode>: Sized {
351    /// Constructs a new root node from the give root node of a different tree
352    /// node type.
353    fn new_root(root: N) -> Self;
354}
355
356impl TreeNode for SyntaxNode {
357    type Token = SyntaxToken;
358
359    fn parent(&self) -> Option<SyntaxNode> {
360        self.parent()
361    }
362
363    fn kind(&self) -> SyntaxKind {
364        self.kind()
365    }
366
367    fn children(&self) -> impl Iterator<Item = Self> {
368        self.children()
369    }
370
371    fn children_with_tokens(&self) -> impl Iterator<Item = NodeOrToken<Self, Self::Token>> {
372        self.children_with_tokens()
373    }
374
375    fn text(&self) -> impl fmt::Display {
376        self.text()
377    }
378
379    fn span(&self) -> Span {
380        let range = self.text_range();
381        let start = usize::from(range.start());
382        Span::new(start, usize::from(range.end()) - start)
383    }
384
385    fn first_token(&self) -> Option<Self::Token> {
386        self.first_token()
387    }
388
389    fn last_token(&self) -> Option<Self::Token> {
390        self.last_token()
391    }
392
393    fn descendants(&self) -> impl Iterator<Item = Self> {
394        self.descendants()
395    }
396
397    fn ancestors(&self) -> impl Iterator<Item = Self> {
398        self.ancestors()
399    }
400}
401
402impl TreeToken for SyntaxToken {
403    type Node = SyntaxNode;
404
405    fn parent(&self) -> SyntaxNode {
406        self.parent().expect("token should have a parent")
407    }
408
409    fn kind(&self) -> SyntaxKind {
410        self.kind()
411    }
412
413    fn text(&self) -> &str {
414        self.text()
415    }
416
417    fn span(&self) -> Span {
418        let range = self.text_range();
419        let start = usize::from(range.start());
420        Span::new(start, usize::from(range.end()) - start)
421    }
422}
423
424/// Represents the AST of a [Document].
425///
426/// See [Document::ast].
427#[derive(Clone, Debug, PartialEq, Eq)]
428pub enum Ast<N: TreeNode = SyntaxNode> {
429    /// The WDL document specifies an unsupported version.
430    Unsupported,
431    /// The WDL document is V1.
432    V1(v1::Ast<N>),
433}
434
435impl<N: TreeNode> Ast<N> {
436    /// Gets the AST as a V1 AST.
437    ///
438    /// Returns `None` if the AST is not a V1 AST.
439    pub fn as_v1(&self) -> Option<&v1::Ast<N>> {
440        match self {
441            Self::V1(ast) => Some(ast),
442            _ => None,
443        }
444    }
445
446    /// Consumes `self` and attempts to return the V1 AST.
447    pub fn into_v1(self) -> Option<v1::Ast<N>> {
448        match self {
449            Self::V1(ast) => Some(ast),
450            _ => None,
451        }
452    }
453
454    /// Consumes `self` and attempts to return the V1 AST.
455    ///
456    /// # Panics
457    ///
458    /// Panics if the AST is not a V1 AST.
459    pub fn unwrap_v1(self) -> v1::Ast<N> {
460        self.into_v1().expect("the AST is not a V1 AST")
461    }
462}
463
464/// Represents a single WDL document.
465///
466/// See [Document::ast] for getting a version-specific Abstract
467/// Syntax Tree.
468#[derive(Clone, PartialEq, Eq, Hash)]
469pub struct Document<N: TreeNode = SyntaxNode>(N);
470
471impl<N: TreeNode> AstNode<N> for Document<N> {
472    fn can_cast(kind: SyntaxKind) -> bool {
473        kind == SyntaxKind::RootNode
474    }
475
476    fn cast(inner: N) -> Option<Self> {
477        if Self::can_cast(inner.kind()) {
478            Some(Self(inner))
479        } else {
480            None
481        }
482    }
483
484    fn inner(&self) -> &N {
485        &self.0
486    }
487}
488
489impl Documented<SyntaxNode> for Document<SyntaxNode> {
490    fn doc_comments(&self) -> Option<Vec<Comment<<SyntaxNode as TreeNode>::Token>>> {
491        let version_statement = self.child::<VersionStatement>()?;
492        let version_keyword = version_statement.keyword();
493        Some(doc_comments::<SyntaxNode>(version_keyword.inner().preceding_trivia()).collect())
494    }
495}
496
497impl Document {
498    /// Parses a document from the given source.
499    ///
500    /// This optionally takes a `fallback_version`, which will be used if a
501    /// [`SupportedVersion`] cannot be determined from the document.
502    ///
503    /// A document and its AST elements are trivially cloned.
504    ///
505    /// # Examples
506    ///
507    /// ```rust
508    /// # use wdl_ast::{Document, AstToken, Ast};
509    /// use wdl_grammar::SupportedVersion;
510    /// use wdl_grammar::version::V1;
511    /// let (document, diagnostics) = Document::parse("version 1.1", None);
512    /// assert!(diagnostics.is_empty());
513    ///
514    /// assert_eq!(
515    ///     document
516    ///         .version_statement()
517    ///         .expect("should have version statement")
518    ///         .version()
519    ///         .text(),
520    ///     "1.1"
521    /// );
522    ///
523    /// match document.ast() {
524    ///     Ast::V1(ast) => {
525    ///         assert_eq!(ast.items().count(), 0);
526    ///     }
527    ///     Ast::Unsupported => panic!("should be a V1 AST"),
528    /// }
529    /// ```
530    ///
531    /// With a fallback version:
532    ///
533    /// ```rust
534    /// # use wdl_ast::{Document, AstToken, Ast};
535    /// # use wdl_grammar::version::{SupportedVersion, V1};
536    /// let fallback_version = SupportedVersion::V1(V1::Three);
537    ///
538    /// let (document, diagnostics) = Document::parse("version foo", Some(fallback_version));
539    /// assert!(diagnostics.is_empty());
540    ///
541    /// assert_eq!(
542    ///     document
543    ///         .version_statement()
544    ///         .expect("should have version statement")
545    ///         .version()
546    ///         .text(),
547    ///     "foo" // Not a valid version!
548    /// );
549    ///
550    /// match document.ast_with_version_fallback(Some(fallback_version)) {
551    ///     Ast::V1(ast) => {
552    ///         assert_eq!(ast.items().count(), 0);
553    ///     }
554    ///     Ast::Unsupported => panic!("should be a V1 AST"),
555    /// }
556    /// ```
557    pub fn parse(
558        source: &str,
559        fallback_version: Option<SupportedVersion>,
560    ) -> (Self, Vec<Diagnostic>) {
561        let (tree, diagnostics) = SyntaxTree::parse(source, fallback_version);
562        (
563            Document::cast(tree.into_syntax()).expect("document should cast"),
564            diagnostics,
565        )
566    }
567}
568
569impl<N: TreeNode> Document<N> {
570    /// Gets the version statement of the document.
571    ///
572    /// This can be used to determine the version of the document that was
573    /// parsed.
574    ///
575    /// A return value of `None` signifies a missing version statement.
576    pub fn version_statement(&self) -> Option<VersionStatement<N>> {
577        self.child()
578    }
579
580    /// Gets the AST representation of the document.
581    pub fn ast(&self) -> Ast<N> {
582        self.ast_with_version_fallback(None)
583    }
584
585    /// Gets the AST representation of the document, falling back to the
586    /// specified WDL version if the document's version statement contains
587    /// an unrecognized version.
588    ///
589    /// A fallback version of `None` does not have any fallback behavior, and is
590    /// equivalent to calling [`Document::ast()`].
591    ///
592    /// <div class="warning">
593    ///
594    /// It is the caller's responsibility to ensure that falling back to the
595    /// given version does not introduce unwanted behavior. For applications
596    /// where correctness is essential, the caller should only provide a
597    /// version that is known to be compatible with the version declared in
598    /// the document.
599    ///
600    /// </div>
601    pub fn ast_with_version_fallback(&self, fallback_version: Option<SupportedVersion>) -> Ast<N> {
602        let Some(stmt) = self.version_statement() else {
603            return Ast::Unsupported;
604        };
605        // Parse the version statement, fall back to the fallback, and finally give up
606        // if neither of those works.
607        let Some(version) = stmt
608            .version()
609            .text()
610            .parse::<SupportedVersion>()
611            .ok()
612            .or(fallback_version)
613        else {
614            return Ast::Unsupported;
615        };
616        match version {
617            SupportedVersion::V1(_) => Ast::V1(v1::Ast(self.0.clone())),
618            _ => Ast::Unsupported,
619        }
620    }
621
622    /// Morphs a document of one node type to a document of a different node
623    /// type.
624    pub fn morph<U: TreeNode + NewRoot<N>>(self) -> Document<U> {
625        Document(U::new_root(self.0))
626    }
627}
628
629impl fmt::Debug for Document {
630    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
631        self.0.fmt(f)
632    }
633}
634
635/// Represents a whitespace token in the AST.
636#[derive(Clone, Debug, PartialEq, Eq, Hash)]
637pub struct Whitespace<T: TreeToken = SyntaxToken>(T);
638
639impl<T: TreeToken> AstToken<T> for Whitespace<T> {
640    fn can_cast(kind: SyntaxKind) -> bool {
641        kind == SyntaxKind::Whitespace
642    }
643
644    fn cast(inner: T) -> Option<Self> {
645        match inner.kind() {
646            SyntaxKind::Whitespace => Some(Self(inner)),
647            _ => None,
648        }
649    }
650
651    fn inner(&self) -> &T {
652        &self.0
653    }
654}
655
656/// The prefix for directive comments.
657pub const DIRECTIVE_COMMENT_PREFIX: &str = "#@";
658/// The delimiter between a directive and its contents
659pub const DIRECTIVE_DELIMITER: &str = ":";
660
661/// A single rule in an `#@ except:` comment.
662#[derive(Clone, Debug, PartialEq, Eq, Hash)]
663pub struct ExceptRule {
664    /// The name of the rule to except.
665    pub name: String,
666    /// The span of the rule in the exception comment.
667    pub span: Span,
668}
669
670impl ExceptRule {
671    /// Find the node that this exception comment targets.
672    pub fn target_node(&self, document: &Document) -> Option<SyntaxNode> {
673        let comment = document.inner().descendants_with_tokens().find_map(|d| {
674            let token = d.into_token()?;
675            let comment = Comment::cast(token)?;
676            if comment.kind() == CommentKind::Directive(DirectiveKind::Except)
677                && self.span.within(comment.span())
678            {
679                Some(comment)
680            } else {
681                None
682            }
683        });
684
685        comment.and_then(|c| {
686            c.inner()
687                .siblings_with_tokens(Direction::Next)
688                .find_map(|sibling| {
689                    if let SyntaxElement::Node(node) = sibling {
690                        Some(node)
691                    } else {
692                        None
693                    }
694                })
695        })
696    }
697}
698
699/// A comment directive for WDL tools to respect.
700#[derive(Debug, PartialEq, Eq)]
701pub enum Directive {
702    /// Ignore any rules contained in the set.
703    Except(HashSet<ExceptRule>),
704}
705
706impl Directive {
707    /// The type of this directive.
708    pub fn kind(&self) -> DirectiveKind {
709        match self {
710            Self::Except(_) => DirectiveKind::Except,
711        }
712    }
713
714    /// Consume this `Directive` and return a set of [`ExceptRule`] if it is
715    /// [`Directive::Except`].
716    pub fn into_except(self) -> Option<HashSet<ExceptRule>> {
717        match self {
718            Self::Except(rules) => Some(rules),
719        }
720    }
721}
722
723/// The type of a [`Comment`].
724#[derive(Copy, Clone, Debug, PartialEq, Eq)]
725pub enum CommentKind {
726    /// The comment is a normal line comment
727    Line,
728    /// The comment is a [`Directive`] (starts with
729    /// [`DIRECTIVE_COMMENT_PREFIX`]).
730    Directive(DirectiveKind),
731    /// The comment is a doc comment (starts with [`DOC_COMMENT_PREFIX`]).
732    Documentation,
733}
734
735/// The type of a [`Directive`].
736#[derive(Copy, Clone, Debug, PartialEq, Eq)]
737pub enum DirectiveKind {
738    /// The comment is an `except` directive.
739    Except,
740}
741
742impl FromStr for DirectiveKind {
743    type Err = ();
744
745    fn from_str(s: &str) -> Result<Self, Self::Err> {
746        match s {
747            "except" => Ok(Self::Except),
748            _ => Err(()),
749        }
750    }
751}
752
753/// The prefix for doc comments.
754pub const DOC_COMMENT_PREFIX: &str = "##";
755
756/// Represents a comment token in the AST.
757#[derive(Debug, Clone, PartialEq, Eq, Hash)]
758pub struct Comment<T: TreeToken = SyntaxToken>(T);
759
760impl<T: TreeToken> AstToken<T> for Comment<T> {
761    fn can_cast(kind: SyntaxKind) -> bool {
762        kind == SyntaxKind::Comment
763    }
764
765    fn cast(inner: T) -> Option<Self> {
766        match inner.kind() {
767            SyntaxKind::Comment => Some(Self(inner)),
768            _ => None,
769        }
770    }
771
772    fn inner(&self) -> &T {
773        &self.0
774    }
775}
776
777/// Split a directive comment into its [`DirectiveKind`] and contents.
778///
779/// This takes the entire comment text.
780fn split_directive(comment: &str) -> Option<(DirectiveKind, &str)> {
781    let s = comment.strip_prefix(DIRECTIVE_COMMENT_PREFIX)?;
782    let (directive, contents) = s.trim().split_once(DIRECTIVE_DELIMITER)?;
783    Some((
784        DirectiveKind::from_str(directive.trim_end()).ok()?,
785        contents,
786    ))
787}
788
789impl Comment {
790    /// Try to parse the comment as a directive.
791    pub fn directive(&self) -> Option<Directive> {
792        let text = self.text();
793        let mut offset = self.span().start();
794
795        let (kind, contents) = split_directive(text)?;
796        offset += text.len() - contents.len();
797
798        match kind {
799            DirectiveKind::Except => Some(Directive::Except(HashSet::from_iter(
800                contents.split(',').filter_map(|original_id| {
801                    let trimmed = original_id.trim();
802                    if trimmed.is_empty() {
803                        return None;
804                    }
805
806                    let name = trimmed.to_string();
807                    offset += original_id.len() - name.len();
808
809                    let span = Span::new(offset, name.len());
810                    offset += name.len() + 1; // + 1 for the comma
811
812                    Some(ExceptRule { name, span })
813                }),
814            ))),
815        }
816    }
817
818    /// The type of comment.
819    pub fn kind(&self) -> CommentKind {
820        let text = self.text();
821        if text.starts_with(DOC_COMMENT_PREFIX) {
822            return CommentKind::Documentation;
823        } else if let Some((kind, _)) = split_directive(text) {
824            return CommentKind::Directive(kind);
825        }
826
827        CommentKind::Line
828    }
829
830    /// Gets whether the comment is an inline comment or not.
831    pub fn is_inline_comment(&self) -> bool {
832        // If there is a preceding token that isn't whitespace with a newline, then
833        // the comment is not alone on this line.
834        if let Some(prev) = self.inner().prev_sibling_or_token() {
835            if prev.kind() == SyntaxKind::Whitespace {
836                !prev
837                    .into_token()
838                    .expect("SyntaxKind::Whitespace is a token")
839                    .text()
840                    .contains('\n')
841            } else {
842                true
843            }
844        } else {
845            false
846        }
847    }
848}
849
850/// Represents a version statement in a WDL AST.
851#[derive(Debug, Clone, PartialEq, Eq, Hash)]
852pub struct VersionStatement<N: TreeNode = SyntaxNode>(N);
853
854impl<N: TreeNode> VersionStatement<N> {
855    /// Gets the version of the version statement.
856    pub fn version(&self) -> Version<N::Token> {
857        self.token()
858            .expect("version statement must have a version token")
859    }
860
861    /// Gets the version keyword of the version statement.
862    pub fn keyword(&self) -> v1::VersionKeyword<N::Token> {
863        self.token()
864            .expect("version statement must have a version keyword")
865    }
866}
867
868impl<N: TreeNode> AstNode<N> for VersionStatement<N> {
869    fn can_cast(kind: SyntaxKind) -> bool {
870        kind == SyntaxKind::VersionStatementNode
871    }
872
873    fn cast(inner: N) -> Option<Self> {
874        match inner.kind() {
875            SyntaxKind::VersionStatementNode => Some(Self(inner)),
876            _ => None,
877        }
878    }
879
880    fn inner(&self) -> &N {
881        &self.0
882    }
883}
884
885/// Represents a version in the AST.
886#[derive(Clone, Debug, PartialEq, Eq, Hash)]
887pub struct Version<T: TreeToken = SyntaxToken>(T);
888
889impl<T: TreeToken> AstToken<T> for Version<T> {
890    fn can_cast(kind: SyntaxKind) -> bool {
891        kind == SyntaxKind::Version
892    }
893
894    fn cast(inner: T) -> Option<Self> {
895        match inner.kind() {
896            SyntaxKind::Version => Some(Self(inner)),
897            _ => None,
898        }
899    }
900
901    fn inner(&self) -> &T {
902        &self.0
903    }
904}
905
906/// Represents an identifier token.
907#[derive(Debug, Clone, PartialEq, Eq, Hash)]
908pub struct Ident<T: TreeToken = SyntaxToken>(T);
909
910impl<T: TreeToken> Ident<T> {
911    /// Gets a hashable representation of the identifier.
912    pub fn hashable(&self) -> TokenText<T> {
913        TokenText(self.0.clone())
914    }
915}
916
917impl<T: TreeToken> AstToken<T> for Ident<T> {
918    fn can_cast(kind: SyntaxKind) -> bool {
919        kind == SyntaxKind::Ident
920    }
921
922    fn cast(inner: T) -> Option<Self> {
923        match inner.kind() {
924            SyntaxKind::Ident => Some(Self(inner)),
925            _ => None,
926        }
927    }
928
929    fn inner(&self) -> &T {
930        &self.0
931    }
932}
933
934/// Helper for hashing tokens by their text.
935///
936/// Normally a token's equality and hash implementation work by comparing
937/// the token's element in the tree; thus, two tokens with the same text
938/// but different positions in the tree will compare and hash differently.
939///
940/// With this hash implementation, two tokens compare and hash identically if
941/// their text is identical.
942#[derive(Debug, Clone)]
943pub struct TokenText<T: TreeToken = SyntaxToken>(T);
944
945impl TokenText {
946    /// Gets the text of the underlying token.
947    pub fn text(&self) -> &str {
948        self.0.text()
949    }
950
951    /// Gets the span of the underlying token.
952    pub fn span(&self) -> Span {
953        self.0.span()
954    }
955}
956
957impl<T: TreeToken> PartialEq for TokenText<T> {
958    fn eq(&self, other: &Self) -> bool {
959        self.0.text() == other.0.text()
960    }
961}
962
963impl<T: TreeToken> Eq for TokenText<T> {}
964
965impl<T: TreeToken> std::hash::Hash for TokenText<T> {
966    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
967        self.0.text().hash(state);
968    }
969}
970
971impl<T: TreeToken> std::borrow::Borrow<str> for TokenText<T> {
972    fn borrow(&self) -> &str {
973        self.0.text()
974    }
975}