Skip to main content

wdl_ast/v1/
import.rs

1//! V1 AST representation for import statements.
2
3use std::ffi::OsStr;
4use std::path::Path;
5
6use rowan::NodeOrToken;
7use url::Url;
8use wdl_grammar::lexer::v1::is_ident;
9
10use super::AliasKeyword;
11use super::AsKeyword;
12use super::Asterisk;
13use super::FromKeyword;
14use super::ImportKeyword;
15use super::LiteralString;
16use crate::AstNode;
17use crate::AstToken;
18use crate::Ident;
19use crate::Span;
20use crate::SyntaxKind;
21use crate::SyntaxNode;
22use crate::TreeNode;
23use crate::TreeToken;
24
25/// Represents an import statement.
26///
27/// Three forms are represented by a single node kind, distinguished by which
28/// optional children are present.
29///
30/// 1. `import <source> [as <alias>] (alias <Old> as <New>)*` — the existing
31///    import form. User-defined types from `<source>` are brought into the
32///    importing document's scope; tasks and workflows are accessible through
33///    the pseudo-namespace.
34/// 2. `import * from <source>` — every task, workflow, and user-defined type
35///    from `<source>` is brought into the importing document's scope.
36/// 3. `import { <member> [as <Name>], ... } from <source>` — only the listed
37///    items are brought into scope.
38///
39/// `<source>` is either a quoted string URI or an unquoted symbolic module
40/// path; the variants are reachable through `source()`. Forms 2 and 3 do not
41/// accept the trailing `as <alias>` or `alias` clauses.
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub struct ImportStatement<N: TreeNode = SyntaxNode>(N);
44
45/// The source of an [`ImportStatement`].
46///
47/// The grammar guarantees that every well-formed `ImportStatementNode` has
48/// exactly one of these two children, so callers always receive a value.
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub enum ImportSource<N: TreeNode = SyntaxNode> {
51    /// A quoted string URI source, e.g. `"some/file.wdl"`.
52    Uri(LiteralString<N>),
53    /// An unquoted symbolic module path source, e.g. `wizard/spellbook`.
54    ModulePath(SymbolicModulePath<N>),
55}
56
57impl<N: TreeNode> ImportSource<N> {
58    /// The span of the source.
59    pub fn span(&self) -> Span {
60        match self {
61            Self::Uri(uri) => uri.span(),
62            Self::ModulePath(path) => path.span(),
63        }
64    }
65}
66
67/// The shape of an [`ImportStatement`].
68///
69/// Callers dispatch on this and then reach for `members`,
70/// `explicit_namespace`, or `aliases` as the form requires.
71#[derive(Copy, Clone, Debug, PartialEq, Eq)]
72pub enum ImportForm {
73    /// `import <source> [as <alias>] (alias <Old> as <New>)*`. Introduces
74    /// a namespace through which the imported module's tasks and workflows
75    /// are accessed; user-defined types are copied into the importing
76    /// document's scope.
77    ///
78    /// ```wdl
79    /// import "csvkit.wdl"
80    /// import wizard/spellbook as book
81    /// ```
82    Namespace,
83    /// `import * from <source>`. Brings every task, workflow, and
84    /// user-defined type from the source into the importing document's
85    /// scope. No namespace.
86    ///
87    /// ```wdl
88    /// import * from wizard/spellbook
89    /// ```
90    Wildcard,
91    /// `import { <member> [as <Name>], ... } from <source>`. Brings only
92    /// the listed members into the importing document's scope, with an
93    /// optional per-member rename. No namespace.
94    ///
95    /// ```wdl
96    /// import { Cauldron, Wand as Staff } from wizard/spellbook
97    /// ```
98    Selected,
99}
100
101impl<N: TreeNode> ImportStatement<N> {
102    /// Gets the `import` keyword of the statement.
103    pub fn keyword(&self) -> ImportKeyword<N::Token> {
104        self.token()
105            .expect("`ImportStatement` should have an `ImportKeyword`")
106    }
107
108    /// The shape of the import statement.
109    pub fn form(&self) -> ImportForm {
110        if self.wildcard().is_some() {
111            ImportForm::Wildcard
112        } else if self.members().is_some() {
113            ImportForm::Selected
114        } else {
115            ImportForm::Namespace
116        }
117    }
118
119    /// The source of the import, either a quoted URI or a symbolic module
120    /// path.
121    pub fn source(&self) -> ImportSource<N> {
122        if let Some(uri) = self.child::<LiteralString<N>>() {
123            return ImportSource::Uri(uri);
124        }
125        if let Some(path) = self.child::<SymbolicModulePath<N>>() {
126            return ImportSource::ModulePath(path);
127        }
128        unreachable!(
129            "a well-formed `ImportStatementNode` always has a `LiteralString` or \
130             `SymbolicModulePath` child"
131        )
132    }
133
134    /// The braced member-selection clause, present in form 3.
135    pub fn members(&self) -> Option<ImportMembers<N>> {
136        self.child()
137    }
138
139    /// The `*` token, present in the wildcard form.
140    pub fn wildcard(&self) -> Option<Asterisk<N::Token>> {
141        self.token()
142    }
143
144    /// The `from` keyword, present in the wildcard and member forms.
145    pub fn from_keyword(&self) -> Option<FromKeyword<N::Token>> {
146        self.token()
147    }
148
149    /// The explicit namespace introduced by the `as <Ident>` clause.
150    ///
151    /// The `as <alias>` clause is only valid on form 1; the grammar rejects
152    /// it on the wildcard and member-selection forms, and this accessor
153    /// short-circuits on those so a future refactor that moves alias tokens
154    /// onto the statement cannot silently change behavior.
155    pub fn explicit_namespace(&self) -> Option<Ident<N::Token>> {
156        if self.form() != ImportForm::Namespace {
157            return None;
158        }
159        let mut tokens = self.0.children_with_tokens().filter_map(|c| c.into_token());
160        while let Some(t) = tokens.next() {
161            if t.kind() == SyntaxKind::AsKeyword {
162                return tokens.find_map(Ident::cast);
163            }
164        }
165        None
166    }
167
168    /// The `alias <src> as <dst>` clauses on a form-1 import.
169    pub fn aliases(&self) -> impl Iterator<Item = ImportAlias<N>> + use<'_, N> {
170        self.children()
171    }
172
173    /// The derived namespace for tasks and workflows reached through this
174    /// import, along with the span at which it is defined.
175    ///
176    /// Only form 1 introduces a namespace; the wildcard and member-selection
177    /// forms bring items directly into the importing document's scope and
178    /// return `None`. For a quoted form-1 import with no `as <alias>`, the
179    /// namespace is the file stem of the URI. For a symbolic form-1 import
180    /// with no `as <alias>`, the namespace is the last component of the
181    /// module path. An explicit `as <alias>` overrides both.
182    pub fn namespace(&self) -> Option<(String, Span)> {
183        if self.form() != ImportForm::Namespace {
184            return None;
185        }
186
187        if let Some(explicit) = self.explicit_namespace() {
188            return Some((explicit.text().to_string(), explicit.span()));
189        }
190
191        match self.source() {
192            ImportSource::Uri(uri) => {
193                let text = uri.text()?;
194                let stem = match Url::parse(text.text()) {
195                    Ok(url) => Path::new(
196                        urlencoding::decode(url.path_segments()?.next_back()?)
197                            .ok()?
198                            .as_ref(),
199                    )
200                    .file_stem()
201                    .and_then(OsStr::to_str)?
202                    .to_string(),
203                    Err(_) => Path::new(text.text())
204                        .file_stem()
205                        .and_then(OsStr::to_str)?
206                        .to_string(),
207                };
208                if !is_ident(&stem) {
209                    return None;
210                }
211                Some((stem, uri.span()))
212            }
213            ImportSource::ModulePath(path) => {
214                let last = path.components().last()?;
215                Some((last.text().to_string(), last.span()))
216            }
217        }
218    }
219}
220
221impl<N: TreeNode> AstNode<N> for ImportStatement<N> {
222    fn can_cast(kind: SyntaxKind) -> bool {
223        kind == SyntaxKind::ImportStatementNode
224    }
225
226    fn cast(inner: N) -> Option<Self> {
227        match inner.kind() {
228            SyntaxKind::ImportStatementNode => Some(Self(inner)),
229            _ => None,
230        }
231    }
232
233    fn inner(&self) -> &N {
234        &self.0
235    }
236}
237
238/// Represents the unquoted path of a symbolic import.
239///
240/// The path consists of one or more identifier components separated by `/`.
241#[derive(Clone, Debug, PartialEq, Eq)]
242pub struct SymbolicModulePath<N: TreeNode = SyntaxNode>(N);
243
244impl<N: TreeNode> SymbolicModulePath<N> {
245    /// The path components, in source order.
246    pub fn components(&self) -> impl Iterator<Item = Ident<N::Token>> + use<'_, N> {
247        self.tokens()
248    }
249
250    /// The path rendered with `/` separators.
251    pub fn text(&self) -> String {
252        let mut out = String::new();
253        let mut first = true;
254        for c in self.components() {
255            if !first {
256                out.push('/');
257            }
258            out.push_str(c.text());
259            first = false;
260        }
261        out
262    }
263}
264
265impl<N: TreeNode> AstNode<N> for SymbolicModulePath<N> {
266    fn can_cast(kind: SyntaxKind) -> bool {
267        kind == SyntaxKind::SymbolicModulePathNode
268    }
269
270    fn cast(inner: N) -> Option<Self> {
271        match inner.kind() {
272            SyntaxKind::SymbolicModulePathNode => Some(Self(inner)),
273            _ => None,
274        }
275    }
276
277    fn inner(&self) -> &N {
278        &self.0
279    }
280}
281
282/// The braced selected-members clause of an import.
283///
284/// The clause contains one or more comma-separated `ImportMember`
285/// entries inside `{` and `}`. A trailing comma is permitted.
286#[derive(Clone, Debug, PartialEq, Eq)]
287pub struct ImportMembers<N: TreeNode = SyntaxNode>(N);
288
289impl<N: TreeNode> ImportMembers<N> {
290    /// The member entries, in source order.
291    pub fn members(&self) -> impl Iterator<Item = ImportMember<N>> + use<'_, N> {
292        self.children()
293    }
294}
295
296impl<N: TreeNode> AstNode<N> for ImportMembers<N> {
297    fn can_cast(kind: SyntaxKind) -> bool {
298        kind == SyntaxKind::ImportMembersNode
299    }
300
301    fn cast(inner: N) -> Option<Self> {
302        match inner.kind() {
303            SyntaxKind::ImportMembersNode => Some(Self(inner)),
304            _ => None,
305        }
306    }
307
308    fn inner(&self) -> &N {
309        &self.0
310    }
311}
312
313/// One member entry inside a braced `ImportMembers` clause.
314///
315/// An entry is a single identifier optionally followed by `as <alias>` to
316/// rename it locally.
317#[derive(Clone, Debug, PartialEq, Eq)]
318pub struct ImportMember<N: TreeNode = SyntaxNode>(N);
319
320impl<N: TreeNode> ImportMember<N> {
321    /// The name of the imported member.
322    pub fn name(&self) -> Ident<N::Token> {
323        self.idents()
324            .next()
325            .expect("member should have a name identifier")
326    }
327
328    /// The optional alias (the `as <Ident>` clause).
329    pub fn alias(&self) -> Option<Ident<N::Token>> {
330        self.idents().nth(1)
331    }
332
333    /// Returns every `Ident` child token in source order.
334    fn idents(&self) -> impl Iterator<Item = Ident<N::Token>> + use<'_, N> {
335        self.tokens()
336    }
337}
338
339impl<N: TreeNode> AstNode<N> for ImportMember<N> {
340    fn can_cast(kind: SyntaxKind) -> bool {
341        kind == SyntaxKind::ImportMemberNode
342    }
343
344    fn cast(inner: N) -> Option<Self> {
345        match inner.kind() {
346            SyntaxKind::ImportMemberNode => Some(Self(inner)),
347            _ => None,
348        }
349    }
350
351    fn inner(&self) -> &N {
352        &self.0
353    }
354}
355
356/// Represents an `alias <src> as <dst>` clause.
357#[derive(Clone, Debug, PartialEq, Eq)]
358pub struct ImportAlias<N: TreeNode = SyntaxNode>(N);
359
360impl<N: TreeNode> ImportAlias<N> {
361    /// Gets the source and target names of the alias.
362    pub fn names(&self) -> (Ident<N::Token>, Ident<N::Token>) {
363        let mut children = self.0.children_with_tokens().filter_map(|c| match c {
364            NodeOrToken::Node(_) => None,
365            NodeOrToken::Token(t) => Ident::cast(t),
366        });
367
368        let source = children.next().expect("expected a source identifier");
369        let target = children.next().expect("expected a target identifier");
370        (source, target)
371    }
372
373    /// Gets the `alias` keyword of the alias.
374    pub fn alias_keyword(&self) -> AliasKeyword<N::Token> {
375        self.token().expect("alias should have an `alias` keyword")
376    }
377
378    /// Gets the `as` keyword of the alias.
379    pub fn as_keyword(&self) -> AsKeyword<N::Token> {
380        self.token().expect("alias should have an `as` keyword")
381    }
382}
383
384impl<N: TreeNode> AstNode<N> for ImportAlias<N> {
385    fn can_cast(kind: SyntaxKind) -> bool {
386        kind == SyntaxKind::ImportAliasNode
387    }
388
389    fn cast(inner: N) -> Option<Self> {
390        match inner.kind() {
391            SyntaxKind::ImportAliasNode => Some(Self(inner)),
392            _ => None,
393        }
394    }
395
396    fn inner(&self) -> &N {
397        &self.0
398    }
399}
400
401#[cfg(test)]
402mod test {
403    use pretty_assertions::assert_eq;
404
405    use super::*;
406    use crate::Ast;
407    use crate::Document;
408
409    #[test]
410    fn quoted_imports() {
411        let (document, diagnostics) = Document::parse(
412            r#"
413version 1.1
414
415import "foo.wdl"
416import "bar.wdl" as x
417import "baz.wdl" alias A as B alias C as D
418import "qux.wdl" as x alias A as B alias C as D
419"#,
420            None,
421        );
422        assert!(diagnostics.is_empty());
423        let Ast::V1(ast) = document.ast() else {
424            panic!("expected a V1 AST");
425        };
426
427        fn assert_aliases<N: TreeNode>(mut aliases: impl Iterator<Item = ImportAlias<N>>) {
428            let alias = aliases.next().unwrap();
429            let (to, from) = alias.names();
430            assert_eq!(to.text(), "A");
431            assert_eq!(from.text(), "B");
432            let alias = aliases.next().unwrap();
433            let (to, from) = alias.names();
434            assert_eq!(to.text(), "C");
435            assert_eq!(from.text(), "D");
436            assert!(aliases.next().is_none());
437        }
438
439        let imports: Vec<_> = ast.imports().collect();
440        assert_eq!(imports.len(), 4);
441
442        for import in &imports {
443            assert_eq!(import.form(), ImportForm::Namespace);
444            assert!(matches!(import.source(), ImportSource::Uri(_)));
445            assert!(import.wildcard().is_none());
446            assert!(import.members().is_none());
447        }
448
449        assert_eq!(uri_text(&imports[0]), "foo.wdl");
450        assert!(imports[0].explicit_namespace().is_none());
451        assert_eq!(
452            imports[0].namespace().map(|(n, _)| n).as_deref(),
453            Some("foo"),
454        );
455        assert_eq!(imports[0].aliases().count(), 0);
456
457        assert_eq!(uri_text(&imports[1]), "bar.wdl");
458        assert_eq!(imports[1].explicit_namespace().unwrap().text(), "x");
459        assert_eq!(imports[1].namespace().map(|(n, _)| n).as_deref(), Some("x"),);
460        assert_eq!(imports[1].aliases().count(), 0);
461
462        assert_eq!(uri_text(&imports[2]), "baz.wdl");
463        assert!(imports[2].explicit_namespace().is_none());
464        assert_eq!(
465            imports[2].namespace().map(|(n, _)| n).as_deref(),
466            Some("baz"),
467        );
468        assert_aliases(imports[2].aliases());
469
470        assert_eq!(uri_text(&imports[3]), "qux.wdl");
471        assert_eq!(imports[3].explicit_namespace().unwrap().text(), "x");
472        assert_eq!(imports[3].namespace().map(|(n, _)| n).as_deref(), Some("x"),);
473        assert_aliases(imports[3].aliases());
474    }
475
476    fn uri_text(import: &ImportStatement) -> String {
477        match import.source() {
478            ImportSource::Uri(uri) => uri.text().unwrap().text().to_string(),
479            ImportSource::ModulePath(_) => panic!("expected a quoted URI source"),
480        }
481    }
482
483    fn module_path_text(import: &ImportStatement) -> String {
484        match import.source() {
485            ImportSource::ModulePath(path) => path.text(),
486            ImportSource::Uri(_) => panic!("expected a symbolic module path source"),
487        }
488    }
489
490    #[test]
491    fn symbolic_imports() {
492        let (document, diagnostics) = Document::parse(
493            r#"
494version 1.4
495
496import openwdl/csvkit
497import openwdl/csvkit as csv
498import * from openwdl/csvkit
499import { sort } from openwdl/csvkit
500import { CsvSort, CsvSortStable as Stable } from "local.wdl"
501"#,
502            None,
503        );
504        assert!(diagnostics.is_empty(), "diagnostics: {diagnostics:#?}");
505        let Ast::V1(ast) = document.ast() else {
506            panic!("expected a V1 AST");
507        };
508
509        let imports: Vec<_> = ast.imports().collect();
510        assert_eq!(imports.len(), 5);
511
512        // Form 1, symbolic, no alias: `import openwdl/csvkit`.
513        assert_eq!(imports[0].form(), ImportForm::Namespace);
514        assert_eq!(imports[0].keyword().text(), "import");
515        assert_eq!(module_path_text(&imports[0]), "openwdl/csvkit");
516        assert!(imports[0].wildcard().is_none());
517        assert!(imports[0].from_keyword().is_none());
518        assert!(imports[0].members().is_none());
519        assert!(imports[0].explicit_namespace().is_none());
520        assert_eq!(imports[0].aliases().count(), 0);
521        assert_eq!(
522            imports[0].namespace().map(|(n, _)| n).as_deref(),
523            Some("csvkit"),
524        );
525
526        // Form 1, symbolic, aliased: `import openwdl/csvkit as csv`.
527        assert_eq!(imports[1].form(), ImportForm::Namespace);
528        assert_eq!(imports[1].keyword().text(), "import");
529        assert_eq!(module_path_text(&imports[1]), "openwdl/csvkit");
530        assert!(imports[1].wildcard().is_none());
531        assert!(imports[1].from_keyword().is_none());
532        assert!(imports[1].members().is_none());
533        assert_eq!(imports[1].explicit_namespace().unwrap().text(), "csv");
534        assert_eq!(imports[1].aliases().count(), 0);
535        assert_eq!(
536            imports[1].namespace().map(|(n, _)| n).as_deref(),
537            Some("csv"),
538        );
539
540        // Form 2, wildcard, symbolic source: `import * from openwdl/csvkit`.
541        assert_eq!(imports[2].form(), ImportForm::Wildcard);
542        assert_eq!(imports[2].keyword().text(), "import");
543        assert_eq!(module_path_text(&imports[2]), "openwdl/csvkit");
544        assert!(imports[2].wildcard().is_some());
545        assert_eq!(imports[2].from_keyword().unwrap().text(), "from");
546        assert!(imports[2].members().is_none());
547        assert!(imports[2].explicit_namespace().is_none());
548        assert_eq!(imports[2].aliases().count(), 0);
549        assert!(imports[2].namespace().is_none());
550
551        // Form 3, single member, symbolic source:
552        // `import { sort } from openwdl/csvkit`.
553        assert_eq!(imports[3].form(), ImportForm::Selected);
554        assert_eq!(imports[3].keyword().text(), "import");
555        assert_eq!(module_path_text(&imports[3]), "openwdl/csvkit");
556        assert!(imports[3].wildcard().is_none());
557        assert_eq!(imports[3].from_keyword().unwrap().text(), "from");
558        assert!(imports[3].explicit_namespace().is_none());
559        assert_eq!(imports[3].aliases().count(), 0);
560        assert!(imports[3].namespace().is_none());
561        let members: Vec<_> = imports[3].members().unwrap().members().collect();
562        assert_eq!(members.len(), 1);
563        assert_eq!(members[0].name().text(), "sort");
564        assert!(members[0].alias().is_none());
565
566        // Form 3, quoted source, multiple members with per-member alias:
567        // `import { CsvSort, CsvSortStable as Stable } from "local.wdl"`.
568        assert_eq!(imports[4].form(), ImportForm::Selected);
569        assert_eq!(imports[4].keyword().text(), "import");
570        assert_eq!(uri_text(&imports[4]), "local.wdl");
571        assert!(imports[4].wildcard().is_none());
572        assert_eq!(imports[4].from_keyword().unwrap().text(), "from");
573        assert!(imports[4].explicit_namespace().is_none());
574        assert_eq!(imports[4].aliases().count(), 0);
575        assert!(imports[4].namespace().is_none());
576        let members: Vec<_> = imports[4].members().unwrap().members().collect();
577        assert_eq!(members.len(), 2);
578        assert_eq!(members[0].name().text(), "CsvSort");
579        assert!(members[0].alias().is_none());
580        assert_eq!(members[1].name().text(), "CsvSortStable");
581        assert_eq!(members[1].alias().unwrap().text(), "Stable");
582    }
583}