1#![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
69pub trait Documented<N: TreeNode>: AstNode<N> {
71 fn doc_comments(&self) -> Option<Vec<Comment<N::Token>>>;
78}
79
80pub 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
98pub trait TreeNode: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
102 type Token: TreeToken;
104
105 fn parent(&self) -> Option<Self>;
109
110 fn kind(&self) -> SyntaxKind;
112
113 fn text(&self) -> impl fmt::Display;
117
118 fn span(&self) -> Span;
120
121 fn children(&self) -> impl Iterator<Item = Self>;
123
124 fn children_with_tokens(&self) -> impl Iterator<Item = NodeOrToken<Self, Self::Token>>;
126
127 fn first_token(&self) -> Option<Self::Token>;
129
130 fn last_token(&self) -> Option<Self::Token>;
132
133 fn descendants(&self) -> impl Iterator<Item = Self>;
135
136 fn ancestors(&self) -> impl Iterator<Item = Self>;
138}
139
140pub trait TreeToken: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
142 type Node: TreeNode;
144
145 fn parent(&self) -> Self::Node;
147
148 fn kind(&self) -> SyntaxKind;
150
151 fn text(&self) -> &str;
153
154 fn span(&self) -> Span;
156}
157
158pub trait AstNode<N: TreeNode>: Sized {
160 fn can_cast(kind: SyntaxKind) -> bool;
162
163 fn cast(inner: N) -> Option<Self>;
165
166 fn inner(&self) -> &N;
168
169 fn kind(&self) -> SyntaxKind {
171 self.inner().kind()
172 }
173
174 fn text<'a>(&'a self) -> impl fmt::Display
179 where
180 N: 'a,
181 {
182 self.inner().text()
183 }
184
185 fn span(&self) -> Span {
187 self.inner().span()
188 }
189
190 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 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 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 fn child<C>(&self) -> Option<C>
226 where
227 C: AstNode<N>,
228 {
229 self.inner().children().find_map(C::cast)
230 }
231
232 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 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 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 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 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 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
311pub trait AstToken<T: TreeToken>: Sized {
313 fn can_cast(kind: SyntaxKind) -> bool;
315
316 fn cast(inner: T) -> Option<Self>;
318
319 fn inner(&self) -> &T;
321
322 fn kind(&self) -> SyntaxKind {
324 self.inner().kind()
325 }
326
327 fn text<'a>(&'a self) -> &'a str
329 where
330 T: 'a,
331 {
332 self.inner().text()
333 }
334
335 fn span(&self) -> Span {
337 self.inner().span()
338 }
339
340 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
352pub trait NewRoot<N: TreeNode>: Sized {
355 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#[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 Unsupported,
439 V1(v1::Ast<N>),
441}
442
443impl<N: TreeNode> Ast<N> {
444 pub fn as_v1(&self) -> Option<&v1::Ast<N>> {
448 match self {
449 Self::V1(ast) => Some(ast),
450 _ => None,
451 }
452 }
453
454 pub fn into_v1(self) -> Option<v1::Ast<N>> {
456 match self {
457 Self::V1(ast) => Some(ast),
458 _ => None,
459 }
460 }
461
462 pub fn unwrap_v1(self) -> v1::Ast<N> {
468 self.into_v1().expect("the AST is not a V1 AST")
469 }
470}
471
472#[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 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 pub fn version_statement(&self) -> Option<VersionStatement<N>> {
589 self.child()
590 }
591
592 pub fn ast(&self) -> Ast<N> {
594 self.ast_with_version_fallback(None)
595 }
596
597 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 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 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#[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
672pub const DIRECTIVE_COMMENT_PREFIX: &str = "#@";
674pub const DIRECTIVE_DELIMITER: &str = ":";
676
677#[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 pub name: String,
686 pub span: Span,
688}
689
690impl ExceptRule {
691 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#[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 Except(HashSet<ExceptRule>),
728}
729
730impl Directive {
731 pub fn kind(&self) -> DirectiveKind {
733 match self {
734 Self::Except(_) => DirectiveKind::Except,
735 }
736 }
737
738 pub fn into_except(self) -> Option<HashSet<ExceptRule>> {
741 match self {
742 Self::Except(rules) => Some(rules),
743 }
744 }
745}
746
747#[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 Line,
756 Directive(DirectiveKind),
759 Documentation,
761}
762
763#[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 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
785pub const DOC_COMMENT_PREFIX: &str = "##";
787
788#[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
813fn 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 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; Some(ExceptRule { name, span })
850 }),
851 ))),
852 }
853 }
854
855 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 pub fn is_inline_comment(&self) -> bool {
869 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#[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 pub fn version(&self) -> Version<N::Token> {
899 self.token()
900 .expect("version statement must have a version token")
901 }
902
903 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#[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#[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 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#[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 pub fn text(&self) -> &str {
1003 self.0.text()
1004 }
1005
1006 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}