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