#![warn(missing_docs)]
#![warn(rust_2018_idioms)]
#![warn(rust_2021_compatibility)]
#![warn(missing_debug_implementations)]
#![warn(clippy::missing_docs_in_private_items)]
#![warn(rustdoc::broken_intra_doc_links)]
use std::collections::HashSet;
use std::fmt;
use std::hash::Hash;
use std::str::FromStr;
pub use rowan::Direction;
use rowan::NodeOrToken;
use v1::CloseBrace;
use v1::CloseHeredoc;
use v1::OpenBrace;
use v1::OpenHeredoc;
pub use wdl_grammar::Diagnostic;
pub use wdl_grammar::Label;
pub use wdl_grammar::Severity;
pub use wdl_grammar::Span;
pub use wdl_grammar::SupportedVersion;
pub use wdl_grammar::SyntaxElement;
pub use wdl_grammar::SyntaxKind;
pub use wdl_grammar::SyntaxNode;
pub use wdl_grammar::SyntaxToken;
pub use wdl_grammar::SyntaxTokenExt;
pub use wdl_grammar::SyntaxTree;
pub use wdl_grammar::WorkflowDescriptionLanguage;
pub use wdl_grammar::lexer;
pub use wdl_grammar::version;
pub mod v1;
mod element;
pub use element::*;
pub trait Documented<N: TreeNode>: AstNode<N> {
fn doc_comments(&self) -> Option<Vec<Comment<N::Token>>>;
}
pub fn doc_comments<N: TreeNode>(
preceding_trivia: impl IntoIterator<Item = N::Token>,
) -> impl Iterator<Item = Comment<N::Token>> {
preceding_trivia
.into_iter()
.take_while(|token| {
token.kind() == SyntaxKind::Whitespace || token.kind() == SyntaxKind::Comment
})
.filter_map(|token| {
if token.kind() == SyntaxKind::Comment && token.text().starts_with(DOC_COMMENT_PREFIX) {
Some(Comment::<N::Token>::cast(token).expect("should be a comment"))
} else {
None
}
})
}
pub trait TreeNode: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
type Token: TreeToken;
fn parent(&self) -> Option<Self>;
fn kind(&self) -> SyntaxKind;
fn text(&self) -> impl fmt::Display;
fn span(&self) -> Span;
fn children(&self) -> impl Iterator<Item = Self>;
fn children_with_tokens(&self) -> impl Iterator<Item = NodeOrToken<Self, Self::Token>>;
fn first_token(&self) -> Option<Self::Token>;
fn last_token(&self) -> Option<Self::Token>;
fn descendants(&self) -> impl Iterator<Item = Self>;
fn ancestors(&self) -> impl Iterator<Item = Self>;
}
pub trait TreeToken: Clone + fmt::Debug + PartialEq + Eq + std::hash::Hash {
type Node: TreeNode;
fn parent(&self) -> Self::Node;
fn kind(&self) -> SyntaxKind;
fn text(&self) -> &str;
fn span(&self) -> Span;
}
pub trait AstNode<N: TreeNode>: Sized {
fn can_cast(kind: SyntaxKind) -> bool;
fn cast(inner: N) -> Option<Self>;
fn inner(&self) -> &N;
fn kind(&self) -> SyntaxKind {
self.inner().kind()
}
fn text<'a>(&'a self) -> impl fmt::Display
where
N: 'a,
{
self.inner().text()
}
fn span(&self) -> Span {
self.inner().span()
}
fn token<C>(&self) -> Option<C>
where
C: AstToken<N::Token>,
{
self.inner()
.children_with_tokens()
.filter_map(|e| e.into_token())
.find_map(|t| C::cast(t))
}
fn tokens<'a, C>(&'a self) -> impl Iterator<Item = C>
where
C: AstToken<N::Token>,
N: 'a,
{
self.inner()
.children_with_tokens()
.filter_map(|e| e.into_token().and_then(C::cast))
}
fn last_token<C>(&self) -> Option<C>
where
C: AstToken<N::Token>,
{
self.inner().last_token().and_then(C::cast)
}
fn child<C>(&self) -> Option<C>
where
C: AstNode<N>,
{
self.inner().children().find_map(C::cast)
}
fn children<'a, C>(&'a self) -> impl Iterator<Item = C>
where
C: AstNode<N>,
N: 'a,
{
self.inner().children().filter_map(C::cast)
}
fn parent<'a, P>(&self) -> Option<P>
where
P: AstNode<N>,
N: 'a,
{
P::cast(self.inner().parent()?)
}
fn scope_span<O, C>(&self, include_braces: bool) -> Option<Span>
where
O: AstToken<N::Token>,
C: AstToken<N::Token>,
{
let open = self.token::<O>()?.span();
let close = self.last_token::<C>()?.span();
let start = if include_braces {
open.start()
} else {
open.end()
};
Some(Span::new(start, close.end() - start))
}
fn braced_scope_span(&self, include_braces: bool) -> Option<Span> {
self.scope_span::<OpenBrace<N::Token>, CloseBrace<N::Token>>(include_braces)
}
fn heredoc_scope_span(&self, include_braces: bool) -> Option<Span> {
self.scope_span::<OpenHeredoc<N::Token>, CloseHeredoc<N::Token>>(include_braces)
}
fn descendants<'a, D>(&'a self) -> impl Iterator<Item = D>
where
D: AstNode<N>,
N: 'a,
{
self.inner().descendants().filter_map(|d| D::cast(d))
}
}
pub trait AstToken<T: TreeToken>: Sized {
fn can_cast(kind: SyntaxKind) -> bool;
fn cast(inner: T) -> Option<Self>;
fn inner(&self) -> &T;
fn kind(&self) -> SyntaxKind {
self.inner().kind()
}
fn text<'a>(&'a self) -> &'a str
where
T: 'a,
{
self.inner().text()
}
fn span(&self) -> Span {
self.inner().span()
}
fn parent<'a, P>(&self) -> Option<P>
where
P: AstNode<T::Node>,
T: 'a,
{
P::cast(self.inner().parent())
}
}
pub trait NewRoot<N: TreeNode>: Sized {
fn new_root(root: N) -> Self;
}
impl TreeNode for SyntaxNode {
type Token = SyntaxToken;
fn parent(&self) -> Option<SyntaxNode> {
self.parent()
}
fn kind(&self) -> SyntaxKind {
self.kind()
}
fn children(&self) -> impl Iterator<Item = Self> {
self.children()
}
fn children_with_tokens(&self) -> impl Iterator<Item = NodeOrToken<Self, Self::Token>> {
self.children_with_tokens()
}
fn text(&self) -> impl fmt::Display {
self.text()
}
fn span(&self) -> Span {
let range = self.text_range();
let start = usize::from(range.start());
Span::new(start, usize::from(range.end()) - start)
}
fn first_token(&self) -> Option<Self::Token> {
self.first_token()
}
fn last_token(&self) -> Option<Self::Token> {
self.last_token()
}
fn descendants(&self) -> impl Iterator<Item = Self> {
self.descendants()
}
fn ancestors(&self) -> impl Iterator<Item = Self> {
self.ancestors()
}
}
impl TreeToken for SyntaxToken {
type Node = SyntaxNode;
fn parent(&self) -> SyntaxNode {
self.parent().expect("token should have a parent")
}
fn kind(&self) -> SyntaxKind {
self.kind()
}
fn text(&self) -> &str {
self.text()
}
fn span(&self) -> Span {
let range = self.text_range();
let start = usize::from(range.start());
Span::new(start, usize::from(range.end()) - start)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Ast<N: TreeNode = SyntaxNode> {
Unsupported,
V1(v1::Ast<N>),
}
impl<N: TreeNode> Ast<N> {
pub fn as_v1(&self) -> Option<&v1::Ast<N>> {
match self {
Self::V1(ast) => Some(ast),
_ => None,
}
}
pub fn into_v1(self) -> Option<v1::Ast<N>> {
match self {
Self::V1(ast) => Some(ast),
_ => None,
}
}
pub fn unwrap_v1(self) -> v1::Ast<N> {
self.into_v1().expect("the AST is not a V1 AST")
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Document<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> AstNode<N> for Document<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::RootNode
}
fn cast(inner: N) -> Option<Self> {
if Self::can_cast(inner.kind()) {
Some(Self(inner))
} else {
None
}
}
fn inner(&self) -> &N {
&self.0
}
}
impl Documented<SyntaxNode> for Document<SyntaxNode> {
fn doc_comments(&self) -> Option<Vec<Comment<<SyntaxNode as TreeNode>::Token>>> {
let version_statement = self.child::<VersionStatement>()?;
let version_keyword = version_statement.keyword();
Some(doc_comments::<SyntaxNode>(version_keyword.inner().preceding_trivia()).collect())
}
}
impl Document {
pub fn parse(
source: &str,
fallback_version: Option<SupportedVersion>,
) -> (Self, Vec<Diagnostic>) {
let (tree, diagnostics) = SyntaxTree::parse(source, fallback_version);
(
Document::cast(tree.into_syntax()).expect("document should cast"),
diagnostics,
)
}
}
impl<N: TreeNode> Document<N> {
pub fn version_statement(&self) -> Option<VersionStatement<N>> {
self.child()
}
pub fn ast(&self) -> Ast<N> {
self.ast_with_version_fallback(None)
}
pub fn ast_with_version_fallback(&self, fallback_version: Option<SupportedVersion>) -> Ast<N> {
let Some(stmt) = self.version_statement() else {
return Ast::Unsupported;
};
let Some(version) = stmt
.version()
.text()
.parse::<SupportedVersion>()
.ok()
.or(fallback_version)
else {
return Ast::Unsupported;
};
match version {
SupportedVersion::V1(_) => Ast::V1(v1::Ast(self.0.clone())),
_ => Ast::Unsupported,
}
}
pub fn morph<U: TreeNode + NewRoot<N>>(self) -> Document<U> {
Document(U::new_root(self.0))
}
}
impl fmt::Debug for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Whitespace<T: TreeToken = SyntaxToken>(T);
impl<T: TreeToken> AstToken<T> for Whitespace<T> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::Whitespace
}
fn cast(inner: T) -> Option<Self> {
match inner.kind() {
SyntaxKind::Whitespace => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &T {
&self.0
}
}
pub const DIRECTIVE_COMMENT_PREFIX: &str = "#@";
pub const DIRECTIVE_DELIMITER: &str = ":";
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct ExceptRule {
pub name: String,
pub span: Span,
}
impl ExceptRule {
pub fn target_node(&self, document: &Document) -> Option<SyntaxNode> {
let comment = document.inner().descendants_with_tokens().find_map(|d| {
let token = d.into_token()?;
let comment = Comment::cast(token)?;
if comment.kind() == CommentKind::Directive(DirectiveKind::Except)
&& self.span.within(comment.span())
{
Some(comment)
} else {
None
}
});
comment.and_then(|c| {
c.inner()
.siblings_with_tokens(Direction::Next)
.find_map(|sibling| {
if let SyntaxElement::Node(node) = sibling {
Some(node)
} else {
None
}
})
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Directive {
Except(HashSet<ExceptRule>),
}
impl Directive {
pub fn kind(&self) -> DirectiveKind {
match self {
Self::Except(_) => DirectiveKind::Except,
}
}
pub fn into_except(self) -> Option<HashSet<ExceptRule>> {
match self {
Self::Except(rules) => Some(rules),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CommentKind {
Line,
Directive(DirectiveKind),
Documentation,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum DirectiveKind {
Except,
}
impl FromStr for DirectiveKind {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"except" => Ok(Self::Except),
_ => Err(()),
}
}
}
pub const DOC_COMMENT_PREFIX: &str = "##";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Comment<T: TreeToken = SyntaxToken>(T);
impl<T: TreeToken> AstToken<T> for Comment<T> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::Comment
}
fn cast(inner: T) -> Option<Self> {
match inner.kind() {
SyntaxKind::Comment => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &T {
&self.0
}
}
fn split_directive(comment: &str) -> Option<(DirectiveKind, &str)> {
let s = comment.strip_prefix(DIRECTIVE_COMMENT_PREFIX)?;
let (directive, contents) = s.trim().split_once(DIRECTIVE_DELIMITER)?;
Some((
DirectiveKind::from_str(directive.trim_end()).ok()?,
contents,
))
}
impl Comment {
pub fn directive(&self) -> Option<Directive> {
let text = self.text();
let mut offset = self.span().start();
let (kind, contents) = split_directive(text)?;
offset += text.len() - contents.len();
match kind {
DirectiveKind::Except => Some(Directive::Except(HashSet::from_iter(
contents.split(',').filter_map(|original_id| {
let trimmed = original_id.trim();
if trimmed.is_empty() {
return None;
}
let name = trimmed.to_string();
offset += original_id.len() - name.len();
let span = Span::new(offset, name.len());
offset += name.len() + 1;
Some(ExceptRule { name, span })
}),
))),
}
}
pub fn kind(&self) -> CommentKind {
let text = self.text();
if text.starts_with(DOC_COMMENT_PREFIX) {
return CommentKind::Documentation;
} else if let Some((kind, _)) = split_directive(text) {
return CommentKind::Directive(kind);
}
CommentKind::Line
}
pub fn is_inline_comment(&self) -> bool {
if let Some(prev) = self.inner().prev_sibling_or_token() {
if prev.kind() == SyntaxKind::Whitespace {
!prev
.into_token()
.expect("SyntaxKind::Whitespace is a token")
.text()
.contains('\n')
} else {
true
}
} else {
false
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct VersionStatement<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> VersionStatement<N> {
pub fn version(&self) -> Version<N::Token> {
self.token()
.expect("version statement must have a version token")
}
pub fn keyword(&self) -> v1::VersionKeyword<N::Token> {
self.token()
.expect("version statement must have a version keyword")
}
}
impl<N: TreeNode> AstNode<N> for VersionStatement<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::VersionStatementNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::VersionStatementNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Version<T: TreeToken = SyntaxToken>(T);
impl<T: TreeToken> AstToken<T> for Version<T> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::Version
}
fn cast(inner: T) -> Option<Self> {
match inner.kind() {
SyntaxKind::Version => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &T {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Ident<T: TreeToken = SyntaxToken>(T);
impl<T: TreeToken> Ident<T> {
pub fn hashable(&self) -> TokenText<T> {
TokenText(self.0.clone())
}
}
impl<T: TreeToken> AstToken<T> for Ident<T> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::Ident
}
fn cast(inner: T) -> Option<Self> {
match inner.kind() {
SyntaxKind::Ident => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &T {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct TokenText<T: TreeToken = SyntaxToken>(T);
impl TokenText {
pub fn text(&self) -> &str {
self.0.text()
}
pub fn span(&self) -> Span {
self.0.span()
}
}
impl<T: TreeToken> PartialEq for TokenText<T> {
fn eq(&self, other: &Self) -> bool {
self.0.text() == other.0.text()
}
}
impl<T: TreeToken> Eq for TokenText<T> {}
impl<T: TreeToken> std::hash::Hash for TokenText<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.text().hash(state);
}
}
impl<T: TreeToken> std::borrow::Borrow<str> for TokenText<T> {
fn borrow(&self) -> &str {
self.0.text()
}
}