use std::ffi::OsStr;
use std::path::Path;
use rowan::NodeOrToken;
use url::Url;
use wdl_grammar::lexer::v1::is_ident;
use super::AliasKeyword;
use super::AsKeyword;
use super::Asterisk;
use super::FromKeyword;
use super::ImportKeyword;
use super::LiteralString;
use crate::AstNode;
use crate::AstToken;
use crate::Ident;
use crate::Span;
use crate::SyntaxKind;
use crate::SyntaxNode;
use crate::TreeNode;
use crate::TreeToken;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImportStatement<N: TreeNode = SyntaxNode>(N);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImportSource<N: TreeNode = SyntaxNode> {
Uri(LiteralString<N>),
ModulePath(SymbolicModulePath<N>),
}
impl<N: TreeNode> ImportSource<N> {
pub fn span(&self) -> Span {
match self {
Self::Uri(uri) => uri.span(),
Self::ModulePath(path) => path.span(),
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ImportForm {
Namespace,
Wildcard,
Selected,
}
impl<N: TreeNode> ImportStatement<N> {
pub fn keyword(&self) -> ImportKeyword<N::Token> {
self.token()
.expect("`ImportStatement` should have an `ImportKeyword`")
}
pub fn form(&self) -> ImportForm {
if self.wildcard().is_some() {
ImportForm::Wildcard
} else if self.members().is_some() {
ImportForm::Selected
} else {
ImportForm::Namespace
}
}
pub fn source(&self) -> ImportSource<N> {
if let Some(uri) = self.child::<LiteralString<N>>() {
return ImportSource::Uri(uri);
}
if let Some(path) = self.child::<SymbolicModulePath<N>>() {
return ImportSource::ModulePath(path);
}
unreachable!(
"a well-formed `ImportStatementNode` always has a `LiteralString` or \
`SymbolicModulePath` child"
)
}
pub fn members(&self) -> Option<ImportMembers<N>> {
self.child()
}
pub fn wildcard(&self) -> Option<Asterisk<N::Token>> {
self.token()
}
pub fn from_keyword(&self) -> Option<FromKeyword<N::Token>> {
self.token()
}
pub fn explicit_namespace(&self) -> Option<Ident<N::Token>> {
if self.form() != ImportForm::Namespace {
return None;
}
let mut tokens = self.0.children_with_tokens().filter_map(|c| c.into_token());
while let Some(t) = tokens.next() {
if t.kind() == SyntaxKind::AsKeyword {
return tokens.find_map(Ident::cast);
}
}
None
}
pub fn aliases(&self) -> impl Iterator<Item = ImportAlias<N>> + use<'_, N> {
self.children()
}
pub fn namespace(&self) -> Option<(String, Span)> {
if self.form() != ImportForm::Namespace {
return None;
}
if let Some(explicit) = self.explicit_namespace() {
return Some((explicit.text().to_string(), explicit.span()));
}
match self.source() {
ImportSource::Uri(uri) => {
let text = uri.text()?;
let stem = match Url::parse(text.text()) {
Ok(url) => Path::new(
urlencoding::decode(url.path_segments()?.next_back()?)
.ok()?
.as_ref(),
)
.file_stem()
.and_then(OsStr::to_str)?
.to_string(),
Err(_) => Path::new(text.text())
.file_stem()
.and_then(OsStr::to_str)?
.to_string(),
};
if !is_ident(&stem) {
return None;
}
Some((stem, uri.span()))
}
ImportSource::ModulePath(path) => {
let last = path.components().last()?;
Some((last.text().to_string(), last.span()))
}
}
}
}
impl<N: TreeNode> AstNode<N> for ImportStatement<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::ImportStatementNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::ImportStatementNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SymbolicModulePath<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> SymbolicModulePath<N> {
pub fn components(&self) -> impl Iterator<Item = Ident<N::Token>> + use<'_, N> {
self.tokens()
}
pub fn text(&self) -> String {
let mut out = String::new();
let mut first = true;
for c in self.components() {
if !first {
out.push('/');
}
out.push_str(c.text());
first = false;
}
out
}
}
impl<N: TreeNode> AstNode<N> for SymbolicModulePath<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::SymbolicModulePathNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::SymbolicModulePathNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImportMembers<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> ImportMembers<N> {
pub fn members(&self) -> impl Iterator<Item = ImportMember<N>> + use<'_, N> {
self.children()
}
}
impl<N: TreeNode> AstNode<N> for ImportMembers<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::ImportMembersNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::ImportMembersNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImportMember<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> ImportMember<N> {
pub fn name(&self) -> Ident<N::Token> {
self.idents()
.next()
.expect("member should have a name identifier")
}
pub fn alias(&self) -> Option<Ident<N::Token>> {
self.idents().nth(1)
}
fn idents(&self) -> impl Iterator<Item = Ident<N::Token>> + use<'_, N> {
self.tokens()
}
}
impl<N: TreeNode> AstNode<N> for ImportMember<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::ImportMemberNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::ImportMemberNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ImportAlias<N: TreeNode = SyntaxNode>(N);
impl<N: TreeNode> ImportAlias<N> {
pub fn names(&self) -> (Ident<N::Token>, Ident<N::Token>) {
let mut children = self.0.children_with_tokens().filter_map(|c| match c {
NodeOrToken::Node(_) => None,
NodeOrToken::Token(t) => Ident::cast(t),
});
let source = children.next().expect("expected a source identifier");
let target = children.next().expect("expected a target identifier");
(source, target)
}
pub fn alias_keyword(&self) -> AliasKeyword<N::Token> {
self.token().expect("alias should have an `alias` keyword")
}
pub fn as_keyword(&self) -> AsKeyword<N::Token> {
self.token().expect("alias should have an `as` keyword")
}
}
impl<N: TreeNode> AstNode<N> for ImportAlias<N> {
fn can_cast(kind: SyntaxKind) -> bool {
kind == SyntaxKind::ImportAliasNode
}
fn cast(inner: N) -> Option<Self> {
match inner.kind() {
SyntaxKind::ImportAliasNode => Some(Self(inner)),
_ => None,
}
}
fn inner(&self) -> &N {
&self.0
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
use crate::Ast;
use crate::Document;
#[test]
fn quoted_imports() {
let (document, diagnostics) = Document::parse(
r#"
version 1.1
import "foo.wdl"
import "bar.wdl" as x
import "baz.wdl" alias A as B alias C as D
import "qux.wdl" as x alias A as B alias C as D
"#,
None,
);
assert!(diagnostics.is_empty());
let Ast::V1(ast) = document.ast() else {
panic!("expected a V1 AST");
};
fn assert_aliases<N: TreeNode>(mut aliases: impl Iterator<Item = ImportAlias<N>>) {
let alias = aliases.next().unwrap();
let (to, from) = alias.names();
assert_eq!(to.text(), "A");
assert_eq!(from.text(), "B");
let alias = aliases.next().unwrap();
let (to, from) = alias.names();
assert_eq!(to.text(), "C");
assert_eq!(from.text(), "D");
assert!(aliases.next().is_none());
}
let imports: Vec<_> = ast.imports().collect();
assert_eq!(imports.len(), 4);
for import in &imports {
assert_eq!(import.form(), ImportForm::Namespace);
assert!(matches!(import.source(), ImportSource::Uri(_)));
assert!(import.wildcard().is_none());
assert!(import.members().is_none());
}
assert_eq!(uri_text(&imports[0]), "foo.wdl");
assert!(imports[0].explicit_namespace().is_none());
assert_eq!(
imports[0].namespace().map(|(n, _)| n).as_deref(),
Some("foo"),
);
assert_eq!(imports[0].aliases().count(), 0);
assert_eq!(uri_text(&imports[1]), "bar.wdl");
assert_eq!(imports[1].explicit_namespace().unwrap().text(), "x");
assert_eq!(imports[1].namespace().map(|(n, _)| n).as_deref(), Some("x"),);
assert_eq!(imports[1].aliases().count(), 0);
assert_eq!(uri_text(&imports[2]), "baz.wdl");
assert!(imports[2].explicit_namespace().is_none());
assert_eq!(
imports[2].namespace().map(|(n, _)| n).as_deref(),
Some("baz"),
);
assert_aliases(imports[2].aliases());
assert_eq!(uri_text(&imports[3]), "qux.wdl");
assert_eq!(imports[3].explicit_namespace().unwrap().text(), "x");
assert_eq!(imports[3].namespace().map(|(n, _)| n).as_deref(), Some("x"),);
assert_aliases(imports[3].aliases());
}
fn uri_text(import: &ImportStatement) -> String {
match import.source() {
ImportSource::Uri(uri) => uri.text().unwrap().text().to_string(),
ImportSource::ModulePath(_) => panic!("expected a quoted URI source"),
}
}
fn module_path_text(import: &ImportStatement) -> String {
match import.source() {
ImportSource::ModulePath(path) => path.text(),
ImportSource::Uri(_) => panic!("expected a symbolic module path source"),
}
}
#[test]
fn symbolic_imports() {
let (document, diagnostics) = Document::parse(
r#"
version 1.4
import openwdl/csvkit
import openwdl/csvkit as csv
import * from openwdl/csvkit
import { sort } from openwdl/csvkit
import { CsvSort, CsvSortStable as Stable } from "local.wdl"
"#,
None,
);
assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:#?}");
let Ast::V1(ast) = document.ast() else {
panic!("expected a V1 AST");
};
let imports: Vec<_> = ast.imports().collect();
assert_eq!(imports.len(), 5);
assert_eq!(imports[0].form(), ImportForm::Namespace);
assert_eq!(imports[0].keyword().text(), "import");
assert_eq!(module_path_text(&imports[0]), "openwdl/csvkit");
assert!(imports[0].wildcard().is_none());
assert!(imports[0].from_keyword().is_none());
assert!(imports[0].members().is_none());
assert!(imports[0].explicit_namespace().is_none());
assert_eq!(imports[0].aliases().count(), 0);
assert_eq!(
imports[0].namespace().map(|(n, _)| n).as_deref(),
Some("csvkit"),
);
assert_eq!(imports[1].form(), ImportForm::Namespace);
assert_eq!(imports[1].keyword().text(), "import");
assert_eq!(module_path_text(&imports[1]), "openwdl/csvkit");
assert!(imports[1].wildcard().is_none());
assert!(imports[1].from_keyword().is_none());
assert!(imports[1].members().is_none());
assert_eq!(imports[1].explicit_namespace().unwrap().text(), "csv");
assert_eq!(imports[1].aliases().count(), 0);
assert_eq!(
imports[1].namespace().map(|(n, _)| n).as_deref(),
Some("csv"),
);
assert_eq!(imports[2].form(), ImportForm::Wildcard);
assert_eq!(imports[2].keyword().text(), "import");
assert_eq!(module_path_text(&imports[2]), "openwdl/csvkit");
assert!(imports[2].wildcard().is_some());
assert_eq!(imports[2].from_keyword().unwrap().text(), "from");
assert!(imports[2].members().is_none());
assert!(imports[2].explicit_namespace().is_none());
assert_eq!(imports[2].aliases().count(), 0);
assert!(imports[2].namespace().is_none());
assert_eq!(imports[3].form(), ImportForm::Selected);
assert_eq!(imports[3].keyword().text(), "import");
assert_eq!(module_path_text(&imports[3]), "openwdl/csvkit");
assert!(imports[3].wildcard().is_none());
assert_eq!(imports[3].from_keyword().unwrap().text(), "from");
assert!(imports[3].explicit_namespace().is_none());
assert_eq!(imports[3].aliases().count(), 0);
assert!(imports[3].namespace().is_none());
let members: Vec<_> = imports[3].members().unwrap().members().collect();
assert_eq!(members.len(), 1);
assert_eq!(members[0].name().text(), "sort");
assert!(members[0].alias().is_none());
assert_eq!(imports[4].form(), ImportForm::Selected);
assert_eq!(imports[4].keyword().text(), "import");
assert_eq!(uri_text(&imports[4]), "local.wdl");
assert!(imports[4].wildcard().is_none());
assert_eq!(imports[4].from_keyword().unwrap().text(), "from");
assert!(imports[4].explicit_namespace().is_none());
assert_eq!(imports[4].aliases().count(), 0);
assert!(imports[4].namespace().is_none());
let members: Vec<_> = imports[4].members().unwrap().members().collect();
assert_eq!(members.len(), 2);
assert_eq!(members[0].name().text(), "CsvSort");
assert!(members[0].alias().is_none());
assert_eq!(members[1].name().text(), "CsvSortStable");
assert_eq!(members[1].alias().unwrap().text(), "Stable");
}
}