diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 8d7b5b1d..6220db09 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -28,6 +28,11 @@ Rule specific checks: - [ ] You have added a test case in `wdl-lint/tests/lints` that covers every possible diagnostic emitted for the rule within the file where the rule is implemented. +- [ ] If you have implemented a new `Visitor` callback, you have also + overridden that callback method for the special `Validator` + (`wdl-ast/src/validation.rs`) and `LintVisitor` + (`wdl-lint/src/visitor.rs`) visitors. These are required to ensure the new + visitor callback will execute. - [ ] You have run `wdl-gauntlet --refresh` to ensure that there are no unintended changes to the baseline configuration file (`Gauntlet.toml`). - [ ] You have run `wdl-gauntlet --refresh --arena` to ensure that all of the diff --git a/Arena.toml b/Arena.toml index 7162ffe5..59d8d939 100644 --- a/Arena.toml +++ b/Arena.toml @@ -1430,11 +1430,6 @@ document = "stjudecloud/workflows:/workflows/chipseq/chipseq-standard.wdl" message = "chipseq-standard.wdl:12:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/chipseq/chipseq-standard.wdl/#L12" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/chipseq/chipseq-standard.wdl" -message = "chipseq-standard.wdl:15:10: note[SectionOrdering]: sections are not in order for workflow `chipseq_standard`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/chipseq/chipseq-standard.wdl/#L15" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/dnaseq/dnaseq-core.wdl" message = "dnaseq-core.wdl:115:1: note[LineWidth]: line exceeds maximum width of 90" @@ -1450,26 +1445,11 @@ document = "stjudecloud/workflows:/workflows/dnaseq/dnaseq-standard-fastq.wdl" message = "dnaseq-standard-fastq.wdl:51:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/dnaseq/dnaseq-standard-fastq.wdl/#L51" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/dnaseq/dnaseq-standard-fastq.wdl" -message = "dnaseq-standard-fastq.wdl:9:10: note[SectionOrdering]: sections are not in order for workflow `dnaseq_standard_fastq_experimental`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/dnaseq/dnaseq-standard-fastq.wdl/#L9" - -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/dnaseq/dnaseq-standard.wdl" -message = "dnaseq-standard.wdl:11:10: note[SectionOrdering]: sections are not in order for workflow `dnaseq_standard_experimental`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/dnaseq/dnaseq-standard.wdl/#L11" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/general/alignment-post.wdl" message = "alignment-post.wdl:6:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/general/alignment-post.wdl/#L6" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/general/alignment-post.wdl" -message = "alignment-post.wdl:8:10: note[SectionOrdering]: sections are not in order for workflow `alignment_post`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/general/alignment-post.wdl/#L8" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/general/samtools-merge.wdl" message = "samtools-merge.wdl:23:1: note[LineWidth]: line exceeds maximum width of 90" @@ -1480,11 +1460,6 @@ document = "stjudecloud/workflows:/workflows/general/samtools-merge.wdl" message = "samtools-merge.wdl:35:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/general/samtools-merge.wdl/#L35" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/general/samtools-merge.wdl" -message = "samtools-merge.wdl:7:10: note[SectionOrdering]: sections are not in order for workflow `samtools_merge`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/general/samtools-merge.wdl/#L7" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/qc/quality-check-standard.wdl" message = "quality-check-standard.wdl:110:1: note[LineWidth]: line exceeds maximum width of 90" @@ -1495,41 +1470,11 @@ document = "stjudecloud/workflows:/workflows/qc/quality-check-standard.wdl" message = "quality-check-standard.wdl:150:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/qc/quality-check-standard.wdl/#L150" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/qc/quality-check-standard.wdl" -message = "quality-check-standard.wdl:18:10: note[SectionOrdering]: sections are not in order for workflow `quality_check`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/qc/quality-check-standard.wdl/#L18" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/qc/quality-check-standard.wdl" message = "quality-check-standard.wdl:435:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/qc/quality-check-standard.wdl/#L435" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/reference/gatk-reference.wdl" -message = "gatk-reference.wdl:7:10: note[SectionOrdering]: sections are not in order for workflow `gatk_reference`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/reference/gatk-reference.wdl/#L7" - -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/reference/make-qc-reference.wdl" -message = "make-qc-reference.wdl:6:10: note[SectionOrdering]: sections are not in order for workflow `make_qc_reference`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/reference/make-qc-reference.wdl/#L6" - -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/rnaseq/rnaseq-standard-fastq.wdl" -message = "rnaseq-standard-fastq.wdl:25:10: note[SectionOrdering]: sections are not in order for workflow `rnaseq_standard_fastq`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/rnaseq/rnaseq-standard-fastq.wdl/#L25" - -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/rnaseq/rnaseq-standard.wdl" -message = "rnaseq-standard.wdl:9:10: note[SectionOrdering]: sections are not in order for workflow `rnaseq_standard`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/rnaseq/rnaseq-standard.wdl/#L9" - -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/rnaseq/rnaseq-variant-calling.wdl" -message = "rnaseq-variant-calling.wdl:6:10: note[SectionOrdering]: sections are not in order for workflow `rnaseq_variant_calling`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/rnaseq/rnaseq-variant-calling.wdl/#L6" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/scrnaseq/10x-bam-to-fastqs.wdl" message = "10x-bam-to-fastqs.wdl:28:1: note[LineWidth]: line exceeds maximum width of 90" @@ -1605,11 +1550,6 @@ document = "stjudecloud/workflows:/workflows/scrnaseq/scrnaseq-standard.wdl" message = "scrnaseq-standard.wdl:28:1: note[LineWidth]: line exceeds maximum width of 90" permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/scrnaseq/scrnaseq-standard.wdl/#L28" -[[diagnostics]] -document = "stjudecloud/workflows:/workflows/scrnaseq/scrnaseq-standard.wdl" -message = "scrnaseq-standard.wdl:40:10: note[SectionOrdering]: sections are not in order for workflow `scrnaseq_standard`" -permalink = "https://github.com/stjudecloud/workflows/blob/efdca837bc35fe5647de6aa95989652a5a9648dc/workflows/scrnaseq/scrnaseq-standard.wdl/#L40" - [[diagnostics]] document = "stjudecloud/workflows:/workflows/scrnaseq/scrnaseq-standard.wdl" message = "scrnaseq-standard.wdl:5:1: note[LineWidth]: line exceeds maximum width of 90" diff --git a/Cargo.toml b/Cargo.toml index d53b1174..afc190fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "wdl", "wdl-analysis", "wdl-ast", + "wdl-format", "wdl-gauntlet", "wdl-grammar", "wdl-lint", diff --git a/Gauntlet.toml b/Gauntlet.toml index 3f20e066..43780bc0 100644 --- a/Gauntlet.toml +++ b/Gauntlet.toml @@ -20,15 +20,15 @@ commit_hash = "4536dd2c6719b86f4c50348f0ce354818260cadb" [repositories."broadinstitute/palantir-workflows"] identifier = "broadinstitute/palantir-workflows" -commit_hash = "1e32078d3b57dcb2291534d0caa30f600d40967e" +commit_hash = "4e7fdfa7992dd1bf4c4721e77c81e4d7b8ecf4fe" [repositories."broadinstitute/warp"] identifier = "broadinstitute/warp" -commit_hash = "e49ffbcd72b138ad4dc6ec62c847943908e4abf9" +commit_hash = "25a98392991857f4a8be429075c0c7496834aa88" [repositories."chanzuckerberg/czid-workflows"] identifier = "chanzuckerberg/czid-workflows" -commit_hash = "a7c54be041e9fbd4cfdd9fac99226e6b4f6f3bfd" +commit_hash = "79f77cff3ebc013930cef3d5d63b3b2ca9b32e30" [repositories."getwilds/ww-fastq-to-cram"] identifier = "getwilds/ww-fastq-to-cram" @@ -44,12 +44,12 @@ commit_hash = "c2c13e85efda8ac7aca7f8765fbaa7c2cacb08b2" [repositories."stjudecloud/workflows"] identifier = "stjudecloud/workflows" -commit_hash = "efdca837bc35fe5647de6aa95989652a5a9648dc" +commit_hash = "ec7cb3e21ef900d71db040943d7e14e94fa3b567" filters = ["/template/task-templates.wdl"] [repositories."theiagen/public_health_bioinformatics"] identifier = "theiagen/public_health_bioinformatics" -commit_hash = "59ee38708ffdfc5dbbdd2f0df3ab02064570447a" +commit_hash = "42a06efca2be1935cc8b5936b1fa9164e5646b80" [[diagnostics]] document = "aws-samples/amazon-omics-tutorials:/example-workflows/gatk-best-practices/workflows/somatic-snps-and-indels/mutec2.wdl" @@ -379,39 +379,39 @@ permalink = "https://github.com/biowdl/tasks/blob/2bf875300d90a3c9c8d670b3d99026 [[diagnostics]] document = "broadinstitute/palantir-workflows:/HaplotypeMap/BuildHaplotypeMap.wdl" message = "BuildHaplotypeMap.wdl:17:1: error: a WDL document must start with a version statement" -permalink = "https://github.com/broadinstitute/palantir-workflows/blob/1e32078d3b57dcb2291534d0caa30f600d40967e/HaplotypeMap/BuildHaplotypeMap.wdl/#L17" +permalink = "https://github.com/broadinstitute/palantir-workflows/blob/4e7fdfa7992dd1bf4c4721e77c81e4d7b8ecf4fe/HaplotypeMap/BuildHaplotypeMap.wdl/#L17" [[diagnostics]] document = "broadinstitute/warp:/pipelines/skylab/scATAC/scATAC.wdl" message = "scATAC.wdl:203:9: error: duplicate key `cpu` in runtime section" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/pipelines/skylab/scATAC/scATAC.wdl/#L203" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/pipelines/skylab/scATAC/scATAC.wdl/#L203" [[diagnostics]] document = "broadinstitute/warp:/tasks/broad/GermlineVariantDiscovery.wdl" -message = "GermlineVariantDiscovery.wdl:137:32: error: expected string, but found integer" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/tasks/broad/GermlineVariantDiscovery.wdl/#L137" +message = "GermlineVariantDiscovery.wdl:140:32: error: expected string, but found integer" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/tasks/broad/GermlineVariantDiscovery.wdl/#L140" [[diagnostics]] document = "broadinstitute/warp:/tasks/broad/GermlineVariantDiscovery.wdl" -message = "GermlineVariantDiscovery.wdl:65:32: error: expected string, but found integer" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/tasks/broad/GermlineVariantDiscovery.wdl/#L65" +message = "GermlineVariantDiscovery.wdl:67:32: error: expected string, but found integer" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/tasks/broad/GermlineVariantDiscovery.wdl/#L67" [[diagnostics]] document = "broadinstitute/warp:/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl" message = "UltimaGenomicsWholeGenomeGermlineTasks.wdl:814:27: error: expected string, but found integer" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl/#L814" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl/#L814" [[diagnostics]] document = "broadinstitute/warp:/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl" message = "UltimaGenomicsWholeGenomeGermlineTasks.wdl:866:27: error: expected string, but found integer" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl/#L866" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/tasks/broad/UltimaGenomicsWholeGenomeGermlineTasks.wdl/#L866" [[diagnostics]] document = "broadinstitute/warp:/tests/cemba/pr/CheckCembaOutputs.wdl" message = "CheckCembaOutputs.wdl:1:1: error: a WDL document must start with a version statement" -permalink = "https://github.com/broadinstitute/warp/blob/e49ffbcd72b138ad4dc6ec62c847943908e4abf9/tests/cemba/pr/CheckCembaOutputs.wdl/#L1" +permalink = "https://github.com/broadinstitute/warp/blob/25a98392991857f4a8be429075c0c7496834aa88/tests/cemba/pr/CheckCembaOutputs.wdl/#L1" [[diagnostics]] document = "chanzuckerberg/czid-workflows:/workflows/index-generation/index-generation.wdl" message = "index-generation.wdl:1:9: error: unsupported WDL version `development`" -permalink = "https://github.com/chanzuckerberg/czid-workflows/blob/a7c54be041e9fbd4cfdd9fac99226e6b4f6f3bfd/workflows/index-generation/index-generation.wdl/#L1" +permalink = "https://github.com/chanzuckerberg/czid-workflows/blob/79f77cff3ebc013930cef3d5d63b3b2ca9b32e30/workflows/index-generation/index-generation.wdl/#L1" diff --git a/wdl-analysis/src/engine.rs b/wdl-analysis/src/engine.rs index 7bbe1827..10749f4c 100644 --- a/wdl-analysis/src/engine.rs +++ b/wdl-analysis/src/engine.rs @@ -550,9 +550,13 @@ impl AnalysisEngine { ) -> (GreenNode, Vec) { let start = Instant::now(); let (document, mut diagnostics) = wdl_ast::Document::parse(source); - if let Some(validator) = validator { - diagnostics.extend(validator.validate(&document).err().unwrap_or_default()); + + if diagnostics.is_empty() { + if let Some(validator) = validator { + diagnostics.extend(validator.validate(&document).err().unwrap_or_default()); + } } + log::info!("parsing of `{id}` completed in {:?}", start.elapsed()); (document.syntax().green().into(), diagnostics) } diff --git a/wdl-analysis/src/scope.rs b/wdl-analysis/src/scope.rs index 7c18154b..5d5e8b2f 100644 --- a/wdl-analysis/src/scope.rs +++ b/wdl-analysis/src/scope.rs @@ -921,6 +921,7 @@ impl DocumentScope { | v1::TaskItem::Output(_) | v1::TaskItem::Command(_) | v1::TaskItem::Requirements(_) + | v1::TaskItem::Hints(_) | v1::TaskItem::Runtime(_) | v1::TaskItem::Metadata(_) | v1::TaskItem::ParameterMetadata(_) => continue, @@ -1017,7 +1018,8 @@ impl DocumentScope { v1::WorkflowItem::Input(_) | v1::WorkflowItem::Output(_) | v1::WorkflowItem::Metadata(_) - | v1::WorkflowItem::ParameterMetadata(_) => continue, + | v1::WorkflowItem::ParameterMetadata(_) + | v1::WorkflowItem::Hints(_) => continue, } } diff --git a/wdl-analysis/tests/analysis.rs b/wdl-analysis/tests/analysis.rs index 31a38408..929d440f 100644 --- a/wdl-analysis/tests/analysis.rs +++ b/wdl-analysis/tests/analysis.rs @@ -110,7 +110,7 @@ fn compare_results(test: &Path, results: Vec) -> Result<()> { // Attempt to strip the CWD from the result path let path: Cow<'_, str> = match &path { // Strip the CWD from the path - Some(path) => path.strip_prefix(&cwd).unwrap_or(&path).to_string_lossy(), + Some(path) => path.strip_prefix(&cwd).unwrap_or(path).to_string_lossy(), // Use the id itself if there is no path None => result.id().to_str(), }; diff --git a/wdl-ast/CHANGELOG.md b/wdl-ast/CHANGELOG.md index 38da9c87..45b29ddc 100644 --- a/wdl-ast/CHANGELOG.md +++ b/wdl-ast/CHANGELOG.md @@ -9,11 +9,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Add support for `meta` and `parameter_meta` sections in struct definitions in + WDL 1.2 ([#127](https://github.com/stjude-rust-labs/wdl/pull/127)). +* Add support for omitting `input` keyword in call statement bodies in WDL 1.2 + ([#125](https://github.com/stjude-rust-labs/wdl/pull/125)). +* Add support for the `Directory` type in WDL 1.2 ([#124](https://github.com/stjude-rust-labs/wdl/pull/124)). +* Add support for multi-line strings in WDL 1.2 ([#123](https://github.com/stjude-rust-labs/wdl/pull/123)). +* Add support for `hints` sections in WDL 1.2 ([#121](https://github.com/stjude-rust-labs/wdl/pull/121)). * Add support for `requirements` sections in WDL 1.2 ([#117](https://github.com/stjude-rust-labs/wdl/pull/117)). * Add support for the exponentiation operator in WDL 1.2 ([#111](https://github.com/stjude-rust-labs/wdl/pull/111)). ### Changed +* Removed `Send` and `Sync` constraints from the `Visitor` trait + ([#128](https://github.com/stjude-rust-labs/wdl/pull/128)). * Changed the API for parsing documents; `Document::parse` now returns `(Document, Vec)` rather than a `Parse` type ([#110](https://github.com/stjude-rust-labs/wdl/pull/110)). * The `Type` enumeration, and friends, in `wdl-ast` no longer implement @@ -39,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* Refactored the `Visitor` trait and validation visitors so that they are not +* Refactored the `Visitor` trait and validation visitors so that they are not in a `v1` module ([#95](https://github.com/stjude-rust-labs/wdl/pull/95)). ## 0.3.0 - 06-13-2024 @@ -59,7 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -* Removed the old AST implementation in favor of new new parser; this also +* Removed the old AST implementation in favor of new new parser; this also removes the `experimental` feature from the crate ([#79](https://github.com/stjude-rust-labs/wdl/pull/79)). * Removed dependency on `miette` and `thiserror` in the experimental parser, re-exported key items from `wdl-grammar`'s experimental parser implementation, diff --git a/wdl-ast/src/lib.rs b/wdl-ast/src/lib.rs index 468d9e78..038dc20e 100644 --- a/wdl-ast/src/lib.rs +++ b/wdl-ast/src/lib.rs @@ -42,10 +42,12 @@ pub use rowan::ast::support; pub use rowan::ast::AstChildren; pub use rowan::ast::AstNode; pub use rowan::Direction; +pub use wdl_grammar::version; 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; @@ -210,15 +212,8 @@ impl Document { pub fn ast(&self) -> Ast { self.version_statement() .as_ref() - .map(|s| { - let v = s.version(); - match v.as_str() { - "1.0" | "1.1" | "1.2" => { - Ast::V1(v1::Ast::cast(self.0.clone()).expect("root should cast")) - } - _ => Ast::Unsupported, - } - }) + .and_then(|s| s.version().as_str().parse::().ok()) + .map(|_| Ast::V1(v1::Ast::cast(self.0.clone()).expect("root should cast"))) .unwrap_or(Ast::Unsupported) } @@ -398,3 +393,44 @@ impl AstToken for Ident { &self.0 } } + +/// Helper for hashing any AST token on string representation alone. +/// +/// Normally an AST token's equality and hash implementation work by comparing +/// the token's element in the AST; thus, two `Ident` tokens with the same name +/// but different positions in the tree will compare and hash differently. +#[derive(Debug, Clone)] +pub struct TokenStrHash(T); + +impl TokenStrHash { + /// Constructs a new token hash for the given token. + pub fn new(token: T) -> Self { + Self(token) + } +} + +impl PartialEq for TokenStrHash { + fn eq(&self, other: &Self) -> bool { + self.0.as_str() == other.0.as_str() + } +} + +impl Eq for TokenStrHash {} + +impl std::hash::Hash for TokenStrHash { + fn hash(&self, state: &mut H) { + self.0.as_str().hash(state); + } +} + +impl std::borrow::Borrow for TokenStrHash { + fn borrow(&self) -> &str { + self.0.as_str() + } +} + +impl AsRef for TokenStrHash { + fn as_ref(&self) -> &T { + &self.0 + } +} diff --git a/wdl-ast/src/v1/decls.rs b/wdl-ast/src/v1/decls.rs index af4c6391..0fd673af 100644 --- a/wdl-ast/src/v1/decls.rs +++ b/wdl-ast/src/v1/decls.rs @@ -342,6 +342,8 @@ pub enum PrimitiveTypeKind { String, /// The primitive is a `File`. File, + /// The primitive is a `Directory` + Directory, } /// Represents a primitive type. @@ -359,6 +361,7 @@ impl PrimitiveType { SyntaxKind::FloatTypeKeyword => Some(PrimitiveTypeKind::Float), SyntaxKind::StringTypeKeyword => Some(PrimitiveTypeKind::String), SyntaxKind::FileTypeKeyword => Some(PrimitiveTypeKind::File), + SyntaxKind::DirectoryTypeKeyword => Some(PrimitiveTypeKind::Directory), _ => None, }) .expect("type should have a kind") @@ -412,6 +415,7 @@ impl fmt::Display for PrimitiveType { PrimitiveTypeKind::Float => write!(f, "Float")?, PrimitiveTypeKind::String => write!(f, "String")?, PrimitiveTypeKind::File => write!(f, "File")?, + PrimitiveTypeKind::Directory => write!(f, "Directory")?, } if self.is_optional() { @@ -764,6 +768,7 @@ impl AstNode for Decl { mod test { use super::*; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -785,6 +790,7 @@ task test { Pair[Boolean, Int] h Object i = object {} MyStruct j + Directory k = "foo" } } "#, @@ -801,7 +807,7 @@ task test { let inputs: Vec<_> = tasks[0].inputs().collect(); assert_eq!(inputs.len(), 1); let decls: Vec<_> = inputs[0].declarations().collect(); - assert_eq!(decls.len(), 10); + assert_eq!(decls.len(), 11); // First input declaration let decl = decls[0].clone().unwrap_unbound_decl(); @@ -883,6 +889,20 @@ task test { assert_eq!(decl.ty().to_string(), "MyStruct"); assert_eq!(decl.name().as_str(), "j"); + // Eleventh input declaration + let decl = decls[10].clone().unwrap_bound_decl(); + assert_eq!(decl.ty().to_string(), "Directory"); + assert_eq!(decl.name().as_str(), "k"); + assert_eq!( + decl.expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "foo" + ); + // Use a visitor to count the number of declarations #[derive(Default)] struct MyVisitor { @@ -893,7 +913,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn bound_decl(&mut self, _: &mut Self::State, reason: VisitReason, _: &BoundDecl) { if reason == VisitReason::Enter { @@ -910,7 +937,7 @@ task test { let mut visitor = MyVisitor::default(); document.visit(&mut (), &mut visitor); - assert_eq!(visitor.bound, 5); + assert_eq!(visitor.bound, 6); assert_eq!(visitor.unbound, 5); } } diff --git a/wdl-ast/src/v1/expr.rs b/wdl-ast/src/v1/expr.rs index d1435c50..cca86f97 100644 --- a/wdl-ast/src/v1/expr.rs +++ b/wdl-ast/src/v1/expr.rs @@ -474,6 +474,12 @@ pub enum LiteralExpr { Struct(LiteralStruct), /// The literal is a `None`. None(LiteralNone), + /// The literal is a `hints`. + Hints(LiteralHints), + /// The literal is an `input`. + Input(LiteralInput), + /// The literal is an `output`. + Output(LiteralOutput), } impl LiteralExpr { @@ -596,6 +602,42 @@ impl LiteralExpr { _ => panic!("not a literal `None`"), } } + + /// Unwraps the expression into a literal `hints`. + /// + /// # Panics + /// + /// Panics if the expression is not a literal `hints`. + pub fn unwrap_hints(self) -> LiteralHints { + match self { + Self::Hints(literal) => literal, + _ => panic!("not a literal `hints`"), + } + } + + /// Unwraps the expression into a literal `input`. + /// + /// # Panics + /// + /// Panics if the expression is not a literal `input`. + pub fn unwrap_input(self) -> LiteralInput { + match self { + Self::Input(literal) => literal, + _ => panic!("not a literal `input`"), + } + } + + /// Unwraps the expression into a literal `output`. + /// + /// # Panics + /// + /// Panics if the expression is not a literal `output`. + pub fn unwrap_output(self) -> LiteralOutput { + match self { + Self::Output(literal) => literal, + _ => panic!("not a literal `output`"), + } + } } impl AstNode for LiteralExpr { @@ -617,6 +659,9 @@ impl AstNode for LiteralExpr { | SyntaxKind::LiteralObjectNode | SyntaxKind::LiteralStructNode | SyntaxKind::LiteralNoneNode + | SyntaxKind::LiteralHintsNode + | SyntaxKind::LiteralInputNode + | SyntaxKind::LiteralOutputNode ) } @@ -635,6 +680,9 @@ impl AstNode for LiteralExpr { SyntaxKind::LiteralObjectNode => Some(Self::Object(LiteralObject(syntax))), SyntaxKind::LiteralStructNode => Some(Self::Struct(LiteralStruct(syntax))), SyntaxKind::LiteralNoneNode => Some(Self::None(LiteralNone(syntax))), + SyntaxKind::LiteralHintsNode => Some(Self::Hints(LiteralHints(syntax))), + SyntaxKind::LiteralInputNode => Some(Self::Input(LiteralInput(syntax))), + SyntaxKind::LiteralOutputNode => Some(Self::Output(LiteralOutput(syntax))), _ => None, } } @@ -651,6 +699,9 @@ impl AstNode for LiteralExpr { Self::Object(o) => &o.0, Self::Struct(s) => &s.0, Self::None(n) => &n.0, + Self::Hints(h) => &h.0, + Self::Input(i) => &i.0, + Self::Output(o) => &o.0, } } } @@ -932,21 +983,33 @@ impl AstNode for LiteralFloat { } } +/// Represents the kind of a literal string. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum LiteralStringKind { + /// The string is a single quoted string. + SingleQuoted, + /// The string is a double quoted string. + DoubleQuoted, + /// The string is a multi-line string. + Multiline, +} + /// Represents a literal string. #[derive(Clone, Debug, PartialEq, Eq)] pub struct LiteralString(pub(super) SyntaxNode); impl LiteralString { - /// Gets the quote character of the literal string. - pub fn quote(&self) -> char { + /// Gets the kind of the string literal. + pub fn kind(&self) -> LiteralStringKind { self.0 .children_with_tokens() .find_map(|c| match c.kind() { - SyntaxKind::SingleQuote => Some('\''), - SyntaxKind::DoubleQuote => Some('"'), + SyntaxKind::SingleQuote => Some(LiteralStringKind::SingleQuoted), + SyntaxKind::DoubleQuote => Some(LiteralStringKind::DoubleQuoted), + SyntaxKind::OpenHeredoc => Some(LiteralStringKind::Multiline), _ => None, }) - .expect("string is missing quote tokens") + .expect("string is missing opening token") } /// Gets the parts of the string. @@ -1071,7 +1134,7 @@ impl AstToken for StringText { /// Represents a placeholder in a string or command. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Placeholder(SyntaxNode); +pub struct Placeholder(pub(crate) SyntaxNode); impl Placeholder { /// Returns whether or not placeholder has a tilde (`~`) opening. @@ -1718,6 +1781,247 @@ impl AstNode for LiteralNone { } } +/// Represents a literal `hints`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralHints(SyntaxNode); + +impl LiteralHints { + /// Gets the items of the literal hints. + pub fn items(&self) -> AstChildren { + children(&self.0) + } +} + +impl AstNode for LiteralHints { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralHintsNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralHintsNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents a literal hints item. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralHintsItem(SyntaxNode); + +impl LiteralHintsItem { + /// Gets the name of the hints item. + pub fn name(&self) -> Ident { + token(&self.0).expect("expected an item name") + } + + /// Gets the expression of the hints item. + pub fn expr(&self) -> Expr { + child(&self.0).expect("expected an item expression") + } +} + +impl AstNode for LiteralHintsItem { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralHintsItemNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralHintsItemNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents a literal `input`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralInput(SyntaxNode); + +impl LiteralInput { + /// Gets the items of the literal input. + pub fn items(&self) -> AstChildren { + children(&self.0) + } +} + +impl AstNode for LiteralInput { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralInputNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralInputNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents a literal input item. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralInputItem(SyntaxNode); + +impl LiteralInputItem { + /// Gets the names of the input item. + /// + /// More than one name indicates a struct member path. + pub fn names(&self) -> impl Iterator { + self.0 + .children_with_tokens() + .filter_map(SyntaxElement::into_token) + .filter_map(Ident::cast) + } + + /// Gets the expression of the input item. + pub fn expr(&self) -> Expr { + child(&self.0).expect("expected an item expression") + } +} + +impl AstNode for LiteralInputItem { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralInputItemNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralInputItemNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents a literal `output`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralOutput(SyntaxNode); + +impl LiteralOutput { + /// Gets the items of the literal output. + pub fn items(&self) -> AstChildren { + children(&self.0) + } +} + +impl AstNode for LiteralOutput { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralOutputNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralOutputNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents a literal output item. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LiteralOutputItem(SyntaxNode); + +impl LiteralOutputItem { + /// Gets the names of the output item. + /// + /// More than one name indicates a struct member path. + pub fn names(&self) -> impl Iterator { + self.0 + .children_with_tokens() + .filter_map(SyntaxElement::into_token) + .filter_map(Ident::cast) + } + + /// Gets the expression of the output item. + pub fn expr(&self) -> Expr { + child(&self.0).expect("expected an item expression") + } +} + +impl AstNode for LiteralOutputItem { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::LiteralOutputItemNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::LiteralOutputItemNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + /// Represents a reference to a name. #[derive(Clone, Debug, PartialEq, Eq)] pub struct NameRef(SyntaxNode); @@ -2077,6 +2381,7 @@ mod test { use super::*; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -2120,7 +2425,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -2276,7 +2588,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -2445,7 +2764,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -2479,6 +2805,12 @@ task test { String b = 'world' String c = "Hello, ${name}!" String d = 'String~{'ception'}!' + String e = <<< this is + a multiline \ + string! + ${first} + ${second} + >>> } "#, ); @@ -2492,27 +2824,27 @@ task test { // Task declarations let decls: Vec<_> = tasks[0].declarations().collect(); - assert_eq!(decls.len(), 4); + assert_eq!(decls.len(), 5); // First declaration assert_eq!(decls[0].ty().to_string(), "String"); assert_eq!(decls[0].name().as_str(), "a"); let s = decls[0].expr().unwrap_literal().unwrap_string(); - assert_eq!(s.quote(), '"'); + assert_eq!(s.kind(), LiteralStringKind::DoubleQuoted); assert_eq!(s.text().unwrap().as_str(), "hello"); // Second declaration assert_eq!(decls[1].ty().to_string(), "String"); assert_eq!(decls[1].name().as_str(), "b"); let s = decls[1].expr().unwrap_literal().unwrap_string(); - assert_eq!(s.quote(), '\''); + assert_eq!(s.kind(), LiteralStringKind::SingleQuoted); assert_eq!(s.text().unwrap().as_str(), "world"); // Third declaration assert_eq!(decls[2].ty().to_string(), "String"); assert_eq!(decls[2].name().as_str(), "c"); let s = decls[2].expr().unwrap_literal().unwrap_string(); - assert_eq!(s.quote(), '"'); + assert_eq!(s.kind(), LiteralStringKind::DoubleQuoted); let parts: Vec<_> = s.parts().collect(); assert_eq!(parts.len(), 3); assert_eq!(parts[0].clone().unwrap_text().as_str(), "Hello, "); @@ -2525,7 +2857,7 @@ task test { assert_eq!(decls[3].ty().to_string(), "String"); assert_eq!(decls[3].name().as_str(), "d"); let s = decls[3].expr().unwrap_literal().unwrap_string(); - assert_eq!(s.quote(), '\''); + assert_eq!(s.kind(), LiteralStringKind::SingleQuoted); let parts: Vec<_> = s.parts().collect(); assert_eq!(parts.len(), 3); assert_eq!(parts[0].clone().unwrap_text().as_str(), "String"); @@ -2543,13 +2875,46 @@ task test { ); assert_eq!(parts[2].clone().unwrap_text().as_str(), "!"); + // Fifth declaration + assert_eq!(decls[4].ty().to_string(), "String"); + assert_eq!(decls[4].name().as_str(), "e"); + let s = decls[4].expr().unwrap_literal().unwrap_string(); + assert_eq!(s.kind(), LiteralStringKind::Multiline); + let parts: Vec<_> = s.parts().collect(); + assert_eq!(parts.len(), 5); + assert_eq!( + parts[0].clone().unwrap_text().as_str(), + " this is\n a multiline \\\n string!\n " + ); + let placeholder = parts[1].clone().unwrap_placeholder(); + assert!(!placeholder.has_tilde()); + assert_eq!( + placeholder.expr().unwrap_name_ref().name().as_str(), + "first" + ); + assert_eq!(parts[2].clone().unwrap_text().as_str(), "\n "); + let placeholder = parts[3].clone().unwrap_placeholder(); + assert!(!placeholder.has_tilde()); + assert_eq!( + placeholder.expr().unwrap_name_ref().name().as_str(), + "second" + ); + assert_eq!(parts[4].clone().unwrap_text().as_str(), "\n "); + // Use a visitor to visit all the string literals without placeholders struct MyVisitor(Vec); impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -2782,7 +3147,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -2980,7 +3352,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -3109,7 +3488,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -3240,7 +3626,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -3390,7 +3783,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -3491,7 +3891,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3508,14 +3915,26 @@ task test { } #[test] - fn name_ref() { + fn literal_hints() { let (document, diagnostics) = Document::parse( r#" -version 1.1 +version 1.2 task test { - Int a = 0 - Int b = a + hints { + foo: hints { + bar: "bar" + baz: "baz" + } + bar: "bar" + baz: hints { + a: 1 + b: 10.0 + c: { + "foo": "bar" + } + } + } } "#, ); @@ -3527,25 +3946,418 @@ task test { assert_eq!(tasks.len(), 1); assert_eq!(tasks[0].name().as_str(), "test"); - // Task declarations - let decls: Vec<_> = tasks[0].declarations().collect(); - assert_eq!(decls.len(), 2); + // Task hints + let hints: Vec<_> = tasks[0].hints().collect(); + assert_eq!(hints.len(), 1); + let items: Vec<_> = hints[0].items().collect(); + assert_eq!(items.len(), 3); - // First declaration - assert_eq!(decls[0].ty().to_string(), "Int"); - assert_eq!(decls[0].name().as_str(), "a"); + // First hints item + assert_eq!(items[0].name().as_str(), "foo"); + let inner: Vec<_> = items[0] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 2); + assert_eq!(inner[0].name().as_str(), "bar"); assert_eq!( - decls[0] + inner[0] .expr() .unwrap_literal() - .unwrap_integer() - .value() - .unwrap(), - 0 - ); - - // Second declaration - assert_eq!(decls[1].ty().to_string(), "Int"); + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + assert_eq!(inner[1].name().as_str(), "baz"); + assert_eq!( + inner[1] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "baz" + ); + + // Second hints item + assert_eq!(items[1].name().as_str(), "bar"); + assert_eq!( + items[1] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + + // Third hints item + assert_eq!(items[2].name().as_str(), "baz"); + let inner: Vec<_> = items[2] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 3); + assert_eq!(inner[0].name().as_str(), "a"); + assert_eq!( + inner[0] + .expr() + .unwrap_literal() + .unwrap_integer() + .value() + .unwrap(), + 1 + ); + assert_eq!(inner[1].name().as_str(), "b"); + assert_relative_eq!( + inner[1] + .expr() + .unwrap_literal() + .unwrap_float() + .value() + .unwrap(), + 10.0 + ); + assert_eq!(inner[2].name().as_str(), "c"); + let map: Vec<_> = inner[2] + .expr() + .unwrap_literal() + .unwrap_map() + .items() + .collect(); + assert_eq!(map.len(), 1); + let (k, v) = map[0].key_value(); + assert_eq!( + k.unwrap_literal().unwrap_string().text().unwrap().as_str(), + "foo" + ); + assert_eq!( + v.unwrap_literal().unwrap_string().text().unwrap().as_str(), + "bar" + ); + + // Use a visitor to count the number of literal `hints` in the tree + struct MyVisitor(usize); + + impl Visitor for MyVisitor { + type State = (); + + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } + + fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { + if reason == VisitReason::Enter { + if let Expr::Literal(LiteralExpr::Hints(_)) = expr { + self.0 += 1; + } + } + } + } + + let mut visitor = MyVisitor(0); + document.visit(&mut (), &mut visitor); + assert_eq!(visitor.0, 2); + } + + #[test] + fn literal_input() { + let (document, diagnostics) = Document::parse( + r#" +version 1.2 + +task test { + hints { + inputs: input { + a: hints { + foo: "bar" + } + b.c.d: hints { + bar: "baz" + } + } + } +} +"#, + ); + + assert!(diagnostics.is_empty()); + let ast = document.ast(); + let ast = ast.as_v1().expect("should be a V1 AST"); + let tasks: Vec<_> = ast.tasks().collect(); + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].name().as_str(), "test"); + + // Task hints + let hints: Vec<_> = tasks[0].hints().collect(); + assert_eq!(hints.len(), 1); + let items: Vec<_> = hints[0].items().collect(); + assert_eq!(items.len(), 1); + + // First hints item + assert_eq!(items[0].name().as_str(), "inputs"); + let input: Vec<_> = items[0] + .expr() + .unwrap_literal() + .unwrap_input() + .items() + .collect(); + assert_eq!(input.len(), 2); + assert_eq!( + input[0] + .names() + .map(|i| i.as_str().to_string()) + .collect::>(), + ["a"] + ); + let inner: Vec<_> = input[0] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].name().as_str(), "foo"); + assert_eq!( + inner[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + assert_eq!( + input[1] + .names() + .map(|i| i.as_str().to_string()) + .collect::>(), + ["b", "c", "d"] + ); + let inner: Vec<_> = input[1] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].name().as_str(), "bar"); + assert_eq!( + inner[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "baz" + ); + + // Use a visitor to count the number of literal `hints` in the tree + struct MyVisitor(usize); + + impl Visitor for MyVisitor { + type State = (); + + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } + + fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { + if reason == VisitReason::Enter { + if let Expr::Literal(LiteralExpr::Input(_)) = expr { + self.0 += 1; + } + } + } + } + + let mut visitor = MyVisitor(0); + document.visit(&mut (), &mut visitor); + assert_eq!(visitor.0, 1); + } + + #[test] + fn literal_output() { + let (document, diagnostics) = Document::parse( + r#" +version 1.2 + +task test { + hints { + outputs: output { + a: hints { + foo: "bar" + } + b.c.d: hints { + bar: "baz" + } + } + } +} +"#, + ); + + assert!(diagnostics.is_empty()); + let ast = document.ast(); + let ast = ast.as_v1().expect("should be a V1 AST"); + let tasks: Vec<_> = ast.tasks().collect(); + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].name().as_str(), "test"); + + // Task hints + let hints: Vec<_> = tasks[0].hints().collect(); + assert_eq!(hints.len(), 1); + let items: Vec<_> = hints[0].items().collect(); + assert_eq!(items.len(), 1); + + // First hints item + assert_eq!(items[0].name().as_str(), "outputs"); + let output: Vec<_> = items[0] + .expr() + .unwrap_literal() + .unwrap_output() + .items() + .collect(); + assert_eq!(output.len(), 2); + assert_eq!( + output[0] + .names() + .map(|i| i.as_str().to_string()) + .collect::>(), + ["a"] + ); + let inner: Vec<_> = output[0] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].name().as_str(), "foo"); + assert_eq!( + inner[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + assert_eq!( + output[1] + .names() + .map(|i| i.as_str().to_string()) + .collect::>(), + ["b", "c", "d"] + ); + let inner: Vec<_> = output[1] + .expr() + .unwrap_literal() + .unwrap_hints() + .items() + .collect(); + assert_eq!(inner.len(), 1); + assert_eq!(inner[0].name().as_str(), "bar"); + assert_eq!( + inner[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "baz" + ); + + // Use a visitor to count the number of literal `hints` in the tree + struct MyVisitor(usize); + + impl Visitor for MyVisitor { + type State = (); + + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } + + fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { + if reason == VisitReason::Enter { + if let Expr::Literal(LiteralExpr::Output(_)) = expr { + self.0 += 1; + } + } + } + } + + let mut visitor = MyVisitor(0); + document.visit(&mut (), &mut visitor); + assert_eq!(visitor.0, 1); + } + + #[test] + fn name_ref() { + let (document, diagnostics) = Document::parse( + r#" +version 1.1 + +task test { + Int a = 0 + Int b = a +} +"#, + ); + + assert!(diagnostics.is_empty()); + let ast = document.ast(); + let ast = ast.as_v1().expect("should be a V1 AST"); + let tasks: Vec<_> = ast.tasks().collect(); + assert_eq!(tasks.len(), 1); + assert_eq!(tasks[0].name().as_str(), "test"); + + // Task declarations + let decls: Vec<_> = tasks[0].declarations().collect(); + assert_eq!(decls.len(), 2); + + // First declaration + assert_eq!(decls[0].ty().to_string(), "Int"); + assert_eq!(decls[0].name().as_str(), "a"); + assert_eq!( + decls[0] + .expr() + .unwrap_literal() + .unwrap_integer() + .value() + .unwrap(), + 0 + ); + + // Second declaration + assert_eq!(decls[1].ty().to_string(), "Int"); assert_eq!(decls[1].name().as_str(), "b"); assert_eq!(decls[1].expr().unwrap_name_ref().name().as_str(), "a"); @@ -3555,7 +4367,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Exit { @@ -3636,7 +4455,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3706,7 +4532,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3783,7 +4616,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3862,7 +4702,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3926,7 +4773,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -3990,7 +4844,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4054,7 +4915,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4118,7 +4986,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4198,7 +5073,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4278,7 +5160,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4358,7 +5247,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4438,7 +5334,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4518,7 +5421,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4598,7 +5508,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4678,7 +5595,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4758,7 +5682,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4838,7 +5769,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -4918,7 +5856,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -5021,7 +5966,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -5112,7 +6064,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { @@ -5187,7 +6146,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn expr(&mut self, _: &mut Self::State, reason: VisitReason, expr: &Expr) { if reason == VisitReason::Enter { diff --git a/wdl-ast/src/v1/import.rs b/wdl-ast/src/v1/import.rs index 4c6199b0..d61b2903 100644 --- a/wdl-ast/src/v1/import.rs +++ b/wdl-ast/src/v1/import.rs @@ -163,6 +163,7 @@ mod test { use super::*; use crate::Ast; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -232,7 +233,14 @@ import "qux.wdl" as x alias A as B alias C as D impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn import_statement( &mut self, diff --git a/wdl-ast/src/v1/struct.rs b/wdl-ast/src/v1/struct.rs index 82247461..ad48183d 100644 --- a/wdl-ast/src/v1/struct.rs +++ b/wdl-ast/src/v1/struct.rs @@ -1,5 +1,7 @@ //! V1 AST representation for struct definitions. +use super::MetadataSection; +use super::ParameterMetadataSection; use super::UnboundDecl; use crate::support::children; use crate::token; @@ -20,10 +22,25 @@ impl StructDefinition { token(&self.0).expect("struct should have a name") } - /// Gets the members of the struct. + /// Gets the items in the struct definition. + pub fn items(&self) -> AstChildren { + children(&self.0) + } + + /// Gets the member declarations of the struct. pub fn members(&self) -> AstChildren { children(&self.0) } + + /// Gets the metadata sections of the struct. + pub fn metadata(&self) -> AstChildren { + children(&self.0) + } + + /// Gets the parameter metadata sections of the struct. + pub fn parameter_metadata(&self) -> AstChildren { + children(&self.0) + } } impl AstNode for StructDefinition { @@ -51,6 +68,55 @@ impl AstNode for StructDefinition { } } +/// Represents an item in a struct definition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum StructItem { + /// The item is a member declaration. + Member(UnboundDecl), + /// The item is a metadata section. + Metadata(MetadataSection), + /// The item is a parameter meta section. + ParameterMetadata(ParameterMetadataSection), +} + +impl AstNode for StructItem { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + matches!( + kind, + SyntaxKind::UnboundDeclNode + | SyntaxKind::MetadataSectionNode + | SyntaxKind::ParameterMetadataSectionNode + ) + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::UnboundDeclNode => Some(Self::Member(UnboundDecl(syntax))), + SyntaxKind::MetadataSectionNode => Some(Self::Metadata(MetadataSection(syntax))), + SyntaxKind::ParameterMetadataSectionNode => { + Some(Self::ParameterMetadata(ParameterMetadataSection(syntax))) + } + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + match self { + Self::Member(m) => &m.0, + Self::Metadata(m) => &m.0, + Self::ParameterMetadata(m) => &m.0, + } + } +} + #[cfg(test)] mod test { use pretty_assertions::assert_eq; @@ -58,6 +124,7 @@ mod test { use crate::v1::StructDefinition; use crate::AstToken; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -80,6 +147,16 @@ struct PrimitiveTypes { String? h File i File? j + Directory k + Directory? l + + meta { + ok: "good" + } + + parameter_meta { + a: "foo" + } } struct ComplexTypes { @@ -93,6 +170,15 @@ struct ComplexTypes { Object? h MyType i MyType? j + Array[Directory] k + + meta { + ok: "good" + } + + parameter_meta { + a: "foo" + } } "#, ); @@ -109,7 +195,7 @@ struct ComplexTypes { // Second struct definition assert_eq!(structs[1].name().as_str(), "PrimitiveTypes"); let members: Vec<_> = structs[1].members().collect(); - assert_eq!(members.len(), 10); + assert_eq!(members.len(), 12); // First member assert_eq!(members[0].name().as_str(), "a"); @@ -161,10 +247,20 @@ struct ComplexTypes { assert_eq!(members[9].ty().to_string(), "File?"); assert!(members[9].ty().is_optional()); + // Eleventh member + assert_eq!(members[10].name().as_str(), "k"); + assert_eq!(members[10].ty().to_string(), "Directory"); + assert!(!members[10].ty().is_optional()); + + // Twelfth member + assert_eq!(members[11].name().as_str(), "l"); + assert_eq!(members[11].ty().to_string(), "Directory?"); + assert!(members[11].ty().is_optional()); + // Third struct definition assert_eq!(structs[2].name().as_str(), "ComplexTypes"); let members: Vec<_> = structs[2].members().collect(); - assert_eq!(members.len(), 10); + assert_eq!(members.len(), 11); // First member assert_eq!(members[0].name().as_str(), "a"); @@ -219,13 +315,25 @@ struct ComplexTypes { assert_eq!(members[9].ty().to_string(), "MyType?"); assert!(members[9].ty().is_optional()); + // Eleventh member + assert_eq!(members[10].name().as_str(), "k"); + assert_eq!(members[10].ty().to_string(), "Array[Directory]"); + assert!(!members[10].ty().is_optional()); + // Use a visitor to count the number of struct definitions in the tree struct MyVisitor(usize); impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn struct_definition( &mut self, diff --git a/wdl-ast/src/v1/task.rs b/wdl-ast/src/v1/task.rs index 5d9e5a3c..5e67afcc 100644 --- a/wdl-ast/src/v1/task.rs +++ b/wdl-ast/src/v1/task.rs @@ -8,6 +8,7 @@ use super::LiteralFloat; use super::LiteralInteger; use super::LiteralString; use super::Placeholder; +use super::StructDefinition; use super::WorkflowDefinition; use crate::support; use crate::support::child; @@ -58,6 +59,11 @@ impl TaskDefinition { children(&self.0) } + /// Gets the hints sections of the task. + pub fn hints(&self) -> AstChildren { + children(&self.0) + } + /// Gets the runtime sections of the task. pub fn runtimes(&self) -> AstChildren { children(&self.0) @@ -115,6 +121,8 @@ pub enum TaskItem { Command(CommandSection), /// The item is a requirements section. Requirements(RequirementsSection), + /// The item is a hints section. + Hints(HintsSection), /// The item is a runtime section. Runtime(RuntimeSection), /// The item is a metadata section. @@ -138,6 +146,7 @@ impl AstNode for TaskItem { | SyntaxKind::OutputSectionNode | SyntaxKind::CommandSectionNode | SyntaxKind::RequirementsSectionNode + | SyntaxKind::HintsSectionNode | SyntaxKind::RuntimeSectionNode | SyntaxKind::MetadataSectionNode | SyntaxKind::ParameterMetadataSectionNode @@ -156,6 +165,7 @@ impl AstNode for TaskItem { SyntaxKind::RequirementsSectionNode => { Some(Self::Requirements(RequirementsSection(syntax))) } + SyntaxKind::HintsSectionNode => Some(Self::Hints(HintsSection(syntax))), SyntaxKind::RuntimeSectionNode => Some(Self::Runtime(RuntimeSection(syntax))), SyntaxKind::MetadataSectionNode => Some(Self::Metadata(MetadataSection(syntax))), SyntaxKind::ParameterMetadataSectionNode => { @@ -172,6 +182,7 @@ impl AstNode for TaskItem { Self::Output(o) => &o.0, Self::Command(c) => &c.0, Self::Requirements(r) => &r.0, + Self::Hints(h) => &h.0, Self::Runtime(r) => &r.0, Self::Metadata(m) => &m.0, Self::ParameterMetadata(m) => &m.0, @@ -180,16 +191,27 @@ impl AstNode for TaskItem { } } -/// Represents either a task or a workflow. +/// Represents the parent of a section. #[derive(Clone, Debug, PartialEq, Eq)] -pub enum TaskOrWorkflow { - /// The item is a task. +pub enum SectionParent { + /// The parent is a task. Task(TaskDefinition), - /// The item is a workflow. + /// The parent is a workflow. Workflow(WorkflowDefinition), + /// The parent is a struct. + Struct(StructDefinition), } -impl TaskOrWorkflow { +impl SectionParent { + /// Gets the name of the section parent. + pub fn name(&self) -> Ident { + match self { + Self::Task(t) => t.name(), + Self::Workflow(w) => w.name(), + Self::Struct(s) => s.name(), + } + } + /// Unwraps to a task definition. /// /// # Panics @@ -213,9 +235,21 @@ impl TaskOrWorkflow { _ => panic!("not a workflow definition"), } } + + /// Unwraps to a struct definition. + /// + /// # Panics + /// + /// Panics if it is not a struct definition. + pub fn unwrap_struct(self) -> StructDefinition { + match self { + Self::Struct(def) => def, + _ => panic!("not a struct definition"), + } + } } -impl AstNode for TaskOrWorkflow { +impl AstNode for SectionParent { type Language = WorkflowDescriptionLanguage; fn can_cast(kind: SyntaxKind) -> bool @@ -224,7 +258,9 @@ impl AstNode for TaskOrWorkflow { { matches!( kind, - SyntaxKind::TaskDefinitionNode | SyntaxKind::WorkflowDefinitionNode + SyntaxKind::TaskDefinitionNode + | SyntaxKind::WorkflowDefinitionNode + | SyntaxKind::StructDefinitionNode ) } @@ -235,6 +271,7 @@ impl AstNode for TaskOrWorkflow { match node.kind() { SyntaxKind::TaskDefinitionNode => Some(Self::Task(TaskDefinition(node))), SyntaxKind::WorkflowDefinitionNode => Some(Self::Workflow(WorkflowDefinition(node))), + SyntaxKind::StructDefinitionNode => Some(Self::Struct(StructDefinition(node))), _ => None, } } @@ -243,6 +280,7 @@ impl AstNode for TaskOrWorkflow { match self { Self::Task(t) => &t.0, Self::Workflow(w) => &w.0, + Self::Struct(s) => &s.0, } } } @@ -258,8 +296,8 @@ impl InputSection { } /// Gets the parent of the input section. - pub fn parent(&self) -> TaskOrWorkflow { - TaskOrWorkflow::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -300,8 +338,8 @@ impl OutputSection { } /// Gets the parent of the output section. - pub fn parent(&self) -> TaskOrWorkflow { - TaskOrWorkflow::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -364,8 +402,8 @@ impl CommandSection { } /// Gets the parent of the command section. - pub fn parent(&self) -> TaskDefinition { - TaskDefinition::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -476,8 +514,8 @@ impl RequirementsSection { } /// Gets the parent of the requirements section. - pub fn parent(&self) -> TaskDefinition { - TaskDefinition::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -548,6 +586,89 @@ impl AstNode for RequirementsItem { } } +/// Represents a hints section in a task definition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HintsSection(pub(crate) SyntaxNode); + +impl HintsSection { + /// Gets the items in the hints section. + pub fn items(&self) -> AstChildren { + children(&self.0) + } + + /// Gets the parent of the hints section. + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) + .expect("parent should cast") + } +} + +impl AstNode for HintsSection { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::HintsSectionNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::HintsSectionNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + +/// Represents an item in a hints section. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct HintsItem(SyntaxNode); + +impl HintsItem { + /// Gets the name of the hints item. + pub fn name(&self) -> Ident { + token(&self.0).expect("expected an item name") + } + + /// Gets the expression of the hints item. + pub fn expr(&self) -> Expr { + child(&self.0).expect("expected an item expression") + } +} + +impl AstNode for HintsItem { + type Language = WorkflowDescriptionLanguage; + + fn can_cast(kind: SyntaxKind) -> bool + where + Self: Sized, + { + kind == SyntaxKind::HintsItemNode + } + + fn cast(syntax: SyntaxNode) -> Option + where + Self: Sized, + { + match syntax.kind() { + SyntaxKind::HintsItemNode => Some(Self(syntax)), + _ => None, + } + } + + fn syntax(&self) -> &SyntaxNode { + &self.0 + } +} + /// Represents a runtime section in a task definition. #[derive(Clone, Debug, PartialEq, Eq)] pub struct RuntimeSection(pub(crate) SyntaxNode); @@ -559,8 +680,8 @@ impl RuntimeSection { } /// Gets the parent of the runtime section. - pub fn parent(&self) -> TaskDefinition { - TaskDefinition::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -592,7 +713,7 @@ impl AstNode for RuntimeSection { /// Represents an item in a runtime section. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct RuntimeItem(SyntaxNode); +pub struct RuntimeItem(pub(crate) SyntaxNode); impl RuntimeItem { /// Gets the name of the runtime item. @@ -642,8 +763,8 @@ impl MetadataSection { } /// Gets the parent of the metadata section. - pub fn parent(&self) -> TaskOrWorkflow { - TaskOrWorkflow::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -675,7 +796,7 @@ impl AstNode for MetadataSection { /// Represents a metadata object item. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct MetadataObjectItem(SyntaxNode); +pub struct MetadataObjectItem(pub(crate) SyntaxNode); impl MetadataObjectItem { /// Gets the name of the item. @@ -979,8 +1100,8 @@ impl ParameterMetadataSection { } /// Gets the parent of the parameter metadata section. - pub fn parent(&self) -> TaskOrWorkflow { - TaskOrWorkflow::cast(self.0.parent().expect("should have a parent")) + pub fn parent(&self) -> SectionParent { + SectionParent::cast(self.0.parent().expect("should have a parent")) .expect("parent should cast") } } @@ -1015,6 +1136,7 @@ mod test { use super::*; use crate::v1::UnboundDecl; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -1022,7 +1144,7 @@ mod test { fn tasks() { let (document, diagnostics) = Document::parse( r#" -version 1.1 +version 1.2 task test { input { @@ -1041,6 +1163,10 @@ task test { container: "baz/qux" } + hints { + foo: "bar" + } + runtime { container: "foo/bar" } @@ -1151,6 +1277,26 @@ task test { "baz/qux" ); + // Task hints + let hints: Vec<_> = tasks[0].hints().collect(); + assert_eq!(hints.len(), 1); + + // First task hints + assert_eq!(hints[0].parent().unwrap_task().name().as_str(), "test"); + let items: Vec<_> = hints[0].items().collect(); + assert_eq!(items.len(), 1); + assert_eq!(items[0].name().as_str(), "foo"); + assert_eq!( + items[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + // Task runtimes let runtimes: Vec<_> = tasks[0].runtimes().collect(); assert_eq!(runtimes.len(), 1); @@ -1232,6 +1378,7 @@ task test { outputs: usize, commands: usize, requirements: usize, + hints: usize, runtimes: usize, metadata: usize, param_metadata: usize, @@ -1242,7 +1389,14 @@ task test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn task_definition( &mut self, @@ -1299,6 +1453,17 @@ task test { } } + fn hints_section( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &HintsSection, + ) { + if reason == VisitReason::Enter { + self.hints += 1; + } + } + fn runtime_section( &mut self, _: &mut Self::State, @@ -1352,6 +1517,7 @@ task test { assert_eq!(visitor.outputs, 1); assert_eq!(visitor.commands, 1); assert_eq!(visitor.requirements, 1); + assert_eq!(visitor.hints, 1); assert_eq!(visitor.runtimes, 1); assert_eq!(visitor.metadata, 1); assert_eq!(visitor.param_metadata, 1); diff --git a/wdl-ast/src/v1/workflow.rs b/wdl-ast/src/v1/workflow.rs index a8dc038c..f9048547 100644 --- a/wdl-ast/src/v1/workflow.rs +++ b/wdl-ast/src/v1/workflow.rs @@ -2,6 +2,7 @@ use super::BoundDecl; use super::Expr; +use super::HintsSection; use super::InputSection; use super::MetadataSection; use super::OutputSection; @@ -58,6 +59,11 @@ impl WorkflowDefinition { children(&self.0) } + /// Gets the hints sections of the workflow. + pub fn hints(&self) -> AstChildren { + children(&self.0) + } + /// Gets the private declarations of the workflow. pub fn declarations(&self) -> AstChildren { children(&self.0) @@ -106,6 +112,8 @@ pub enum WorkflowItem { Metadata(MetadataSection), /// The item is a parameter meta section. ParameterMetadata(ParameterMetadataSection), + /// The item is a hints section. + Hints(HintsSection), /// The item is a private bound declaration. Declaration(BoundDecl), } @@ -126,6 +134,7 @@ impl AstNode for WorkflowItem { | SyntaxKind::CallStatementNode | SyntaxKind::MetadataSectionNode | SyntaxKind::ParameterMetadataSectionNode + | SyntaxKind::HintsSectionNode | SyntaxKind::BoundDeclNode ) } @@ -146,6 +155,7 @@ impl AstNode for WorkflowItem { SyntaxKind::ParameterMetadataSectionNode => { Some(Self::ParameterMetadata(ParameterMetadataSection(syntax))) } + SyntaxKind::HintsSectionNode => Some(Self::Hints(HintsSection(syntax))), SyntaxKind::BoundDeclNode => Some(Self::Declaration(BoundDecl(syntax))), _ => None, } @@ -160,6 +170,7 @@ impl AstNode for WorkflowItem { Self::Call(s) => &s.0, Self::Metadata(m) => &m.0, Self::ParameterMetadata(m) => &m.0, + Self::Hints(h) => &h.0, Self::Declaration(d) => &d.0, } } @@ -567,6 +578,7 @@ mod test { use super::*; use crate::v1::UnboundDecl; use crate::Document; + use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -611,6 +623,10 @@ workflow test { } } + hints { + foo: "bar" + } + String x = "private" } "#, @@ -873,6 +889,26 @@ workflow test { "a name to greet" ); + // Workflow hints + let hints: Vec<_> = workflows[0].hints().collect(); + assert_eq!(hints.len(), 1); + + // First workflow hints + assert_eq!(hints[0].parent().unwrap_workflow().name().as_str(), "test"); + let items: Vec<_> = hints[0].items().collect(); + assert_eq!(items.len(), 1); + assert_eq!(items[0].name().as_str(), "foo"); + assert_eq!( + items[0] + .expr() + .unwrap_literal() + .unwrap_string() + .text() + .unwrap() + .as_str(), + "bar" + ); + // Workflow declarations let decls: Vec<_> = workflows[0].declarations().collect(); assert_eq!(decls.len(), 1); @@ -908,7 +944,14 @@ workflow test { impl Visitor for MyVisitor { type State = (); - fn document(&mut self, _: &mut Self::State, _: VisitReason, _: &Document) {} + fn document( + &mut self, + _: &mut Self::State, + _: VisitReason, + _: &Document, + _: SupportedVersion, + ) { + } fn workflow_definition( &mut self, diff --git a/wdl-ast/src/validation.rs b/wdl-ast/src/validation.rs index ab119030..85b9b64b 100644 --- a/wdl-ast/src/validation.rs +++ b/wdl-ast/src/validation.rs @@ -1,17 +1,17 @@ //! Validator for WDL documents. -use std::cmp::Ordering; - use super::v1; use super::Comment; use super::Diagnostic; use super::VisitReason; use super::Whitespace; use crate::Document; +use crate::SupportedVersion; use crate::VersionStatement; use crate::Visitor; mod counts; +mod exprs; mod keys; mod numbers; mod requirements; @@ -85,15 +85,7 @@ impl Validator { if diagnostics.0.is_empty() { Ok(()) } else { - // Sort the diagnostics by start of the primary label - diagnostics - .0 - .sort_by(|a, b| match (a.labels().next(), b.labels().next()) { - (None, None) => Ordering::Equal, - (None, Some(_)) => Ordering::Less, - (Some(_), None) => Ordering::Greater, - (Some(a), Some(b)) => a.span().start().cmp(&b.span().start()), - }); + diagnostics.0.sort(); Err(diagnostics.0) } } @@ -110,6 +102,7 @@ impl Default for Validator { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), ], } } @@ -118,9 +111,15 @@ impl Default for Validator { impl Visitor for Validator { type State = Diagnostics; - fn document(&mut self, state: &mut Self::State, reason: VisitReason, doc: &Document) { + fn document( + &mut self, + state: &mut Self::State, + reason: VisitReason, + doc: &Document, + version: SupportedVersion, + ) { for visitor in self.visitors.iter_mut() { - visitor.document(state, reason, doc); + visitor.document(state, reason, doc, version); } } @@ -241,6 +240,17 @@ impl Visitor for Validator { } } + fn hints_section( + &mut self, + state: &mut Self::State, + reason: VisitReason, + section: &v1::HintsSection, + ) { + for visitor in self.visitors.iter_mut() { + visitor.hints_section(state, reason, section); + } + } + fn runtime_section( &mut self, state: &mut Self::State, @@ -252,6 +262,17 @@ impl Visitor for Validator { } } + fn runtime_item( + &mut self, + state: &mut Self::State, + reason: VisitReason, + item: &v1::RuntimeItem, + ) { + for visitor in self.visitors.iter_mut() { + visitor.runtime_item(state, reason, item); + } + } + fn metadata_section( &mut self, state: &mut Self::State, @@ -285,6 +306,17 @@ impl Visitor for Validator { } } + fn metadata_object_item( + &mut self, + state: &mut Self::State, + reason: VisitReason, + item: &v1::MetadataObjectItem, + ) { + for visitor in self.visitors.iter_mut() { + visitor.metadata_object_item(state, reason, item); + } + } + fn unbound_decl( &mut self, state: &mut Self::State, @@ -314,6 +346,17 @@ impl Visitor for Validator { } } + fn placeholder( + &mut self, + state: &mut Self::State, + reason: VisitReason, + placeholder: &v1::Placeholder, + ) { + for visitor in self.visitors.iter_mut() { + visitor.placeholder(state, reason, placeholder); + } + } + fn conditional_statement( &mut self, state: &mut Self::State, diff --git a/wdl-ast/src/validation/counts.rs b/wdl-ast/src/validation/counts.rs index cb507965..3055c2e9 100644 --- a/wdl-ast/src/validation/counts.rs +++ b/wdl-ast/src/validation/counts.rs @@ -12,9 +12,9 @@ use crate::v1::OutputSection; use crate::v1::ParameterMetadataSection; use crate::v1::RequirementsSection; use crate::v1::RuntimeSection; +use crate::v1::SectionParent; use crate::v1::StructDefinition; use crate::v1::TaskDefinition; -use crate::v1::TaskOrWorkflow; use crate::v1::WorkflowDefinition; use crate::Ast; use crate::AstNode; @@ -24,6 +24,7 @@ use crate::Diagnostics; use crate::Document; use crate::Ident; use crate::Span; +use crate::SupportedVersion; use crate::SyntaxKind; use crate::SyntaxNode; use crate::ToSpan; @@ -96,15 +97,16 @@ fn keyword(node: &SyntaxNode, section: Section) -> SyntaxToken { /// Creates a "duplicate section" diagnostic fn duplicate_section( - parent: TaskOrWorkflow, + parent: SectionParent, section: Section, first: Span, duplicate: &SyntaxNode, ) -> Diagnostic { let token = keyword(duplicate, section); let (context, name) = match parent { - TaskOrWorkflow::Task(t) => ("task", t.name()), - TaskOrWorkflow::Workflow(w) => ("workflow", w.name()), + SectionParent::Task(t) => ("task", t.name()), + SectionParent::Workflow(w) => ("workflow", w.name()), + SectionParent::Struct(w) => ("struct", w.name()), }; Diagnostic::error(format!( @@ -120,7 +122,7 @@ fn duplicate_section( /// Creates the "conflicting section" diagnostic fn conflicting_section( - parent: TaskOrWorkflow, + parent: SectionParent, section: Section, conflicting: &SyntaxNode, first_span: Span, @@ -128,8 +130,9 @@ fn conflicting_section( ) -> Diagnostic { let token = keyword(conflicting, section); let (context, name) = match parent { - TaskOrWorkflow::Task(t) => ("task", t.name()), - TaskOrWorkflow::Workflow(w) => ("workflow", w.name()), + SectionParent::Task(t) => ("task", t.name()), + SectionParent::Workflow(w) => ("workflow", w.name()), + SectionParent::Struct(w) => ("struct", w.name()), }; Diagnostic::error(format!( @@ -186,10 +189,10 @@ pub struct CountingVisitor { requirements: Option, /// The span of the first runtime section in the task. runtime: Option, - /// The span of the first metadata section in the task or workflow. + /// The span of the first metadata section in the task, workflow, or struct. metadata: Option, - /// The span of the first parameter metadata section in the task or - /// workflow. + /// The span of the first parameter metadata section in the task, workflow, + /// or struct. param_metadata: Option, } @@ -209,7 +212,13 @@ impl CountingVisitor { impl Visitor for CountingVisitor { type State = Diagnostics; - fn document(&mut self, state: &mut Self::State, reason: VisitReason, doc: &Document) { + fn document( + &mut self, + state: &mut Self::State, + reason: VisitReason, + doc: &Document, + _: SupportedVersion, + ) { if reason == VisitReason::Enter { // Upon entry of of a document, reset the visitor entirely. *self = Default::default(); @@ -265,6 +274,7 @@ impl Visitor for CountingVisitor { def: &StructDefinition, ) { if reason == VisitReason::Exit { + self.reset(); return; } @@ -272,7 +282,7 @@ impl Visitor for CountingVisitor { state.add(empty_struct(def.name())); } - self.has_task = true; + self.has_struct = true; } fn command_section( @@ -287,7 +297,7 @@ impl Visitor for CountingVisitor { if let Some(command) = self.command { state.add(duplicate_section( - TaskOrWorkflow::Task(section.parent()), + section.parent(), Section::Command, command, section.syntax(), @@ -362,7 +372,7 @@ impl Visitor for CountingVisitor { if let Some(requirements) = self.requirements { state.add(duplicate_section( - TaskOrWorkflow::Task(section.parent()), + section.parent(), Section::Requirements, requirements, section.syntax(), @@ -372,7 +382,7 @@ impl Visitor for CountingVisitor { if let Some(runtime) = self.runtime { state.add(conflicting_section( - TaskOrWorkflow::Task(section.parent()), + section.parent(), Section::Requirements, section.syntax(), runtime, @@ -398,7 +408,7 @@ impl Visitor for CountingVisitor { if let Some(runtime) = self.runtime { state.add(duplicate_section( - TaskOrWorkflow::Task(section.parent()), + section.parent(), Section::Runtime, runtime, section.syntax(), @@ -408,7 +418,7 @@ impl Visitor for CountingVisitor { if let Some(requirements) = self.requirements { state.add(conflicting_section( - TaskOrWorkflow::Task(section.parent()), + section.parent(), Section::Runtime, section.syntax(), requirements, diff --git a/wdl-ast/src/validation/exprs.rs b/wdl-ast/src/validation/exprs.rs new file mode 100644 index 00000000..a9492e0b --- /dev/null +++ b/wdl-ast/src/validation/exprs.rs @@ -0,0 +1,166 @@ +//! Validation of scoped expressions. + +use std::fmt; + +use rowan::ast::support::token; + +use crate::v1; +use crate::version::V1; +use crate::AstNode; +use crate::Diagnostic; +use crate::Diagnostics; +use crate::Document; +use crate::Span; +use crate::SupportedVersion; +use crate::SyntaxKind; +use crate::ToSpan; +use crate::VisitReason; +use crate::Visitor; + +/// Creates a "hints scope required" diagnostic. +fn hints_scope_required(literal: &Literal) -> Diagnostic { + Diagnostic::error(format!( + "`{literal}` literals can only be used within a hints section" + )) + .with_highlight(literal.span()) +} + +/// Creates a "literal cannot nest" diagnostic. +fn literal_cannot_nest(nested: &Literal, outer: &Literal) -> Diagnostic { + Diagnostic::error(format!( + "`{nested}` literals cannot be nested within `{outer}` literals" + )) + .with_label( + format!("this `{nested}` literal cannot be nested"), + nested.span(), + ) + .with_label(format!("the outer `{outer}` literal is here"), outer.span()) +} + +/// Keeps track of the spans of a `hints`, `input`, or `output` literal. +#[derive(Debug, Clone, Copy)] +enum Literal { + /// The literal is a `hints`. + Hints(Span), + /// The literal is an `input`. + Input(Span), + /// The literal is an `output`. + Output(Span), +} + +impl Literal { + /// Gets the span of literal. + fn span(&self) -> Span { + match self { + Self::Hints(s) | Self::Input(s) | Self::Output(s) => *s, + } + } +} + +impl fmt::Display for Literal { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Hints(_) => write!(f, "hints"), + Self::Input(_) => write!(f, "input"), + Self::Output(_) => write!(f, "output"), + } + } +} + +/// An AST visitor that ensures that certain expressions only appear in +/// acceptable scopes. +#[derive(Debug, Default)] +pub struct ScopedExprVisitor { + /// The version of the document we're currently visiting. + version: Option, + /// Whether or not we're currently in a `hints` section. + in_hints_section: bool, + /// The stack of literals encountered. + literals: Vec, +} + +impl Visitor for ScopedExprVisitor { + type State = Diagnostics; + + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + version: SupportedVersion, + ) { + if reason == VisitReason::Exit { + return; + } + + *self = Default::default(); + self.version = Some(version); + } + + fn hints_section(&mut self, _: &mut Self::State, reason: VisitReason, _: &v1::HintsSection) { + self.in_hints_section = reason == VisitReason::Enter; + } + + fn expr(&mut self, state: &mut Self::State, reason: VisitReason, expr: &v1::Expr) { + // Only visit expressions for WDL >=1.2 + if self.version.expect("should have a version") < SupportedVersion::V1(V1::Two) { + return; + } + + if reason == VisitReason::Exit { + match expr { + v1::Expr::Literal(v1::LiteralExpr::Hints(_)) + | v1::Expr::Literal(v1::LiteralExpr::Input(_)) + | v1::Expr::Literal(v1::LiteralExpr::Output(_)) => { + self.literals.pop(); + } + _ => {} + } + return; + } + + let literal = match expr { + v1::Expr::Literal(v1::LiteralExpr::Hints(l)) => Literal::Hints( + token(l.syntax(), SyntaxKind::HintsKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + ), + v1::Expr::Literal(v1::LiteralExpr::Input(l)) => Literal::Input( + token(l.syntax(), SyntaxKind::InputKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + ), + v1::Expr::Literal(v1::LiteralExpr::Output(l)) => Literal::Output( + token(l.syntax(), SyntaxKind::OutputKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + ), + _ => return, + }; + + if self.in_hints_section { + // Check for prohibited nesting + let prohibited = match literal { + Literal::Hints(_) => { + self.literals.len() > 1 + || (self.literals.len() == 1 + && matches!(self.literals[0], Literal::Hints(_))) + } + Literal::Input(_) | Literal::Output(_) => !self.literals.is_empty(), + }; + + if prohibited { + let outer = self.literals.last().expect("should have an outer literal"); + state.add(literal_cannot_nest(&literal, outer)); + } + } else { + // Any use of these literals outside of a `hints` section is prohibited + state.add(hints_scope_required(&literal)); + } + + self.literals.push(literal); + } +} diff --git a/wdl-ast/src/validation/keys.rs b/wdl-ast/src/validation/keys.rs index 56e2c8a3..6f9e326a 100644 --- a/wdl-ast/src/validation/keys.rs +++ b/wdl-ast/src/validation/keys.rs @@ -1,9 +1,10 @@ //! Validation of unique keys in an AST. -use std::collections::HashMap; +use std::collections::HashSet; use std::fmt; use crate::v1::Expr; +use crate::v1::HintsSection; use crate::v1::LiteralExpr; use crate::v1::MetadataObject; use crate::v1::MetadataSection; @@ -16,6 +17,8 @@ use crate::Diagnostics; use crate::Document; use crate::Ident; use crate::Span; +use crate::SupportedVersion; +use crate::TokenStrHash; use crate::VisitReason; use crate::Visitor; @@ -24,6 +27,8 @@ use crate::Visitor; enum Context { /// The error is in the requirements section. RequirementsSection, + /// The error is in the hints section. + HintsSection, /// The error is in a runtime section. RuntimeSection, /// The error is in a metadata section. @@ -42,6 +47,7 @@ impl fmt::Display for Context { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::RequirementsSection => write!(f, "requirements section"), + Self::HintsSection => write!(f, "hints section"), Self::RuntimeSection => write!(f, "runtime section"), Self::MetadataSection => write!(f, "metadata section"), Self::ParameterMetadataSection => write!(f, "parameter metadata section"), @@ -74,7 +80,7 @@ fn conflicting_key(context: Context, name: &Ident, first: Span) -> Diagnostic { /// Checks the given set of keys for duplicates fn check_duplicate_keys( - keys: &mut HashMap, + keys: &mut HashSet>, aliases: &[(&str, &str)], names: impl Iterator, context: Context, @@ -83,7 +89,7 @@ fn check_duplicate_keys( keys.clear(); for name in names { if let Some(first) = keys.get(name.as_str()) { - diagnostics.add(duplicate_key(context, &name, *first)); + diagnostics.add(duplicate_key(context, &name, first.as_ref().span())); continue; } @@ -97,12 +103,12 @@ fn check_duplicate_keys( }; if let Some(first) = keys.get(*alias) { - diagnostics.add(conflicting_key(context, &name, *first)); + diagnostics.add(conflicting_key(context, &name, first.as_ref().span())); break; } } - keys.insert(name.as_str().to_string(), name.span()); + keys.insert(TokenStrHash::new(name)); } } @@ -117,12 +123,18 @@ fn check_duplicate_keys( /// * object literals /// * struct literals #[derive(Default, Debug)] -pub struct UniqueKeysVisitor(HashMap); +pub struct UniqueKeysVisitor(HashSet>); impl Visitor for UniqueKeysVisitor { type State = Diagnostics; - fn document(&mut self, _: &mut Self::State, reason: VisitReason, _: &Document) { + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + _: SupportedVersion, + ) { if reason == VisitReason::Exit { return; } @@ -154,6 +166,25 @@ impl Visitor for UniqueKeysVisitor { ); } + fn hints_section( + &mut self, + state: &mut Self::State, + reason: VisitReason, + section: &HintsSection, + ) { + if reason == VisitReason::Exit { + return; + } + + check_duplicate_keys( + &mut self.0, + &[], + section.items().map(|i| i.name()), + Context::HintsSection, + state, + ); + } + fn runtime_section( &mut self, state: &mut Self::State, @@ -222,8 +253,8 @@ impl Visitor for UniqueKeysVisitor { } // As metadata objects are nested inside of metadata sections and objects, - // use a different map to check the keys - let mut keys = HashMap::new(); + // use a different set to check the keys + let mut keys = HashSet::new(); check_duplicate_keys( &mut keys, &[], diff --git a/wdl-ast/src/validation/numbers.rs b/wdl-ast/src/validation/numbers.rs index b0ddc0cd..dfe3f285 100644 --- a/wdl-ast/src/validation/numbers.rs +++ b/wdl-ast/src/validation/numbers.rs @@ -9,6 +9,7 @@ use crate::Diagnostic; use crate::Diagnostics; use crate::Document; use crate::Span; +use crate::SupportedVersion; use crate::SyntaxKind; use crate::ToSpan; use crate::VisitReason; @@ -46,7 +47,13 @@ pub struct NumberVisitor { impl Visitor for NumberVisitor { type State = Diagnostics; - fn document(&mut self, _: &mut Self::State, reason: VisitReason, _: &Document) { + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + _: SupportedVersion, + ) { if reason == VisitReason::Exit { return; } diff --git a/wdl-ast/src/validation/requirements.rs b/wdl-ast/src/validation/requirements.rs index 6776656a..ed66e030 100644 --- a/wdl-ast/src/validation/requirements.rs +++ b/wdl-ast/src/validation/requirements.rs @@ -6,6 +6,7 @@ use crate::Diagnostic; use crate::Diagnostics; use crate::Document; use crate::Ident; +use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -26,7 +27,13 @@ pub struct RequirementsVisitor; impl Visitor for RequirementsVisitor { type State = Diagnostics; - fn document(&mut self, _: &mut Self::State, reason: VisitReason, _: &Document) { + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + _: SupportedVersion, + ) { if reason == VisitReason::Exit { return; } diff --git a/wdl-ast/src/validation/strings.rs b/wdl-ast/src/validation/strings.rs index 69fbdbef..f745139f 100644 --- a/wdl-ast/src/validation/strings.rs +++ b/wdl-ast/src/validation/strings.rs @@ -1,14 +1,17 @@ //! Validation of string literals in an AST. +use rowan::ast::AstNode; use wdl_grammar::lexer::v1::EscapeToken; use wdl_grammar::lexer::v1::Logos; -use crate::v1::StringText; +use crate::v1; +use crate::v1::LiteralStringKind; use crate::AstToken; use crate::Diagnostic; use crate::Diagnostics; use crate::Document; use crate::Span; +use crate::SupportedVersion; use crate::VisitReason; use crate::Visitor; @@ -123,7 +126,13 @@ pub struct LiteralTextVisitor; impl Visitor for LiteralTextVisitor { type State = Diagnostics; - fn document(&mut self, _: &mut Self::State, reason: VisitReason, _: &Document) { + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + _: SupportedVersion, + ) { if reason == VisitReason::Exit { return; } @@ -132,11 +141,26 @@ impl Visitor for LiteralTextVisitor { *self = Default::default(); } - fn string_text(&mut self, state: &mut Self::State, text: &StringText) { - check_text( - state, - text.syntax().text_range().start().into(), - text.as_str(), - ); + fn string_text(&mut self, state: &mut Self::State, text: &v1::StringText) { + let string = v1::LiteralString::cast(text.syntax().parent().expect("should have a parent")) + .expect("node should cast"); + match string.kind() { + LiteralStringKind::SingleQuoted | LiteralStringKind::DoubleQuoted => { + // Check the text of a normal string to ensure escape sequences are correct and + // characters that are required to be escaped are actually escaped. + check_text( + state, + text.syntax().text_range().start().into(), + text.as_str(), + ); + } + LiteralStringKind::Multiline => { + // Don't check the text of multiline strings as they are treated + // like commands where almost all of the text is literal and the + // only escape is escaping the closing `>>>`; the only + // difference between a multiline string and a command is how + // line continuation whitespace is normalized. + } + } } } diff --git a/wdl-ast/src/validation/version.rs b/wdl-ast/src/validation/version.rs index c58c305b..1d28627c 100644 --- a/wdl-ast/src/validation/version.rs +++ b/wdl-ast/src/validation/version.rs @@ -1,17 +1,17 @@ //! Validation of supported syntax for WDL versions. -use std::str::FromStr; - use rowan::ast::support::token; +use wdl_grammar::version::V1; use wdl_grammar::ToSpan; use crate::v1; +use crate::v1::Expr; use crate::AstNode; -use crate::AstToken; use crate::Diagnostic; use crate::Diagnostics; use crate::Document; use crate::Span; +use crate::SupportedVersion; use crate::SyntaxKind; use crate::VisitReason; use crate::Visitor; @@ -28,58 +28,59 @@ fn requirements_section(span: Span) -> Diagnostic { .with_highlight(span) } -/// Represents a supported V1 WDL version. -// NOTE: it is expected that this enumeration is in increasing order of 1.x versions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum V1 { - /// The document version is 1.0. - Zero, - /// The document version is 1.1. - One, - /// The document version is 1.2. - Two, +/// Creates a "hints section requirement" diagnostic. +fn hints_section(span: Span) -> Diagnostic { + Diagnostic::error("use of the `hints` section requires WDL version 1.2").with_highlight(span) } -/// Represents a supported WDL version. -// NOTE: it is expected that this enumeration is in increasing order of WDL versions. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -enum Version { - /// The document version is 1.x. - V1(V1), +/// Creates a "multi-line string requirement" diagnostic. +fn multiline_string_requirement(span: Span) -> Diagnostic { + Diagnostic::error("use of multi-line strings requires WDL version 1.2").with_highlight(span) } -impl FromStr for Version { - type Err = (); +/// Creates a "directory type" requirement diagnostic. +fn directory_type_requirement(span: Span) -> Diagnostic { + Diagnostic::error("use of the `Directory` type requires WDL version 1.2").with_highlight(span) +} - fn from_str(s: &str) -> Result { - match s { - "1.0" => Ok(Self::V1(V1::Zero)), - "1.1" => Ok(Self::V1(V1::One)), - "1.2" => Ok(Self::V1(V1::Two)), - _ => Err(()), - } - } +/// Creates an "input keyword" requirement diagnostic. +fn input_keyword_requirement(span: Span) -> Diagnostic { + Diagnostic::error("omitting the `input` keyword in a call statement requires WDL version 1.2") + .with_label("missing an `input` keyword before this input", span) + .with_fix("add an `input` keyword followed by a colon before any call inputs") +} + +/// Creates a "struct metadata requirement" diagnostic. +fn struct_metadata_requirement(kind: &str, span: Span) -> Diagnostic { + Diagnostic::error(format!( + "use of a `{kind}` section in a struct definition requires WDL version 1.2" + )) + .with_highlight(span) } /// An AST visitor that ensures the syntax present in the document matches the /// document's declared version. #[derive(Debug, Default)] pub struct VersionVisitor { - /// Stores the version of the WDL document we're visiting. - version: Option, + /// Stores the supported version of the WDL document we're visiting. + version: Option, } impl Visitor for VersionVisitor { type State = Diagnostics; - fn document(&mut self, _: &mut Self::State, reason: VisitReason, document: &Document) { + fn document( + &mut self, + _: &mut Self::State, + reason: VisitReason, + _: &Document, + version: SupportedVersion, + ) { if reason == VisitReason::Exit { return; } - self.version = document - .version_statement() - .and_then(|s| s.version().as_str().parse().ok()); + self.version = Some(version); } fn requirements_section( @@ -93,7 +94,7 @@ impl Visitor for VersionVisitor { } if let Some(version) = self.version { - if version < Version::V1(V1::Two) { + if version < SupportedVersion::V1(V1::Two) { state.add(requirements_section( token(section.syntax(), SyntaxKind::RequirementsKeyword) .expect("should have keyword") @@ -104,6 +105,28 @@ impl Visitor for VersionVisitor { } } + fn hints_section( + &mut self, + state: &mut Self::State, + reason: VisitReason, + section: &v1::HintsSection, + ) { + if reason == VisitReason::Exit { + return; + } + + if let Some(version) = self.version { + if version < SupportedVersion::V1(V1::Two) { + state.add(hints_section( + token(section.syntax(), SyntaxKind::HintsKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + )); + } + } + } + fn expr(&mut self, state: &mut Self::State, reason: VisitReason, expr: &v1::Expr) { if reason == VisitReason::Exit { return; @@ -111,7 +134,7 @@ impl Visitor for VersionVisitor { if let Some(version) = self.version { match expr { - v1::Expr::Exponentiation(e) if version < Version::V1(V1::Two) => { + Expr::Exponentiation(e) if version < SupportedVersion::V1(V1::Two) => { state.add(exponentiation_requirement( token(e.syntax(), SyntaxKind::Exponentiation) .expect("should have operator") @@ -119,8 +142,117 @@ impl Visitor for VersionVisitor { .to_span(), )); } + v1::Expr::Literal(v1::LiteralExpr::String(s)) + if version < SupportedVersion::V1(V1::Two) + && s.kind() == v1::LiteralStringKind::Multiline => + { + state.add(multiline_string_requirement( + s.syntax().text_range().to_span(), + )); + } _ => {} } } } + + fn bound_decl(&mut self, state: &mut Self::State, reason: VisitReason, decl: &v1::BoundDecl) { + if reason == VisitReason::Exit { + return; + } + + if let Some(version) = self.version { + if let v1::Type::Primitive(ty) = decl.ty() { + if version < SupportedVersion::V1(V1::Two) + && ty.kind() == v1::PrimitiveTypeKind::Directory + { + state.add(directory_type_requirement( + ty.syntax().text_range().to_span(), + )); + } + } + } + } + + fn unbound_decl( + &mut self, + state: &mut Self::State, + reason: VisitReason, + decl: &v1::UnboundDecl, + ) { + if reason == VisitReason::Exit { + return; + } + + if let Some(version) = self.version { + if let v1::Type::Primitive(ty) = decl.ty() { + if version < SupportedVersion::V1(V1::Two) + && ty.kind() == v1::PrimitiveTypeKind::Directory + { + state.add(directory_type_requirement( + ty.syntax().text_range().to_span(), + )); + } + } + } + } + + fn call_statement( + &mut self, + state: &mut Self::State, + reason: VisitReason, + stmt: &v1::CallStatement, + ) { + if reason == VisitReason::Exit { + return; + } + + if let Some(version) = self.version { + if version < SupportedVersion::V1(V1::Two) { + // Ensure there is a input keyword child token if there are inputs + if let Some(input) = stmt.inputs().next() { + if rowan::ast::support::token(stmt.syntax(), SyntaxKind::InputKeyword).is_none() + { + state.add(input_keyword_requirement( + input.syntax().text_range().to_span(), + )); + } + } + } + } + } + + fn struct_definition( + &mut self, + state: &mut Self::State, + reason: VisitReason, + def: &v1::StructDefinition, + ) { + if reason == VisitReason::Exit { + return; + } + + if let Some(version) = self.version { + if version < SupportedVersion::V1(V1::Two) { + if let Some(section) = def.metadata().next() { + state.add(struct_metadata_requirement( + "meta", + token(section.syntax(), SyntaxKind::MetaKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + )); + } + + if let Some(section) = def.parameter_metadata().next() { + state.add(struct_metadata_requirement( + "parameter_meta", + token(section.syntax(), SyntaxKind::ParameterMetaKeyword) + .expect("should have keyword") + .text_range() + .to_span(), + )); + } + } + } + } } diff --git a/wdl-ast/src/visitor.rs b/wdl-ast/src/visitor.rs index f096ffe8..dab4ac45 100644 --- a/wdl-ast/src/visitor.rs +++ b/wdl-ast/src/visitor.rs @@ -29,13 +29,17 @@ use crate::v1::CommandSection; use crate::v1::CommandText; use crate::v1::ConditionalStatement; use crate::v1::Expr; +use crate::v1::HintsSection; use crate::v1::ImportStatement; use crate::v1::InputSection; use crate::v1::MetadataObject; +use crate::v1::MetadataObjectItem; use crate::v1::MetadataSection; use crate::v1::OutputSection; use crate::v1::ParameterMetadataSection; +use crate::v1::Placeholder; use crate::v1::RequirementsSection; +use crate::v1::RuntimeItem; use crate::v1::RuntimeSection; use crate::v1::ScatterStatement; use crate::v1::StringText; @@ -44,8 +48,10 @@ use crate::v1::TaskDefinition; use crate::v1::UnboundDecl; use crate::v1::WorkflowDefinition; use crate::AstNode; +use crate::AstToken as _; use crate::Comment; use crate::Document; +use crate::SupportedVersion; use crate::SyntaxKind; use crate::SyntaxNode; use crate::VersionStatement; @@ -58,7 +64,7 @@ use crate::Whitespace; /// that receives both a [VisitReason::Enter] call and a /// matching [VisitReason::Exit] call. #[allow(unused_variables)] -pub trait Visitor: Send + Sync { +pub trait Visitor { /// Represents the external visitation state. type State; @@ -67,7 +73,13 @@ pub trait Visitor: Send + Sync { /// A visitor must implement this method and response to /// `VisitReason::Enter` with resetting any internal state so that a visitor /// may be reused between documents. - fn document(&mut self, state: &mut Self::State, reason: VisitReason, doc: &Document); + fn document( + &mut self, + state: &mut Self::State, + reason: VisitReason, + doc: &Document, + version: SupportedVersion, + ); /// Visits a whitespace token. fn whitespace(&mut self, state: &mut Self::State, whitespace: &Whitespace) {} @@ -159,6 +171,15 @@ pub trait Visitor: Send + Sync { ) { } + /// Visits a hints section node. + fn hints_section( + &mut self, + state: &mut Self::State, + reason: VisitReason, + section: &HintsSection, + ) { + } + /// Visits a runtime section node. fn runtime_section( &mut self, @@ -168,6 +189,9 @@ pub trait Visitor: Send + Sync { ) { } + /// Visits a runtime item node. + fn runtime_item(&mut self, state: &mut Self::State, reason: VisitReason, item: &RuntimeItem) {} + /// Visits a metadata section node. fn metadata_section( &mut self, @@ -195,6 +219,15 @@ pub trait Visitor: Send + Sync { ) { } + /// Visits a metadata object item in a metadata object. + fn metadata_object_item( + &mut self, + state: &mut Self::State, + reason: VisitReason, + item: &MetadataObjectItem, + ) { + } + /// Visits an unbound declaration node. fn unbound_decl(&mut self, state: &mut Self::State, reason: VisitReason, decl: &UnboundDecl) {} @@ -207,6 +240,15 @@ pub trait Visitor: Send + Sync { /// Visits a string text token in a literal string node. fn string_text(&mut self, state: &mut Self::State, text: &StringText) {} + /// Visits a placeholder node. + fn placeholder( + &mut self, + state: &mut Self::State, + reason: VisitReason, + placeholder: &Placeholder, + ) { + } + /// Visits a conditional statement node in a workflow. fn conditional_statement( &mut self, @@ -246,7 +288,14 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor match element.kind() { SyntaxKind::RootNode => { - visitor.document(state, reason, &Document(element.into_node().unwrap())) + let document = Document(element.into_node().unwrap()); + + let version = document + .version_statement() + .and_then(|s| s.version().as_str().parse::().ok()) + .expect("only WDL documents with supported versions can be visited"); + + visitor.document(state, reason, &document, version) } SyntaxKind::VersionStatementNode => visitor.version_statement( state, @@ -306,6 +355,9 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor reason, &RequirementsSection(element.into_node().unwrap()), ), + SyntaxKind::HintsSectionNode => { + visitor.hints_section(state, reason, &HintsSection(element.into_node().unwrap())) + } SyntaxKind::RequirementsItemNode => { // Skip this node as it's part of a requirements section } @@ -315,7 +367,7 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor &RuntimeSection(element.into_node().unwrap()), ), SyntaxKind::RuntimeItemNode => { - // Skip this node as it's part of a runtime section + visitor.runtime_item(state, reason, &RuntimeItem(element.into_node().unwrap())) } SyntaxKind::MetadataSectionNode => visitor.metadata_section( state, @@ -332,9 +384,13 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor reason, &MetadataObject(element.into_node().unwrap()), ), - SyntaxKind::MetadataObjectItemNode - | SyntaxKind::MetadataArrayNode - | SyntaxKind::LiteralNullNode => { + SyntaxKind::MetadataObjectItemNode => visitor.metadata_object_item( + state, + reason, + &MetadataObjectItem(element.into_node().unwrap()), + ), + + SyntaxKind::MetadataArrayNode | SyntaxKind::LiteralNullNode => { // Skip these nodes as they're part of a metadata section } k if Expr::can_cast(k) => visitor.expr( @@ -344,7 +400,10 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor ), SyntaxKind::LiteralMapItemNode | SyntaxKind::LiteralObjectItemNode - | SyntaxKind::LiteralStructItemNode => { + | SyntaxKind::LiteralStructItemNode + | SyntaxKind::LiteralHintsItemNode + | SyntaxKind::LiteralInputItemNode + | SyntaxKind::LiteralOutputItemNode => { // Skip these nodes as they're part of literal expressions } k @ (SyntaxKind::LiteralIntegerNode @@ -357,6 +416,9 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor | SyntaxKind::LiteralMapNode | SyntaxKind::LiteralObjectNode | SyntaxKind::LiteralStructNode + | SyntaxKind::LiteralHintsNode + | SyntaxKind::LiteralInputNode + | SyntaxKind::LiteralOutputNode | SyntaxKind::ParenthesizedExprNode | SyntaxKind::NameRefNode | SyntaxKind::IfExprNode @@ -380,8 +442,10 @@ pub(crate) fn visit(root: &SyntaxNode, state: &mut V::State, visitor | SyntaxKind::AccessExprNode) => { unreachable!("`{k:?}` should be handled by `Expr::can_cast`") } - SyntaxKind::PlaceholderNode - | SyntaxKind::PlaceholderSepOptionNode + SyntaxKind::PlaceholderNode => { + visitor.placeholder(state, reason, &Placeholder(element.into_node().unwrap())) + } + SyntaxKind::PlaceholderSepOptionNode | SyntaxKind::PlaceholderDefaultOptionNode | SyntaxKind::PlaceholderTrueFalseOptionNode => { // Skip these nodes as they're part of a placeholder diff --git a/wdl-ast/tests/validation/directory-type-unsupported/source.errors b/wdl-ast/tests/validation/directory-type-unsupported/source.errors new file mode 100644 index 00000000..8d5ef46a --- /dev/null +++ b/wdl-ast/tests/validation/directory-type-unsupported/source.errors @@ -0,0 +1,6 @@ +error: use of the `Directory` type requires WDL version 1.2 + ┌─ tests/validation/directory-type-unsupported/source.wdl:6:5 + │ +6 │ Directory x = "foo" + │ ^^^^^^^^^ + diff --git a/wdl-ast/tests/validation/directory-type-unsupported/source.wdl b/wdl-ast/tests/validation/directory-type-unsupported/source.wdl new file mode 100644 index 00000000..cfdcc3f6 --- /dev/null +++ b/wdl-ast/tests/validation/directory-type-unsupported/source.wdl @@ -0,0 +1,7 @@ +## This is a test of using a `Directory` type from WDL 1.1. + +version 1.1 + +workflow test { + Directory x = "foo" +} diff --git a/wdl-ast/tests/validation/directory-type/source.errors b/wdl-ast/tests/validation/directory-type/source.errors new file mode 100644 index 00000000..e69de29b diff --git a/wdl-ast/tests/validation/directory-type/source.wdl b/wdl-ast/tests/validation/directory-type/source.wdl new file mode 100644 index 00000000..45b98120 --- /dev/null +++ b/wdl-ast/tests/validation/directory-type/source.wdl @@ -0,0 +1,8 @@ +## This is a test of using the `Directory` type from WDL 1.2. +## This test should not have any diagnostics. + +version 1.2 + +workflow test { + Directory x = "foo" +} diff --git a/wdl-ast/tests/validation/duplicate-meta/source.errors b/wdl-ast/tests/validation/duplicate-meta/source.errors index f57b881d..060e6caf 100644 --- a/wdl-ast/tests/validation/duplicate-meta/source.errors +++ b/wdl-ast/tests/validation/duplicate-meta/source.errors @@ -16,3 +16,12 @@ error: workflow `w` contains a duplicate metadata section 22 │ meta { │ ^^^^ this metadata section is a duplicate +error: struct `X` contains a duplicate metadata section + ┌─ tests/validation/duplicate-meta/source.wdl:34:5 + │ +30 │ meta { + │ ---- first metadata section is defined here + · +34 │ meta { + │ ^^^^ this metadata section is a duplicate + diff --git a/wdl-ast/tests/validation/duplicate-meta/source.wdl b/wdl-ast/tests/validation/duplicate-meta/source.wdl index d601005e..d9a1f8d1 100644 --- a/wdl-ast/tests/validation/duplicate-meta/source.wdl +++ b/wdl-ast/tests/validation/duplicate-meta/source.wdl @@ -1,6 +1,6 @@ -# This is a test of too many meta sections in a task and workflow. +# This is a test of too many meta sections in a task, workflow, and struct definition. -version 1.1 +version 1.2 task t { meta { @@ -23,3 +23,15 @@ workflow w { } } + +struct X { + String x + + meta { + + } + + meta { + + } +} diff --git a/wdl-ast/tests/validation/duplicate-param-meta/source.errors b/wdl-ast/tests/validation/duplicate-param-meta/source.errors index 2cdcaac8..c80e3cb0 100644 --- a/wdl-ast/tests/validation/duplicate-param-meta/source.errors +++ b/wdl-ast/tests/validation/duplicate-param-meta/source.errors @@ -16,3 +16,12 @@ error: workflow `w` contains a duplicate parameter metadata section 22 │ parameter_meta { │ ^^^^^^^^^^^^^^ this parameter metadata section is a duplicate +error: struct `X` contains a duplicate parameter metadata section + ┌─ tests/validation/duplicate-param-meta/source.wdl:34:5 + │ +30 │ parameter_meta { + │ -------------- first parameter metadata section is defined here + · +34 │ parameter_meta { + │ ^^^^^^^^^^^^^^ this parameter metadata section is a duplicate + diff --git a/wdl-ast/tests/validation/duplicate-param-meta/source.wdl b/wdl-ast/tests/validation/duplicate-param-meta/source.wdl index a635aef6..edc459d6 100644 --- a/wdl-ast/tests/validation/duplicate-param-meta/source.wdl +++ b/wdl-ast/tests/validation/duplicate-param-meta/source.wdl @@ -1,6 +1,6 @@ -# This is a test of too many param meta sections in a task and workflow. +# This is a test of too many param meta sections in a task, workflow, and struct definition. -version 1.1 +version 1.2 task t { parameter_meta { @@ -23,3 +23,15 @@ workflow w { } } + +struct X { + String x + + parameter_meta { + + } + + parameter_meta { + + } +} diff --git a/wdl-ast/tests/validation/hints-duplicate-keys/source.errors b/wdl-ast/tests/validation/hints-duplicate-keys/source.errors new file mode 100644 index 00000000..0165bf64 --- /dev/null +++ b/wdl-ast/tests/validation/hints-duplicate-keys/source.errors @@ -0,0 +1,27 @@ +error: duplicate key `memory` in hints section + ┌─ tests/validation/hints-duplicate-keys/source.wdl:11:9 + │ + 9 │ memory: "first" + │ ------ first key with this name is here +10 │ +11 │ memory: "dup" + │ ^^^^^^ this key is a duplicate + +error: duplicate key `container` in hints section + ┌─ tests/validation/hints-duplicate-keys/source.wdl:12:9 + │ + 7 │ container: "first" + │ --------- first key with this name is here + · +12 │ container: "dup" + │ ^^^^^^^^^ this key is a duplicate + +error: duplicate key `disks` in hints section + ┌─ tests/validation/hints-duplicate-keys/source.wdl:13:9 + │ + 8 │ disks: "first" + │ ----- first key with this name is here + · +13 │ disks: "dup" + │ ^^^^^ this key is a duplicate + diff --git a/wdl-ast/tests/validation/hints-duplicate-keys/source.wdl b/wdl-ast/tests/validation/hints-duplicate-keys/source.wdl new file mode 100644 index 00000000..da657cc6 --- /dev/null +++ b/wdl-ast/tests/validation/hints-duplicate-keys/source.wdl @@ -0,0 +1,17 @@ +# This is a test for duplicate keys in a hints section. + +version 1.2 + +task test { + hints { + container: "first" + disks: "first" + memory: "first" + + memory: "dup" + container: "dup" + disks: "dup" + } + + command <<<>>> +} diff --git a/wdl-ast/tests/validation/hints-unsupported/source.errors b/wdl-ast/tests/validation/hints-unsupported/source.errors new file mode 100644 index 00000000..5f9307b5 --- /dev/null +++ b/wdl-ast/tests/validation/hints-unsupported/source.errors @@ -0,0 +1,12 @@ +error: use of the `hints` section requires WDL version 1.2 + ┌─ tests/validation/hints-unsupported/source.wdl:8:5 + │ +8 │ hints { + │ ^^^^^ + +error: use of the `hints` section requires WDL version 1.2 + ┌─ tests/validation/hints-unsupported/source.wdl:18:5 + │ +18 │ hints { + │ ^^^^^ + diff --git a/wdl-ast/tests/validation/hints-unsupported/source.wdl b/wdl-ast/tests/validation/hints-unsupported/source.wdl new file mode 100644 index 00000000..90d2198e --- /dev/null +++ b/wdl-ast/tests/validation/hints-unsupported/source.wdl @@ -0,0 +1,25 @@ +## This is a test of using a hint section in a 1.1 document + +version 1.1 + +task foo { + command <<<>>> + + hints { + inputs: input { + a: hints { + foo: "bar" + } + } + } +} + +workflow bar { + hints { + inputs: input { + a: hints { + foo: "bar" + } + } + } +} diff --git a/wdl-ast/tests/validation/hints/source.errors b/wdl-ast/tests/validation/hints/source.errors new file mode 100644 index 00000000..e69de29b diff --git a/wdl-ast/tests/validation/hints/source.wdl b/wdl-ast/tests/validation/hints/source.wdl new file mode 100644 index 00000000..ae9190ba --- /dev/null +++ b/wdl-ast/tests/validation/hints/source.wdl @@ -0,0 +1,26 @@ +## This is a test of using a hint section in a 1.2 document +## There should be no diagnostics emitted. + +version 1.2 + +task foo { + command <<<>>> + + hints { + inputs: input { + a: hints { + foo: "bar" + } + } + } +} + +workflow bar { + hints { + inputs: input { + a: hints { + foo: "bar" + } + } + } +} diff --git a/wdl-ast/tests/validation/missing-call-input-unsupported/source.errors b/wdl-ast/tests/validation/missing-call-input-unsupported/source.errors new file mode 100644 index 00000000..1e0f5bf1 --- /dev/null +++ b/wdl-ast/tests/validation/missing-call-input-unsupported/source.errors @@ -0,0 +1,8 @@ +error: omitting the `input` keyword in a call statement requires WDL version 1.2 + ┌─ tests/validation/missing-call-input-unsupported/source.wdl:6:16 + │ +6 │ call foo { foo = bar } + │ ^^^^^^^^^ missing an `input` keyword before this input + │ + = fix: add an `input` keyword followed by a colon before any call inputs + diff --git a/wdl-ast/tests/validation/missing-call-input-unsupported/source.wdl b/wdl-ast/tests/validation/missing-call-input-unsupported/source.wdl new file mode 100644 index 00000000..f2eec58a --- /dev/null +++ b/wdl-ast/tests/validation/missing-call-input-unsupported/source.wdl @@ -0,0 +1,7 @@ +## This is a test of a missing input keyword in a call body for WDL 1.1 + +version 1.1 + +workflow test { + call foo { foo = bar } +} diff --git a/wdl-ast/tests/validation/missing-call-input/source.errors b/wdl-ast/tests/validation/missing-call-input/source.errors new file mode 100644 index 00000000..e69de29b diff --git a/wdl-ast/tests/validation/missing-call-input/source.wdl b/wdl-ast/tests/validation/missing-call-input/source.wdl new file mode 100644 index 00000000..65cb617f --- /dev/null +++ b/wdl-ast/tests/validation/missing-call-input/source.wdl @@ -0,0 +1,8 @@ +## This is a test of a missing call input in WDL 1.2. +## There should be no diagnostics for this test. + +version 1.2 + +workflow test { + call foo { foo = bar } +} diff --git a/wdl-ast/tests/validation/multiline-strings-unsupported/source.errors b/wdl-ast/tests/validation/multiline-strings-unsupported/source.errors new file mode 100644 index 00000000..d3faf207 --- /dev/null +++ b/wdl-ast/tests/validation/multiline-strings-unsupported/source.errors @@ -0,0 +1,12 @@ +error: use of multi-line strings requires WDL version 1.2 + ┌─ tests/validation/multiline-strings-unsupported/source.wdl:11:14 + │ +11 │ foo: <<< not supported! >>> + │ ^^^^^^^^^^^^^^^^^^^^^^ + +error: use of multi-line strings requires WDL version 1.2 + ┌─ tests/validation/multiline-strings-unsupported/source.wdl:14:16 + │ +14 │ String x = <<< not supported! >>> + │ ^^^^^^^^^^^^^^^^^^^^^^ + diff --git a/wdl-ast/tests/validation/multiline-strings-unsupported/source.wdl b/wdl-ast/tests/validation/multiline-strings-unsupported/source.wdl new file mode 100644 index 00000000..22865911 --- /dev/null +++ b/wdl-ast/tests/validation/multiline-strings-unsupported/source.wdl @@ -0,0 +1,15 @@ +## This is a test of detecting unsupported multi-line strings. + +version 1.1 + +task foo { + command <<<>>> +} + +workflow test { + meta { + foo: <<< not supported! >>> + } + + String x = <<< not supported! >>> +} diff --git a/wdl-ast/tests/validation/multiline-strings/source.errors b/wdl-ast/tests/validation/multiline-strings/source.errors new file mode 100644 index 00000000..e69de29b diff --git a/wdl-ast/tests/validation/multiline-strings/source.wdl b/wdl-ast/tests/validation/multiline-strings/source.wdl new file mode 100644 index 00000000..8834bc4d --- /dev/null +++ b/wdl-ast/tests/validation/multiline-strings/source.wdl @@ -0,0 +1,13 @@ +## This is a test of multi-line strings from WDL 1.2 +## There should be no diagnostics for this test. + +version 1.2 + +workflow ok { + String ok = <<< + This is a multi-line string. + It may contain either ${<<>>} or ~{"tilde"} placeholders. + It may contain line continuations \ + And escaped endings \>>>. + >>> +} diff --git a/wdl-ast/tests/validation/scoped-exprs/source.errors b/wdl-ast/tests/validation/scoped-exprs/source.errors new file mode 100644 index 00000000..be9b0710 --- /dev/null +++ b/wdl-ast/tests/validation/scoped-exprs/source.errors @@ -0,0 +1,154 @@ +error: `hints` literals can only be used within a hints section + ┌─ tests/validation/scoped-exprs/source.wdl:47:13 + │ +47 │ Int a = hints { + │ ^^^^^ + +error: `input` literals can only be used within a hints section + ┌─ tests/validation/scoped-exprs/source.wdl:51:13 + │ +51 │ Int b = input { + │ ^^^^^ + +error: `output` literals can only be used within a hints section + ┌─ tests/validation/scoped-exprs/source.wdl:55:13 + │ +55 │ Int c = output { + │ ^^^^^^ + +error: `hints` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:61:18 + │ +60 │ ok: hints { + │ ----- the outer `hints` literal is here +61 │ bad: hints { + │ ^^^^^ this `hints` literal cannot be nested + +error: `input` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:62:22 + │ +61 │ bad: hints { + │ ----- the outer `hints` literal is here +62 │ bad: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `output` literals cannot be nested within `input` literals + ┌─ tests/validation/scoped-exprs/source.wdl:63:26 + │ +62 │ bad: input { + │ ----- the outer `input` literal is here +63 │ bad: output { + │ ^^^^^^ this `output` literal cannot be nested + +error: `hints` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:71:22 + │ +70 │ ok: hints { + │ ----- the outer `hints` literal is here +71 │ bad: hints { + │ ^^^^^ this `hints` literal cannot be nested + +error: `input` literals cannot be nested within `input` literals + ┌─ tests/validation/scoped-exprs/source.wdl:75:21 + │ +69 │ inputs: input { + │ ----- the outer `input` literal is here + · +75 │ inputs: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `input` literals cannot be nested within `input` literals + ┌─ tests/validation/scoped-exprs/source.wdl:76:20 + │ +75 │ inputs: input { + │ ----- the outer `input` literal is here +76 │ a: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `hints` literals cannot be nested within `input` literals + ┌─ tests/validation/scoped-exprs/source.wdl:79:20 + │ +75 │ inputs: input { + │ ----- the outer `input` literal is here + · +79 │ b: hints { + │ ^^^^^ this `hints` literal cannot be nested + +error: `input` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:80:24 + │ +79 │ b: hints { + │ ----- the outer `hints` literal is here +80 │ a: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `output` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:83:24 + │ +79 │ b: hints { + │ ----- the outer `hints` literal is here + · +83 │ b: output { + │ ^^^^^^ this `output` literal cannot be nested + +error: `hints` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:86:24 + │ +79 │ b: hints { + │ ----- the outer `hints` literal is here + · +86 │ c: hints { + │ ^^^^^ this `hints` literal cannot be nested + +error: `output` literals cannot be nested within `input` literals + ┌─ tests/validation/scoped-exprs/source.wdl:90:20 + │ +75 │ inputs: input { + │ ----- the outer `input` literal is here + · +90 │ c: output { + │ ^^^^^^ this `output` literal cannot be nested + +error: `input` literals cannot be nested within `output` literals + ┌─ tests/validation/scoped-exprs/source.wdl:96:16 + │ +95 │ outputs: output { + │ ------ the outer `output` literal is here +96 │ a: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `input` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:100:20 + │ + 99 │ b: hints { + │ ----- the outer `hints` literal is here +100 │ a: input { + │ ^^^^^ this `input` literal cannot be nested + +error: `output` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:103:20 + │ + 99 │ b: hints { + │ ----- the outer `hints` literal is here + · +103 │ b: output { + │ ^^^^^^ this `output` literal cannot be nested + +error: `hints` literals cannot be nested within `hints` literals + ┌─ tests/validation/scoped-exprs/source.wdl:106:20 + │ + 99 │ b: hints { + │ ----- the outer `hints` literal is here + · +106 │ c: hints { + │ ^^^^^ this `hints` literal cannot be nested + +error: `output` literals cannot be nested within `output` literals + ┌─ tests/validation/scoped-exprs/source.wdl:110:16 + │ + 95 │ outputs: output { + │ ------ the outer `output` literal is here + · +110 │ c: output { + │ ^^^^^^ this `output` literal cannot be nested + diff --git a/wdl-ast/tests/validation/scoped-exprs/source.wdl b/wdl-ast/tests/validation/scoped-exprs/source.wdl new file mode 100644 index 00000000..9e1f15fb --- /dev/null +++ b/wdl-ast/tests/validation/scoped-exprs/source.wdl @@ -0,0 +1,115 @@ +## This is a test of ensuring certain expressions can only be used in particular scopes. + +version 1.2 + +task ok { + command <<<>>> + + hints { + a: hints { + a: "a" + b: 1 + c: 1.0 + d: [1, 2, 3] + } + inputs: input { + foo: hints { + a: "a" + b: "b" + c: "c" + } + baz.bar.qux: hints { + foo: "foo" + bar: "bar" + baz: "baz" + } + } + c: "foo" + d: 1 + outputs: output { + foo: hints { + a: "a" + b: "b" + c: "c" + } + baz.bar.qux: hints { + foo: "foo" + bar: "bar" + baz: "baz" + } + } + } +} + +task bad { + command <<<>>> + + Int a = hints { + foo: "bar" + } + + Int b = input { + foo: "bar" + } + + Int c = output { + foo: "bar" + } + + hints { + ok: hints { + bad: hints { + bad: input { + bad: output { + + } + } + } + } + inputs: input { + ok: hints { + bad: hints { + + } + } + inputs: input { + a: input { + + } + b: hints { + a: input { + + } + b: output { + + } + c: hints { + + } + } + c: output { + + } + } + } + outputs: output { + a: input { + + } + b: hints { + a: input { + + } + b: output { + + } + c: hints { + + } + } + c: output { + + } + } + } +} diff --git a/wdl-ast/tests/validation/struct-metadata-unsupported/source.errors b/wdl-ast/tests/validation/struct-metadata-unsupported/source.errors new file mode 100644 index 00000000..9690de64 --- /dev/null +++ b/wdl-ast/tests/validation/struct-metadata-unsupported/source.errors @@ -0,0 +1,12 @@ +error: use of a `meta` section in a struct definition requires WDL version 1.2 + ┌─ tests/validation/struct-metadata-unsupported/source.wdl:8:5 + │ +8 │ meta { + │ ^^^^ + +error: use of a `parameter_meta` section in a struct definition requires WDL version 1.2 + ┌─ tests/validation/struct-metadata-unsupported/source.wdl:12:5 + │ +12 │ parameter_meta { + │ ^^^^^^^^^^^^^^ + diff --git a/wdl-ast/tests/validation/struct-metadata-unsupported/source.wdl b/wdl-ast/tests/validation/struct-metadata-unsupported/source.wdl new file mode 100644 index 00000000..6a754b75 --- /dev/null +++ b/wdl-ast/tests/validation/struct-metadata-unsupported/source.wdl @@ -0,0 +1,18 @@ +## This is a test of struct metadata sections in a WDL 1.1 document. + +version 1.1 + +struct Foo { + Int a + + meta { + foo: "bar" + } + + parameter_meta { + a: "foo" + b: "bar" + } + + String b +} diff --git a/wdl-ast/tests/validation/struct-metadata/source.errors b/wdl-ast/tests/validation/struct-metadata/source.errors new file mode 100644 index 00000000..e69de29b diff --git a/wdl-ast/tests/validation/struct-metadata/source.wdl b/wdl-ast/tests/validation/struct-metadata/source.wdl new file mode 100644 index 00000000..5878975e --- /dev/null +++ b/wdl-ast/tests/validation/struct-metadata/source.wdl @@ -0,0 +1,19 @@ +## This is a test of struct metadata sections in a WDL 1.2 document. +## This test should have no diagnostics. + +version 1.2 + +struct Foo { + Int a + + meta { + foo: "bar" + } + + parameter_meta { + a: "foo" + b: "bar" + } + + String b +} diff --git a/wdl-format/Cargo.toml b/wdl-format/Cargo.toml new file mode 100644 index 00000000..3115e5ef --- /dev/null +++ b/wdl-format/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "wdl-format" +version = "0.1.0" +license.workspace = true +edition.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +anyhow.workspace = true +wdl-ast = { path = "../wdl-ast", version = "0.4.0" } + +[dev-dependencies] +pretty_assertions = { workspace = true } +approx = { workspace = true } +rayon = { workspace = true } +colored = { workspace = true } +codespan-reporting = { workspace = true } + +[features] +codespan = ["wdl-ast/codespan"] + +[[test]] +name = "format" +required-features = ["codespan"] +harness = false diff --git a/wdl-format/src/format.rs b/wdl-format/src/format.rs new file mode 100644 index 00000000..58a7516d --- /dev/null +++ b/wdl-format/src/format.rs @@ -0,0 +1,911 @@ +//! A module for formatting WDL code. + +use anyhow::Result; +use wdl_ast::v1::Decl; +use wdl_ast::v1::DocumentItem; +use wdl_ast::v1::InputSection; +use wdl_ast::v1::MetadataObjectItem; +use wdl_ast::v1::MetadataSection; +use wdl_ast::v1::OutputSection; +use wdl_ast::v1::ParameterMetadataSection; +use wdl_ast::v1::StructDefinition; +use wdl_ast::AstNode; +use wdl_ast::AstToken; +use wdl_ast::Diagnostic; +use wdl_ast::Direction; +use wdl_ast::Document; +use wdl_ast::SyntaxElement; +use wdl_ast::SyntaxKind; +use wdl_ast::Validator; +use wdl_ast::VersionStatement; + +/// Newline constant used for formatting. +pub const NEWLINE: &str = "\n"; +/// Indentation constant used for formatting. +pub const INDENT: &str = " "; + +mod comments; +mod import; +mod task; +mod workflow; + +use comments::format_inline_comment; +use comments::format_preceding_comments; +use import::format_imports; +use task::format_task; +use workflow::format_workflow; + +/// Format a version statement. +fn format_version_statement(version_statement: VersionStatement) -> String { + // Collect comments that preceed the version statement. + // Note as this must be the first element in the document, + // the logic is slightly different than the 'format_preceding_comments' + // function. We are walking backwards through the syntax tree, so we must + // collect the comments in a vector and reverse them to get them in the + // correct order. + let mut preceding_comments = Vec::new(); + for sibling in version_statement + .syntax() + .siblings_with_tokens(Direction::Prev) + { + match sibling.kind() { + SyntaxKind::Comment => { + preceding_comments.push(sibling.to_string().trim().to_owned()); + } + SyntaxKind::Whitespace => { + // Ignore + } + SyntaxKind::VersionStatementNode => { + // Ignore the root node + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", sibling.kind()); + } + } + } + + let mut result = String::new(); + for comment in preceding_comments.iter().rev() { + result.push_str(comment); + result.push_str(NEWLINE); + } + + if !result.is_empty() { + // If there are preamble comments, ensure a blank line is inserted + result.push_str(NEWLINE); + } + result.push_str("version"); + let version_keyword = version_statement.syntax().first_token().unwrap(); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(version_keyword), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(version_statement.version().syntax().clone()), + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with("version") { + result.push(' '); + } else if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } + result.push_str(version_statement.version().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(version_statement.syntax().clone()), + true, + )); + + result +} + +/// Format the inner portion of a meta/parameter_meta section. +fn format_metadata_item(item: &MetadataObjectItem) -> String { + let mut result = String::new(); + let two_indents = INDENT.repeat(2); + let three_indents = INDENT.repeat(3); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(item.syntax().clone()), + 2, + false, + )); + result.push_str(&two_indents); + result.push_str(item.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(item.name().syntax().clone()), + false, + )); + + let colon = item + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Colon) + .expect("metadata item should have a colon"); + result.push_str(&format_preceding_comments( + &colon, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&three_indents); + } + result.push(':'); + result.push_str(&format_inline_comment(&colon, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(item.value().syntax().clone()), + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&three_indents); + } else { + result.push(' '); + } + result.push_str(&item.value().syntax().to_string()); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(item.syntax().clone()), + true, + )); + + result +} + +/// Format a meta section. +fn format_meta_section(meta: MetadataSection) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(meta.syntax().clone()), + 1, + false, + )); + + result.push_str(INDENT); + result.push_str("meta"); + let meta_keyword = meta.syntax().first_token().unwrap(); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(meta_keyword.clone()), + false, + )); + + let open_brace = meta + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("metadata section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for item in meta.items() { + result.push_str(&format_metadata_item(&item)); + } + + let close_brace = meta + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("metadata section should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, 0, false)); + result.push_str(INDENT); + result.push('}'); + + result.push_str(&format_inline_comment( + &SyntaxElement::Node(meta.syntax().clone()), + true, + )); + + result +} + +/// Format a parameter meta section. +fn format_parameter_meta_section(parameter_meta: ParameterMetadataSection) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(parameter_meta.syntax().clone()), + 1, + false, + )); + + result.push_str(INDENT); + result.push_str("parameter_meta"); + let parameter_meta_keyword = parameter_meta.syntax().first_token().unwrap(); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(parameter_meta_keyword.clone()), + false, + )); + + let open_brace = parameter_meta + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("parameter metadata section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for item in parameter_meta.items() { + result.push_str(&format_metadata_item(&item)); + } + + let close_brace = parameter_meta + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("parameter metadata section should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, 0, false)); + result.push_str(INDENT); + result.push('}'); + + result.push_str(&format_inline_comment( + &SyntaxElement::Node(parameter_meta.syntax().clone()), + true, + )); + + result +} + +/// Format an input section. +fn format_input_section(input: InputSection) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(input.syntax().clone()), + 1, + false, + )); + + result.push_str(INDENT); + result.push_str("input"); + let input_keyword = input + .syntax() + .first_token() + .expect("input section should have a token"); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(input_keyword.clone()), + false, + )); + + let open_brace = input + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("input section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for decl in input.declarations() { + result.push_str(&format_declaration(&decl, 2)); + } + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token( + input + .syntax() + .last_token() + .expect("input section should have a token"), + ), + 1, + false, + )); + result.push_str(INDENT); + result.push('}'); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(input.syntax().clone()), + true, + )); + + result +} + +/// Format an output section. +fn format_output_section(output: OutputSection) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(output.syntax().clone()), + 1, + false, + )); + + result.push_str(INDENT); + result.push_str("output"); + let output_keyword = output + .syntax() + .first_token() + .expect("output section should have a token"); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(output_keyword.clone()), + false, + )); + let open_brace = output + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("output section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for decl in output.declarations() { + result.push_str(&format_declaration(&Decl::Bound(decl), 2)); + } + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token( + output + .syntax() + .last_token() + .expect("output section should have a token"), + ), + 1, + false, + )); + result.push_str(INDENT); + result.push('}'); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(output.syntax().clone()), + true, + )); + + result +} + +/// Format a declaration. +fn format_declaration(declaration: &Decl, num_indents: usize) -> String { + let mut result = String::new(); + let next_indent_level = num_indents + 1; + let cur_indents = INDENT.repeat(num_indents); + let next_indents = INDENT.repeat(next_indent_level); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(declaration.syntax().clone()), + num_indents, + false, + )); + result.push_str(&cur_indents); + + result.push_str(&declaration.ty().to_string()); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(declaration.ty().syntax().clone()), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(declaration.name().syntax().clone()), + next_indent_level, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(declaration.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(declaration.name().syntax().clone()), + false, + )); + + if let Some(expr) = declaration.expr() { + let equal_sign = declaration + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Assignment) + .expect("Bound declaration should have an equal sign"); + + result.push_str(&format_preceding_comments( + &equal_sign, + next_indent_level, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push('='); + result.push_str(&format_inline_comment(&equal_sign, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(expr.syntax().clone()), + next_indent_level, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(&expr.syntax().to_string()); // TODO: format expressions + } + result.push_str(&format_inline_comment( + &SyntaxElement::Node(declaration.syntax().clone()), + true, + )); + + result +} + +/// Format a struct definition +fn format_struct_definition(struct_def: &StructDefinition) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(struct_def.syntax().clone()), + 0, + false, + )); + result.push_str("struct"); + let struct_keyword = struct_def + .syntax() + .first_token() + .expect("struct definition should have a token"); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(struct_keyword.clone()), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(struct_def.name().syntax().clone()), + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push_str(struct_def.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(struct_def.name().syntax().clone()), + false, + )); + + let open_brace = struct_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("struct definition should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 0, + !result.ends_with(NEWLINE), + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for decl in struct_def.members() { + result.push_str(&format_declaration(&Decl::Unbound(decl), 1)); + } + + let close_brace = struct_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("struct definition should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, 0, false)); + result.push('}'); + result.push_str(&format_inline_comment(&close_brace, true)); + + result +} + +/// Format a WDL document. +pub fn format_document(code: &str) -> Result> { + let (document, diagnostics) = Document::parse(code); + if !diagnostics.is_empty() { + return Err(diagnostics); + } + let mut validator = Validator::default(); + match validator.validate(&document) { + Ok(_) => { + // The document is valid, so we can format it. + } + Err(diagnostics) => return Err(diagnostics), + } + + let mut result = String::new(); + result.push_str(&format_version_statement( + document.version_statement().unwrap(), + )); + result.push_str(NEWLINE); + + let ast = document.ast(); + let ast = ast.as_v1().unwrap(); + result.push_str(&format_imports(ast.imports())); + + ast.items().for_each(|item| { + match item { + DocumentItem::Import(_) => { + // Imports have already been formatted + } + DocumentItem::Workflow(workflow_def) => { + if !result.ends_with(&NEWLINE.repeat(2)) { + result.push_str(NEWLINE); + } + result.push_str(&format_workflow(&workflow_def)); + } + DocumentItem::Task(task_def) => { + if !result.ends_with(&NEWLINE.repeat(2)) { + result.push_str(NEWLINE); + } + result.push_str(&format_task(&task_def)); + } + DocumentItem::Struct(struct_def) => { + if !result.ends_with(&NEWLINE.repeat(2)) { + result.push_str(NEWLINE); + } + result.push_str(&format_struct_definition(&struct_def)); + } + }; + }); + + Ok(result) +} + +// #[cfg(test)] +// mod tests { +// use super::*; + +// #[test] +// fn test_format_with_imports_and_preceding_comments() { +// let code = " +// version 1.1 + +// workflow test {} +// # this comment belongs to fileC +// import \"fileC.wdl\" +// # this comment belongs to fileB +// import \"fileB.wdl\" as foo +// # fileA 1 +// import +// # fileA 2.1 +// # fileA 2.2 +// \"fileA.wdl\" +// # fileA 3 +// as +// # fileA 4 +// bar +// # fileA 5 +// alias +// # fileA 6 +// qux +// # fileA 7 +// as +// # fileA 8 +// Qux"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\n# fileA 1\nimport\n # fileA 2.1\n # fileA 2.2\n \ +// \"fileA.wdl\"\n # fileA 3\n as\n # fileA 4\n bar\n # fileA \ +// 5\n alias\n # fileA 6\n qux\n # fileA 7\n as\n \ +// # fileA 8\n Qux\n# this comment belongs to fileB\nimport \"fileB.wdl\" as \ +// foo\n# this comment belongs to fileC\nimport \"fileC.wdl\"\n\nworkflow test {\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_imports_and_inline_comments() { +// let code = " +// version 1.0 + +// import \"fileB.wdl\" as foo # fileB +// workflow test {} +// import \"fileC.wdl\" +// import # fileA 1 +// \"fileA.wdl\" # fileA 2 +// as # fileA 3 +// bar # fileA 4 +// alias # fileA 5 +// qux # fileA 6 +// as # fileA 7 +// Qux # fileA 8 +// "; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.0\n\nimport # fileA 1\n \"fileA.wdl\" # fileA 2\n as # fileA 3\n bar # fileA 4\n alias # fileA 5\n qux # fileA 6\n as # fileA 7\n Qux # fileA 8\nimport \"fileB.wdl\" as foo # fileB\nimport \"fileC.wdl\"\n\nworkflow test {\n}\n", +// ); +// } + +// #[test] +// fn test_format_without_comments() { +// let code = "version 1.1\nworkflow test {}"; +// let formatted = format_document(code).unwrap(); +// assert_eq!(formatted, "version 1.1\n\nworkflow test {\n}\n"); +// } + +// #[test] +// fn test_format_with_imports_and_all_comments() { +// let code = " +// version 1.1 + +// # this comment belongs to fileB +// import \"fileB.wdl\" as foo # also fileB +// # fileA 1.1 +// import # fileA 1.2 +// # fileA 2.1 +// # fileA 2.2 +// \"fileA.wdl\" # fileA 2.3 +// # fileA 3.1 +// as # fileA 3.2 +// # fileA 4.1 +// bar # fileA 4.2 +// # fileA 5.1 +// alias # fileA 5.2 +// # fileA 6.1 +// qux # fileA 6.2 +// # fileA 7.1 +// as # fileA 7.2 +// # fileA 8.1 +// Qux # fileA 8.2 +// workflow test {} +// # this comment belongs to fileC +// import \"fileC.wdl\""; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\n# fileA 1.1\nimport # fileA 1.2\n # fileA 2.1\n # fileA 2.2\n \"fileA.wdl\" # fileA 2.3\n # fileA 3.1\n as # fileA 3.2\n # fileA 4.1\n bar # fileA 4.2\n # fileA 5.1\n alias # fileA 5.2\n # fileA 6.1\n qux # fileA 6.2\n # fileA 7.1\n as # fileA 7.2\n # fileA 8.1\n Qux # fileA 8.2\n# this comment belongs to fileB\nimport \"fileB.wdl\" as foo # also fileB\n# this comment belongs to fileC\nimport \"fileC.wdl\"\n\nworkflow test {\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_imports_and_no_comments() { +// let code = " +// version 1.1 + +// import \"fileB.wdl\" as foo +// import \"fileA.wdl\" as bar alias cows as horses +// workflow test {} +// import \"fileC.wdl\" alias qux as Qux"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nimport \"fileA.wdl\" as bar alias cows as horses\nimport \ +// \"fileB.wdl\" as foo\nimport \"fileC.wdl\" alias qux as Qux\n\nworkflow test {\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_meta_with_all_comments() { +// let code = " +// version 1.1 + +// workflow test { # workflow comment +// # meta comment +// meta # also meta comment +// # open brace +// { # open brace +// # author comment +// author: \"me\" # author comment +// # email comment +// email: \"me@stjude.org\" # email comment +// } # trailing comment +// }"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nworkflow test { # workflow comment\n # meta comment\n meta # also meta comment\n # open brace\n { # open brace\n # author comment\n author: \"me\" # author comment\n # email comment\n email: \"me@stjude.org\" # email comment\n } # trailing comment\n\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_meta_without_comments() { +// let code = " +// version 1.1 + +// workflow test { +// meta { +// author: \"me\" +// email: \"me@stjude.org\" +// } +// } +// "; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nworkflow test {\n meta {\n author: \"me\"\n email: \ +// \"me@stjude.org\"\n }\n\n}\n" +// ); +// } +// #[test] +// fn test_format_with_parameter_metadata() { +// let code = " +// version 1.1 +// # workflow comment +// workflow test { +// input { +// String foo +// } +// # parameter_meta comment +// parameter_meta { # parameter_meta comment +// foo: \"bar\" # foo comment +// } +// } + +// "; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\n# workflow comment\nworkflow test {\n # parameter_meta comment\n parameter_meta { # parameter_meta comment\n foo: \"bar\" # foo comment\n }\n\n input {\n String foo\n }\n\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_inputs() { +// let code = " +// version 1.1 + +// workflow test { +// input { +// # foo comment +// String foo # another foo comment +// Int # mid-bar comment +// bar +// } +// }"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nworkflow test {\n input {\n # foo comment\n String \ +// foo # another foo comment\n Int # mid-bar comment\n bar\n \ +// }\n\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_calls() { +// let code = " +// version 1.1 + +// workflow test { +// # foo comment +// call foo +// # bar comment +// call bar as baz +// call qux # mid-qux inline comment +// # mid-qux full-line comment +// after baz # after qux +// call lorem after ipsum { input: # after input token +// bazam, +// bam = select_bam +// } +// }"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nworkflow test {\n # foo comment\n call foo\n # bar comment\n call bar as baz\n call qux # mid-qux inline comment\n # mid-qux full-line comment\n after baz # after qux\n call lorem after ipsum { input: # after input token\n bazam,\n bam = select_bam,\n }\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_conditionals_and_scatters() { +// let code = " +// version 1.1 + +// workflow test { +// if (true) { +// call foo +// scatter (abc in bar) { +// if (false) { +// call bar +// } +// if ( +// a > b # expr comment +// ) { +// scatter (x in [1, 2, 3]) { +// call baz +// }} +// }} +// }"; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "version 1.1\n\nworkflow test {\n if (true) {\n call foo\n scatter (abc in bar) {\n if (false) {\n call bar\n }\n if (a > b # expr comment\n ) {\n scatter (x in [1, 2, 3]) {\n call baz\n }\n }\n }\n }\n}\n" +// ); +// } + +// #[test] +// fn test_format_with_inline_comments() { +// let code = " +// # preamble one +// # preamble two +// version # 1 +// 1.1 # 2 +// workflow # 3 +// test # 4 +// { # 5 +// meta # 6 +// { # 7 +// # 8 +// # 9 +// description # 10 +// : # 11 +// \"what a nightmare\" # 12 +// } # 13 +// parameter_meta # 14 +// { # 15 +// foo # 16 +// : # 17 +// \"bar\" # 18 +// } # 19 +// input # 20 +// { # 21 +// String # 22 +// foo # 23 +// } # 24 +// if # 25 +// ( # 26 +// true # 27 +// ) # 28 +// { # 29 +// scatter # 30 +// ( # 31 +// x # 32 +// in # 33 +// [1,2,3] # 34 +// ) # 35 +// { # 36 +// call # 37 +// task # 38 +// as # 39 +// task_alias # 40 +// after # 41 +// cows_come_home # 42 +// } # 43 +// } # 44 +// } # 45 +// "; +// let formatted = format_document(code).unwrap(); +// assert_eq!( +// formatted, +// "# preamble one\n# preamble two\n\nversion # 1\n 1.1 # 2\n\nworkflow # 3\n test # 4\n{ # 5\n meta # 6\n { # 7\n # 8\n # 9\n description # 10\n : # 11\n \"what a nightmare\" # 12\n } # 13\n\n parameter_meta # 14\n { # 15\n foo # 16\n : # 17\n \"bar\" # 18\n } # 19\n\n input # 20\n { # 21\n String # 22\n foo # 23\n } # 24\n\n if # 25\n ( # 26\n true # 27\n ) # 28\n { # 29\n scatter # 30\n ( # 31\n x # 32\n in # 33\n [1,2,3] # 34\n ) # 35\n { # 36\n call # 37\n task # 38\n as # 39\n task_alias # 40\n after # 41\n cows_come_home # 42\n } # 43\n } # 44\n} # 45\n" +// ); +// } +// } diff --git a/wdl-format/src/format/comments.rs b/wdl-format/src/format/comments.rs new file mode 100644 index 00000000..481363ec --- /dev/null +++ b/wdl-format/src/format/comments.rs @@ -0,0 +1,128 @@ +//! Format comments in a WDL file. +//! +//! All comments will be treated as either "preceding" or "inline" comments. +//! A preceding comment is a comment that appears on a line before an element, +//! if and only if that element is the first element of its line. preceding +//! comments should always appear, without any blank lines, immediately before +//! the element they are commenting on. preceding comments should be indented +//! to the same level as the element they are commenting on. An inline +//! comment is a comment that appears on the same line as an element, if and +//! only if that element is the last element of its line. Inline comments should +//! always appear immediately after the element they are commenting on. + +use wdl_ast::SyntaxElement; +use wdl_ast::SyntaxKind; + +use super::INDENT; +use super::NEWLINE; + +/// Inline comment space constant used for formatting. +pub const INLINE_COMMENT_SPACE: &str = " "; + +/// Format comments that preceed a node. +/// +/// This function will return the empty string if no comments are found +/// (regardless of the value of 'prepend_newline'). +/// This function will format all comments that appear before a node, +/// so long as those comments are on their own line. If a comment is +/// found that is not on its own line, this function will stop looking +/// for comments. This function will return a string with all comments +/// formatted with the correct indentation and a newline at the end. +/// If 'prepend_newline' is true, a newline will be prepended to the +/// formatted comments. +pub fn format_preceding_comments( + element: &SyntaxElement, + num_indents: usize, + prepend_newline: bool, +) -> String { + // This walks _backwards_ through the syntax tree to find comments + // so we must collect them in a vector and later reverse them to get them in the + // correct order. + let mut preceding_comments = Vec::new(); + + let mut prev = element.prev_sibling_or_token(); + while let Some(cur) = prev { + match cur.kind() { + SyntaxKind::Comment => { + // Ensure this comment "belongs" to the root element. + // A preceding comment on a blank line is considered to belong to the element. + // Othewise, the comment "belongs" to whatever + // else is on that line. + if let Some(before_cur) = cur.prev_sibling_or_token() { + match before_cur.kind() { + SyntaxKind::Whitespace => { + if before_cur.to_string().contains('\n') { + // The 'cur' comment is on is on its own line. + // It "belongs" to the current element. + let trimmed_comment = cur.clone().to_string().trim().to_owned(); + preceding_comments.push(trimmed_comment); + } + } + _ => { + // The 'cur' comment is on the same line as this + // token. It "belongs" + // to whatever is currently being processed. + } + } + } + } + SyntaxKind::Whitespace => { + // Ignore + } + _ => { + // We've backed up to non-trivia, so we can stop + break; + } + } + prev = cur.prev_sibling_or_token() + } + + let mut result = String::new(); + if prepend_newline && !preceding_comments.is_empty() { + result.push_str(NEWLINE); + } + for comment in preceding_comments.iter().rev() { + for _ in 0..num_indents { + result.push_str(INDENT); + } + result.push_str(comment); + result.push_str(NEWLINE); + } + result +} + +/// Format a comment on the same line as an element. +/// +/// If no comments are found this returns an empty string unless +/// 'newline_needed' is true. If a comment is found, this will return the +/// comment with a newline. If a comment is not found, but 'newline_needed' is +/// true, this will return a newline. Else it will return the empty string. +pub fn format_inline_comment(element: &SyntaxElement, newline_needed: bool) -> String { + let mut result = String::new(); + let mut next = element.next_sibling_or_token(); + while let Some(cur) = next { + match cur.kind() { + SyntaxKind::Comment => { + result.push_str(INLINE_COMMENT_SPACE); + result.push_str(cur.to_string().trim()); + result.push_str(NEWLINE); + break; + } + SyntaxKind::Whitespace => { + if cur.to_string().contains('\n') { + // We've looked ahead past the current line, so we can stop + break; + } + } + _ => { + // Something is between the element and the end of the line + break; + } + } + next = cur.next_sibling_or_token(); + } + if result.is_empty() && newline_needed { + result.push_str(NEWLINE); + } + result +} diff --git a/wdl-format/src/format/import.rs b/wdl-format/src/format/import.rs new file mode 100644 index 00000000..f6f31d59 --- /dev/null +++ b/wdl-format/src/format/import.rs @@ -0,0 +1,189 @@ +//! This module contains the functions for formatting import statements. + +use std::collections::HashMap; + +use wdl_ast::v1::ImportStatement; +use wdl_ast::AstChildren; +use wdl_ast::AstNode; +use wdl_ast::SyntaxElement; +use wdl_ast::SyntaxKind; + +use super::comments::format_inline_comment; +use super::comments::format_preceding_comments; +use super::INDENT; +use super::NEWLINE; + +/// Format a list of import statements. +pub fn format_imports(imports: AstChildren) -> String { + // Collect the imports into a map so we can sort them + // The key is the contents of the literal string node and if present, the alias + // name. The value is the formatted import statement with any found + // comments. + let mut import_map: HashMap = HashMap::new(); + let one_indent = INDENT; + let two_indents = INDENT.repeat(2); + for import in imports { + let mut key = String::new(); + let mut val = String::new(); + + val.push_str(&format_preceding_comments( + &SyntaxElement::Node(import.syntax().clone()), + 0, + false, + )); + + val.push_str("import"); + let import_keyword = import.syntax().first_token().unwrap(); + val.push_str(&format_inline_comment( + &SyntaxElement::Token(import_keyword.clone()), + false, + )); + let mut next = import_keyword.next_sibling_or_token(); + while let Some(cur) = next { + match cur.kind() { + SyntaxKind::LiteralStringNode => { + val.push_str(&format_preceding_comments(&cur, 1, !val.ends_with(NEWLINE))); + if val.ends_with("import") { + val.push(' '); + } else if val.ends_with(NEWLINE) { + val.push_str(one_indent); + } + cur.as_node() + .unwrap() + .children_with_tokens() + .for_each(|string_part| match string_part.kind() { + SyntaxKind::DoubleQuote | SyntaxKind::SingleQuote => { + val.push('"'); + } + SyntaxKind::LiteralStringText => { + key.push_str(&string_part.to_string()); + val.push_str(&string_part.to_string()); + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", cur.kind()); + } + }); + val.push_str(&format_inline_comment(&cur, false)); + } + SyntaxKind::AsKeyword => { + val.push_str(&format_preceding_comments(&cur, 1, !val.ends_with(NEWLINE))); + if val.ends_with(NEWLINE) { + val.push_str(one_indent); + } else { + val.push(' '); + } + val.push_str("as"); + val.push_str(&format_inline_comment(&cur, false)); + } + SyntaxKind::Ident => { + key.push_str(&cur.to_string()); + + val.push_str(&format_preceding_comments(&cur, 2, !val.ends_with(NEWLINE))); + if val.ends_with("as") { + val.push(' '); + } else { + val.push_str(&two_indents); + } + val.push_str(&cur.to_string()); + val.push_str(&format_inline_comment(&cur, false)); + } + SyntaxKind::ImportAliasNode => { + val.push_str(&format_preceding_comments(&cur, 1, !val.ends_with(NEWLINE))); + let mut second_ident_of_clause = false; + cur.as_node() + .unwrap() + .children_with_tokens() + .for_each(|alias_part| match alias_part.kind() { + SyntaxKind::AliasKeyword => { + // This should always be the first child processed + if val.ends_with(NEWLINE) { + val.push_str(one_indent); + } else { + val.push(' '); + } + val.push_str("alias"); + val.push_str(&format_inline_comment(&alias_part, false)); + } + SyntaxKind::Ident => { + val.push_str(&format_preceding_comments( + &alias_part, + 2, + !val.ends_with(NEWLINE), + )); + if val.ends_with("alias") || val.ends_with("as") { + val.push(' '); + } else { + val.push_str(&two_indents); + } + val.push_str(&alias_part.to_string()); + if !second_ident_of_clause { + val.push_str(&format_inline_comment(&alias_part, false)); + second_ident_of_clause = true; + } // else will be handled by outer loop + } + SyntaxKind::AsKeyword => { + val.push_str(&format_preceding_comments( + &alias_part, + 2, + !val.ends_with(NEWLINE), + )); + if val.ends_with(NEWLINE) { + val.push_str(&two_indents); + } else { + val.push(' '); + } + val.push_str("as"); + val.push_str(&format_inline_comment(&alias_part, false)); + } + SyntaxKind::ImportAliasNode => { + // Ignore the root node + } + SyntaxKind::Whitespace => { + // Ignore + } + SyntaxKind::Comment => { + // This comment will be included by + // a call to ' + // format_preceding_comments' or + // 'format_inline_comment' + // in another match arm + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", alias_part.kind()); + } + }); + } + SyntaxKind::Whitespace => { + // Ignore + } + SyntaxKind::Comment => { + // This comment will be included by a call to + // 'format_inline_comment' or 'format_preceding_comments' + // in another match arm + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", cur.kind()); + } + } + next = cur.next_sibling_or_token(); + } + + let newline_needed = !val.ends_with(NEWLINE); + val.push_str(&format_inline_comment( + &SyntaxElement::Node(import.syntax().clone()), + newline_needed, + )); + + import_map.insert(key, val); + } + + let mut import_vec: Vec<_> = import_map.into_iter().collect(); + import_vec.sort_by(|a, b| a.0.cmp(&b.0)); + + let mut result = String::new(); + for (_, val) in import_vec { + result.push_str(&val); + } + + result +} diff --git a/wdl-format/src/format/task.rs b/wdl-format/src/format/task.rs new file mode 100644 index 00000000..a3670dc1 --- /dev/null +++ b/wdl-format/src/format/task.rs @@ -0,0 +1,340 @@ +//! Format a task definition. +use wdl_ast::v1::CommandPart; +use wdl_ast::v1::CommandSection; +use wdl_ast::v1::RuntimeSection; +use wdl_ast::v1::TaskDefinition; +use wdl_ast::v1::TaskItem; +use wdl_ast::AstNode; +use wdl_ast::AstToken; +use wdl_ast::SyntaxElement; +use wdl_ast::SyntaxKind; + +use super::comments::format_inline_comment; +use super::comments::format_preceding_comments; +use super::INDENT; +use super::NEWLINE; +use super::*; + +/// Format a command section. +fn format_command_section(command: CommandSection) -> String { + let mut result = String::new(); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(command.syntax().clone()), + 0, + false, + )); + result.push_str(INDENT); + result.push_str("command"); + result.push_str(&format_inline_comment( + &command + .syntax() + .first_child_or_token() + .expect("Command section should have a first child"), + false, + )); + + if command.is_heredoc() { + let open_heredoc = command + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenHeredoc) + .expect("Command section should have an open heredoc"); + result.push_str(&format_preceding_comments( + &open_heredoc, + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(INDENT); + } else { + result.push(' '); + } + result.push_str("<<<"); + // Open heredoc inline comment will be part of the command text + } else { + let open_brace = command + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Command section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } else { + result.push_str(INDENT); + } + result.push('{'); + // Open brace inline comment will be part of the command text + } + + for part in command.parts() { + match part { + CommandPart::Text(t) => { + result.push_str(t.as_str()); + } + CommandPart::Placeholder(p) => { + result.push_str(&p.syntax().to_string()); + } + } + } + + if command.is_heredoc() { + let close_heredoc = command + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseHeredoc) + .expect("Command section should have a close heredoc"); + // Close heredoc preceding comment will be part of the command text + result.push_str(">>>"); + result.push_str(&format_inline_comment(&close_heredoc, true)); + } else { + let close_brace = command + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Command section should have a close brace"); + // Close brace preceding comment will be part of the command text + result.push('}'); + result.push_str(&format_inline_comment(&close_brace, true)); + } + + result +} + +/// Format a runtime section +fn format_runtime_section(runtime: RuntimeSection) -> String { + let mut result = String::new(); + let one_indent = INDENT; + let two_indents = INDENT.repeat(2); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(runtime.syntax().clone()), + 1, + false, + )); + result.push_str(one_indent); + result.push_str("runtime"); + result.push_str(&format_inline_comment( + &runtime + .syntax() + .first_child_or_token() + .expect("Runtime section should have a first child"), + false, + )); + + let open_brace = runtime + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Runtime section should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 1, + !result.ends_with(NEWLINE), + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } else { + result.push_str(one_indent); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for item in runtime.items() { + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(item.syntax().clone()), + 2, + false, + )); + result.push_str(&two_indents); + result.push_str(item.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(item.name().syntax().clone()), + false, + )); + + let colon = item + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Colon) + .expect("Runtime item should have a colon"); + result.push_str(&format_preceding_comments( + &colon, + 2, + !result.ends_with(NEWLINE), + )); + result.push(':'); + result.push_str(&format_inline_comment(&colon, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(item.expr().syntax().clone()), + 2, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&two_indents); + } else { + result.push(' '); + } + result.push_str(&item.expr().syntax().to_string()); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(item.syntax().clone()), + true, + )); + } + + let close_brace = runtime + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Runtime section should have a close brace"); + if !result.ends_with(NEWLINE) { + result.push_str(NEWLINE); + } + result.push_str(&format_preceding_comments(&close_brace, 1, false)); + result.push_str(one_indent); + result.push('}'); + result.push_str(&format_inline_comment(&close_brace, true)); + + result +} + +/// Format a task definition. +pub fn format_task(task_def: &TaskDefinition) -> String { + let mut result = String::new(); + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(task_def.syntax().clone()), + 0, + false, + )); + result.push_str("task"); + result.push_str(&format_inline_comment( + &task_def + .syntax() + .first_child_or_token() + .expect("Task definition should have a first child"), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(task_def.name().syntax().clone()), + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with("task") { + result.push(' '); + } else { + result.push_str(INDENT); + } + result.push_str(task_def.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(task_def.name().syntax().clone()), + false, + )); + + let open_brace = task_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Task definition should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 0, + !result.ends_with(NEWLINE), + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + let mut meta_section_str = String::new(); + let mut parameter_meta_section_str = String::new(); + let mut input_section_str = String::new(); + let mut declaration_section_str = String::new(); + let mut command_section_str = String::new(); + let mut output_section_str = String::new(); + let mut runtime_section_str = String::new(); + for item in task_def.items() { + match item { + TaskItem::Metadata(m) => { + meta_section_str.push_str(&format_meta_section(m)); + } + TaskItem::ParameterMetadata(pm) => { + parameter_meta_section_str.push_str(&format_parameter_meta_section(pm)); + } + TaskItem::Input(i) => { + input_section_str.push_str(&format_input_section(i)); + } + TaskItem::Declaration(d) => { + declaration_section_str.push_str(&format_declaration(&Decl::Bound(d), 1)); + } + TaskItem::Command(c) => { + command_section_str.push_str(&format_command_section(c)); + } + TaskItem::Output(o) => { + output_section_str.push_str(&format_output_section(o)); + } + TaskItem::Runtime(r) => { + runtime_section_str.push_str(&format_runtime_section(r)); + } + TaskItem::Requirements(r) => { + // TODO + } + TaskItem::Hints(h) => { + // TODO + } + } + } + + if !meta_section_str.is_empty() { + result.push_str(&meta_section_str); + result.push_str(NEWLINE); + } + if !parameter_meta_section_str.is_empty() { + result.push_str(¶meter_meta_section_str); + result.push_str(NEWLINE); + } + if !input_section_str.is_empty() { + result.push_str(&input_section_str); + result.push_str(NEWLINE); + } + if !declaration_section_str.is_empty() { + result.push_str(&declaration_section_str); + result.push_str(NEWLINE); + } + if !command_section_str.is_empty() { + result.push_str(&command_section_str); + if !output_section_str.is_empty() || !runtime_section_str.is_empty() { + result.push_str(NEWLINE); + } + } + if !output_section_str.is_empty() { + result.push_str(&output_section_str); + if !runtime_section_str.is_empty() { + result.push_str(NEWLINE); + } + } + if !runtime_section_str.is_empty() { + result.push_str(&runtime_section_str); + } + + let close_brace = task_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Task definition should have a close brace"); + if !result.ends_with(NEWLINE) { + result.push_str(NEWLINE); + } + result.push_str(&format_preceding_comments(&close_brace, 0, false)); + result.push('}'); + result.push_str(&format_inline_comment(&close_brace, true)); + + result +} diff --git a/wdl-format/src/format/workflow.rs b/wdl-format/src/format/workflow.rs new file mode 100644 index 00000000..a56ee4d1 --- /dev/null +++ b/wdl-format/src/format/workflow.rs @@ -0,0 +1,807 @@ +//! Format a workflow definition. +use wdl_ast::v1::CallStatement; +use wdl_ast::v1::ConditionalStatement; +use wdl_ast::v1::ScatterStatement; +use wdl_ast::v1::WorkflowDefinition; +use wdl_ast::v1::WorkflowItem; +use wdl_ast::v1::WorkflowStatement; +use wdl_ast::AstNode; +use wdl_ast::AstToken; +use wdl_ast::SyntaxElement; +use wdl_ast::SyntaxKind; + +use super::comments::format_inline_comment; +use super::comments::format_preceding_comments; +use super::INDENT; +use super::NEWLINE; +use super::*; + +/// Format a call statement. +fn format_call_statement(call: CallStatement, num_indents: usize) -> String { + let mut result = String::new(); + let next_num_indents = num_indents + 1; + let cur_indents = INDENT.repeat(num_indents); + let next_indents = INDENT.repeat(next_num_indents); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(call.syntax().clone()), + num_indents, + false, + )); + result.push_str(&cur_indents); + result.push_str("call"); + result.push_str(&format_inline_comment( + &call + .syntax() + .first_child_or_token() + .expect("Call statement should have a child"), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(call.target().syntax().clone()), + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with("call") { + result.push(' '); + } else if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } + result.push_str(&call.target().syntax().to_string()); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(call.target().syntax().clone()), + false, + )); + + if let Some(alias) = call.alias() { + for child in alias.syntax().children_with_tokens() { + match child.kind() { + SyntaxKind::AsKeyword => { + result.push_str(&format_preceding_comments( + &child, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str("as"); + result.push_str(&format_inline_comment(&child, false)) + } + SyntaxKind::Ident => { + result.push_str(&format_preceding_comments( + &child, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(&child.to_string()); + + // This will be the last child processed which means it won't have any "next" + // siblings. So we go up a level and check if there are + // siblings of the 'CallAliasNode'. + result.push_str(&format_inline_comment( + &SyntaxElement::Node(alias.syntax().clone()), + false, + )); + } + SyntaxKind::Whitespace => { + // Ignore + } + SyntaxKind::Comment => { + // Handled by another match arm + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", child.kind()); + } + } + } + } + + for after in call.after() { + for child in after.syntax().children_with_tokens() { + match child.kind() { + SyntaxKind::AfterKeyword => { + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(after.syntax().clone()), + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str("after"); + result.push_str(&format_inline_comment(&child, false)); + } + SyntaxKind::Ident => { + result.push_str(&format_preceding_comments( + &child, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(&child.to_string()); + + // This will be the last child processed which means it won't have any "next" + // siblings. So we go up a level and check if there are + // siblings of the 'CallAfterNode'. + result.push_str(&format_inline_comment( + &SyntaxElement::Node(after.syntax().clone()), + false, + )); + } + SyntaxKind::Whitespace => { + // Ignore + } + SyntaxKind::Comment => { + // Handled by another match arm + } + _ => { + unreachable!("Unexpected syntax kind: {:?}", child.kind()); + } + } + } + } + + let inputs: Vec<_> = call.inputs().collect(); + if !inputs.is_empty() { + let open_brace = call + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Call statement should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, false)); + + let input_keyword = call + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::InputKeyword) + .expect("Call statement should have an input keyword"); + result.push_str(&format_preceding_comments( + &input_keyword, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str("input"); + result.push_str(&format_inline_comment(&input_keyword, false)); + + let colon = call + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Colon) + .expect("Call statement should have a colon"); + result.push_str(&format_preceding_comments( + &colon, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } + result.push(':'); + result.push_str(&format_inline_comment(&colon, false)); + + if inputs.len() == 1 { + let input = &inputs[0]; + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(input.syntax().clone()), + next_num_indents, + false, + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(input.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(input.name().syntax().clone()), + false, + )); + + if let Some(expr) = input.expr() { + let equal_sign = input + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Assignment) + .expect("Call input should have an equal sign"); + result.push_str(&format_preceding_comments( + &equal_sign, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push('='); + result.push_str(&format_inline_comment(&equal_sign, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(expr.syntax().clone()), + next_num_indents, + false, + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } else { + result.push_str(&next_indents); + } + result.push_str(&expr.syntax().to_string()); // TODO: format expressions + } + + result.push_str(&format_inline_comment( + &SyntaxElement::Node(input.syntax().clone()), + false, + )); + + // TODO check for comments belonging to a potential comma + + let close_brace = call + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Call statement should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, num_indents, false)); + if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } else { + result.push(' '); + } + result.push('}'); + } else { + // Multiple inputs + if !result.ends_with(NEWLINE) { + result.push_str(NEWLINE); + } + + let mut commas = call + .syntax() + .children_with_tokens() + .filter(|c| c.kind() == SyntaxKind::Comma); + for item in inputs { + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(item.syntax().clone()), + next_num_indents, + false, + )); + + result.push_str(&next_indents); + result.push_str(item.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(item.name().syntax().clone()), + false, + )); + + if let Some(expr) = item.expr() { + let equal_sign = item + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::Assignment) + .expect("Call input should have an equal sign"); + result.push_str(&format_preceding_comments( + &equal_sign, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push('='); + result.push_str(&format_inline_comment(&equal_sign, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(expr.syntax().clone()), + next_num_indents, + false, + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } else { + result.push_str(&next_indents); + } + result.push_str(&expr.syntax().to_string()); // TODO: format expressions + } + + result.push_str(&format_inline_comment( + &SyntaxElement::Node(item.syntax().clone()), + false, + )); + + if let Some(cur_comma) = commas.next() { + result.push_str(&format_preceding_comments( + &cur_comma, + next_num_indents, + !result.ends_with(NEWLINE), + )); + result.push(','); + result.push_str(&format_inline_comment(&cur_comma, false)); + } else { + result.push(','); + } + if !result.ends_with(NEWLINE) { + result.push_str(NEWLINE); + } + } + + let close_brace = call + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Call statement should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, num_indents, false)); + result.push_str(&cur_indents); + result.push('}'); + } + } + + result.push_str(&format_inline_comment( + &SyntaxElement::Node(call.syntax().clone()), + true, + )); + + result +} + +/// Format a conditional statement. +fn format_conditional(conditional: ConditionalStatement, num_indents: usize) -> String { + let mut result = String::new(); + let next_num_indents = num_indents + 1; + let cur_indents = INDENT.repeat(num_indents); + let next_indents = INDENT.repeat(next_num_indents); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(conditional.syntax().clone()), + num_indents, + false, + )); + + let if_keyword = conditional + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::IfKeyword) + .expect("Conditional statement should have an if keyword"); + result.push_str(&cur_indents); + result.push_str("if"); + result.push_str(&format_inline_comment(&if_keyword, false)); + + let open_paren = conditional + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenParen) + .expect("Conditional statement should have an open paren"); + result.push_str(&format_preceding_comments( + &open_paren, + num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } else { + result.push(' '); + } + result.push('('); + let mut paren_on_same_line = true; + result.push_str(&format_inline_comment(&open_paren, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(conditional.expr().syntax().clone()), + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + paren_on_same_line = false; + result.push_str(&next_indents); + } + let conditional_expr = conditional.expr().syntax().to_string(); + if conditional_expr.contains('\n') { + paren_on_same_line = false; + } + result.push_str(&conditional_expr); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(conditional.expr().syntax().clone()), + false, + )); + + let close_paren = conditional + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseParen) + .expect("Conditional statement should have a close paren"); + result.push_str(&format_preceding_comments( + &close_paren, + num_indents, + !result.ends_with(NEWLINE), + )); + if !paren_on_same_line && result.ends_with(&conditional_expr) { + // No comments were added after the multi-line conditional expression + // So let's start a new line with the proper indentation + result.push_str(NEWLINE); + result.push_str(&cur_indents); + } else if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } + result.push(')'); + result.push_str(&format_inline_comment(&close_paren, false)); + + let open_brace = conditional + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Conditional statement should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(')') { + result.push(' '); + } else { + result.push_str(&cur_indents); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for statement in conditional.statements() { + match statement { + WorkflowStatement::Call(c) => { + result.push_str(&format_call_statement(c, next_num_indents)); + } + WorkflowStatement::Conditional(c) => { + result.push_str(&format_conditional(c, next_num_indents)); + } + WorkflowStatement::Scatter(s) => { + result.push_str(&format_scatter(s, next_num_indents)); + } + WorkflowStatement::Declaration(d) => { + result.push_str(&format_declaration(&Decl::Bound(d), next_num_indents)); + } + } + } + + let close_brace = conditional + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Conditional statement should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, num_indents, false)); + result.push_str(&cur_indents); + result.push('}'); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(conditional.syntax().clone()), + true, + )); + + result +} + +/// Format a scatter statement +fn format_scatter(scatter: ScatterStatement, num_indents: usize) -> String { + let mut result = String::new(); + let next_num_indents = num_indents + 1; + let cur_indents = INDENT.repeat(num_indents); + let next_indents = INDENT.repeat(next_num_indents); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(scatter.syntax().clone()), + num_indents, + false, + )); + + let scatter_keyword = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::ScatterKeyword) + .expect("Scatter statement should have a scatter keyword"); + result.push_str(&cur_indents); + result.push_str("scatter"); + result.push_str(&format_inline_comment(&scatter_keyword, false)); + + let open_paren = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenParen) + .expect("Scatter statement should have an open paren"); + result.push_str(&format_preceding_comments( + &open_paren, + num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } else { + result.push(' '); + } + result.push('('); + result.push_str(&format_inline_comment(&open_paren, false)); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(scatter.variable().syntax().clone()), + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } + result.push_str(scatter.variable().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(scatter.variable().syntax().clone()), + false, + )); + + let in_keyword = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::InKeyword) + .expect("Scatter statement should have an in keyword"); + result.push_str(&format_preceding_comments( + &in_keyword, + next_num_indents, + !result.ends_with(NEWLINE), + )); + + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str("in"); + result.push_str(&format_inline_comment(&in_keyword, false)); + + let mut paren_on_same_line = true; + let scatter_expr = scatter.expr().syntax().to_string(); + if scatter_expr.contains('\n') { + paren_on_same_line = false; + } + + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(scatter.expr().syntax().clone()), + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(NEWLINE) { + result.push_str(&next_indents); + } else { + result.push(' '); + } + result.push_str(&scatter_expr); // TODO: format expr + result.push_str(&format_inline_comment( + &SyntaxElement::Node(scatter.expr().syntax().clone()), + false, + )); + + let close_paren = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseParen) + .expect("Scatter statement should have a close paren"); + result.push_str(&format_preceding_comments( + &close_paren, + num_indents, + !result.ends_with(NEWLINE), + )); + if !paren_on_same_line && result.ends_with(&scatter_expr) { + // No comments were added after the scatter expression (which would reset the + // indentation) So let's start a new line with the proper + // indentation + result.push_str(NEWLINE); + result.push_str(&cur_indents); + } else if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } + result.push(')'); + result.push_str(&format_inline_comment(&close_paren, false)); + + let open_brace = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Scatter statement should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + next_num_indents, + !result.ends_with(NEWLINE), + )); + if result.ends_with(')') { + result.push(' '); + } else { + result.push_str(&cur_indents); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + for statement in scatter.statements() { + match statement { + WorkflowStatement::Call(c) => { + result.push_str(&format_call_statement(c, next_num_indents)); + } + WorkflowStatement::Conditional(c) => { + result.push_str(&format_conditional(c, next_num_indents)); + } + WorkflowStatement::Scatter(s) => { + result.push_str(&format_scatter(s, next_num_indents)); + } + WorkflowStatement::Declaration(d) => { + result.push_str(&format_declaration(&Decl::Bound(d), next_num_indents)); + } + } + } + + let close_brace = scatter + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Scatter statement should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, num_indents, false)); + if result.ends_with(NEWLINE) { + result.push_str(&cur_indents); + } else { + result.push_str(NEWLINE); + result.push_str(&cur_indents); + } + result.push('}'); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(scatter.syntax().clone()), + true, + )); + + result +} + +/// Format a workflow definition. +pub fn format_workflow(workflow_def: &WorkflowDefinition) -> String { + let mut result = String::new(); + result.push_str(&format_preceding_comments( + &SyntaxElement::Node(workflow_def.syntax().clone()), + 0, + false, + )); + result.push_str("workflow"); + result.push_str(&format_inline_comment( + &workflow_def + .syntax() + .first_child_or_token() + .expect("Workflow definition should have a child"), + false, + )); + + result.push_str(&format_preceding_comments( + &SyntaxElement::Token(workflow_def.name().syntax().clone()), + 1, + !result.ends_with(NEWLINE), + )); + if result.ends_with("workflow") { + result.push(' '); + } else { + result.push_str(INDENT); + } + result.push_str(workflow_def.name().as_str()); + result.push_str(&format_inline_comment( + &SyntaxElement::Token(workflow_def.name().syntax().clone()), + false, + )); + + let open_brace = workflow_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::OpenBrace) + .expect("Workflow definition should have an open brace"); + result.push_str(&format_preceding_comments( + &open_brace, + 0, + !result.ends_with(NEWLINE), + )); + if !result.ends_with(NEWLINE) { + result.push(' '); + } + result.push('{'); + result.push_str(&format_inline_comment(&open_brace, true)); + + let mut meta_section_str = String::new(); + let mut parameter_meta_section_str = String::new(); + let mut input_section_str = String::new(); + let mut body_str = String::new(); + let mut output_section_str = String::new(); + for item in workflow_def.items() { + match item { + WorkflowItem::Metadata(m) => { + meta_section_str.push_str(&format_meta_section(m)); + } + WorkflowItem::ParameterMetadata(pm) => { + parameter_meta_section_str.push_str(&format_parameter_meta_section(pm)); + } + WorkflowItem::Input(i) => { + input_section_str.push_str(&format_input_section(i)); + } + WorkflowItem::Output(o) => { + output_section_str.push_str(&format_output_section(o)); + } + WorkflowItem::Call(c) => { + body_str.push_str(&format_call_statement(c, 1)); + } + WorkflowItem::Conditional(c) => { + body_str.push_str(&format_conditional(c, 1)); + } + WorkflowItem::Scatter(s) => { + body_str.push_str(&format_scatter(s, 1)); + } + WorkflowItem::Declaration(d) => { + body_str.push_str(&format_declaration(&Decl::Bound(d), 1)); + } + WorkflowItem::Hints(h) => { + // TODO + } + } + } + + if !meta_section_str.is_empty() { + result.push_str(&meta_section_str); + result.push_str(NEWLINE); + } + if !parameter_meta_section_str.is_empty() { + result.push_str(¶meter_meta_section_str); + result.push_str(NEWLINE); + } + if !input_section_str.is_empty() { + result.push_str(&input_section_str); + result.push_str(NEWLINE); + } + if !body_str.is_empty() { + result.push_str(&body_str); + if !output_section_str.is_empty() { + result.push_str(NEWLINE); + } + } + if !output_section_str.is_empty() { + result.push_str(&output_section_str); + } + + let close_brace = workflow_def + .syntax() + .children_with_tokens() + .find(|c| c.kind() == SyntaxKind::CloseBrace) + .expect("Workflow definition should have a close brace"); + result.push_str(&format_preceding_comments(&close_brace, 0, false)); + if !result.ends_with(NEWLINE) { + result.push_str(NEWLINE); + } + result.push('}'); + result.push_str(&format_inline_comment( + &SyntaxElement::Node(workflow_def.syntax().clone()), + true, + )); + + result +} diff --git a/wdl-format/src/lib.rs b/wdl-format/src/lib.rs new file mode 100644 index 00000000..ecb4f1e5 --- /dev/null +++ b/wdl-format/src/lib.rs @@ -0,0 +1,10 @@ +//! A library for auto-formatting WDL code. + +#![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)] + +pub mod format; diff --git a/wdl-format/tests/format.rs b/wdl-format/tests/format.rs new file mode 100644 index 00000000..e4156aa4 --- /dev/null +++ b/wdl-format/tests/format.rs @@ -0,0 +1,192 @@ +//! The format file tests. +//! +//! This test looks for directories in `tests/format`. +//! +//! Each directory is expected to contain: +//! +//! * `source.wdl` - the test input source to parse. +//! * `source.formatted` - the expected formatted output. +//! +//! The `source.formatted` file may be automatically generated or updated by +//! setting the `BLESS` environment variable when running this test. + +use std::collections::HashSet; +use std::env; +use std::ffi::OsStr; +use std::fs; +use std::path::Path; +use std::path::PathBuf; +use std::process::exit; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; + +use codespan_reporting::files::SimpleFile; +use codespan_reporting::term; +use codespan_reporting::term::termcolor::Buffer; +use codespan_reporting::term::Config; +use colored::Colorize; +use pretty_assertions::StrComparison; +use rayon::prelude::*; +use wdl_ast::Diagnostic; +use wdl_format::format::format_document; + +fn find_tests() -> Vec { + // Check for filter arguments consisting of test names + let mut filter = HashSet::new(); + for arg in std::env::args().skip_while(|a| a != "--").skip(1) { + if !arg.starts_with('-') { + filter.insert(arg); + } + } + + let mut tests: Vec = Vec::new(); + for entry in Path::new("tests/format").read_dir().unwrap() { + let entry = entry.expect("failed to read directory"); + let path = entry.path(); + if !path.is_dir() + || (!filter.is_empty() + && !filter.contains(entry.file_name().to_str().expect("name should be UTF-8"))) + { + continue; + } + + tests.push(path); + } + + tests.sort(); + tests +} + +fn format_diagnostics(diagnostics: &[Diagnostic], path: &Path, source: &str) -> String { + let file = SimpleFile::new(path.as_os_str().to_str().unwrap(), source); + let mut buffer = Buffer::no_color(); + for diagnostic in diagnostics { + term::emit( + &mut buffer, + &Config::default(), + &file, + &diagnostic.to_codespan(), + ) + .expect("should emit"); + } + + String::from_utf8(buffer.into_inner()).expect("should be UTF-8") +} + +fn compare_result(path: &Path, result: &str) -> Result<(), String> { + if env::var_os("BLESS").is_some() { + fs::write(path, &result).map_err(|e| { + format!( + "failed to write result file `{path}`: {e}", + path = path.display() + ) + })?; + return Ok(()); + } + + let expected = fs::read_to_string(path) + .map_err(|e| { + format!( + "failed to read result file `{path}`: {e}", + path = path.display() + ) + })? + .replace("\r\n", "\n"); + + if expected != result { + return Err(format!( + "result is not as expected:\n{}", + StrComparison::new(&expected, &result), + )); + } + + Ok(()) +} + +fn run_test(test: &Path, ntests: &AtomicUsize) -> Result<(), String> { + let path = test.join("source.wdl"); + let source = std::fs::read_to_string(&path).map_err(|e| { + format!( + "failed to read source file `{path}`: {e}", + path = path.display() + ) + })?; + + let formatted = format_document(&source).map_err(|e| { + format!( + "failed to format `{path}`: {e}", + path = path.display(), + e = format_diagnostics(&e, path.as_path(), &source) + ) + })?; + compare_result(path.with_extension("formatted").as_path(), &formatted)?; + + ntests.fetch_add(1, Ordering::SeqCst); + Ok(()) +} + +fn main() { + let tests = find_tests(); + println!("\nrunning {} tests\n", tests.len()); + + let ntests = AtomicUsize::new(0); + let errors = tests + .par_iter() + .filter_map(|test| { + let test_name = test.file_stem().and_then(OsStr::to_str).unwrap(); + match std::panic::catch_unwind(|| { + match run_test(test, &ntests) + .map_err(|e| format!("failed to run test `{path}`: {e}", path = test.display())) + .err() + { + Some(e) => { + println!("test {test_name} ... {failed}", failed = "failed".red()); + Some((test_name, e)) + } + None => { + println!("test {test_name} ... {ok}", ok = "ok".green()); + None + } + } + }) { + Ok(result) => result, + Err(e) => { + println!( + "test {test_name} ... {panicked}", + panicked = "panicked".red() + ); + Some(( + test_name, + format!( + "test panicked: {e:?}", + e = e + .downcast_ref::() + .map(|s| s.as_str()) + .or_else(|| e.downcast_ref::<&str>().copied()) + .unwrap_or("no panic message") + ), + )) + } + } + }) + .collect::>(); + + if !errors.is_empty() { + eprintln!( + "\n{count} test(s) {failed}:", + count = errors.len(), + failed = "failed".red() + ); + + for (name, msg) in errors.iter() { + eprintln!("{name}: {msg}", msg = msg.red()); + } + + exit(1); + } + + println!( + "\ntest result: ok. {} passed\n", + ntests.load(Ordering::SeqCst) + ); +} diff --git a/wdl-format/tests/format/version_stmt_no_comments/source.formatted b/wdl-format/tests/format/version_stmt_no_comments/source.formatted new file mode 100644 index 00000000..610bfcc4 --- /dev/null +++ b/wdl-format/tests/format/version_stmt_no_comments/source.formatted @@ -0,0 +1,4 @@ +version 1.0 + +workflow test { +} diff --git a/wdl-format/tests/format/version_stmt_no_comments/source.wdl b/wdl-format/tests/format/version_stmt_no_comments/source.wdl new file mode 100644 index 00000000..caada20d --- /dev/null +++ b/wdl-format/tests/format/version_stmt_no_comments/source.wdl @@ -0,0 +1,7 @@ +version +1.0 +workflow +test +{ + +} \ No newline at end of file diff --git a/wdl-grammar/CHANGELOG.md b/wdl-grammar/CHANGELOG.md index ee0f60f7..604f0623 100644 --- a/wdl-grammar/CHANGELOG.md +++ b/wdl-grammar/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Add support for `meta` and `parameter_meta` sections in struct definitions in + WDL 1.2 ([#127](https://github.com/stjude-rust-labs/wdl/pull/127)). +* Add support for omitting `input` keyword in call statement bodies in WDL 1.2 + ([#125](https://github.com/stjude-rust-labs/wdl/pull/125)). +* Add support for the `Directory` type in WDL 1.2 ([#124](https://github.com/stjude-rust-labs/wdl/pull/124)). +* Add support for multi-line strings in WDL 1.2 ([#123](https://github.com/stjude-rust-labs/wdl/pull/123)). +* Add support for `hints` sections in WDL 1.2 ([#121](https://github.com/stjude-rust-labs/wdl/pull/121)). * Add support for `requirements` sections in WDL 1.2 ([#117](https://github.com/stjude-rust-labs/wdl/pull/117)). * Add support for the exponentiation operator in WDL 1.2 ([#111](https://github.com/stjude-rust-labs/wdl/pull/111)). diff --git a/wdl-grammar/src/diagnostic.rs b/wdl-grammar/src/diagnostic.rs index ccd95f9f..d0af4e94 100644 --- a/wdl-grammar/src/diagnostic.rs +++ b/wdl-grammar/src/diagnostic.rs @@ -1,5 +1,6 @@ //! Definition of diagnostics displayed to users. +use std::cmp::Ordering; use std::fmt; use rowan::TextRange; @@ -56,7 +57,7 @@ impl From for Span { } /// Represents the severity of a diagnostic. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub enum Severity { /// The diagnostic is displayed as an error. Error, @@ -67,7 +68,7 @@ pub enum Severity { } /// Represents a diagnostic to display to the user. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct Diagnostic { /// The optional rule associated with the diagnostic. rule: Option, @@ -83,6 +84,38 @@ pub struct Diagnostic { labels: Vec