diff --git a/wdl-analysis/CHANGELOG.md b/wdl-analysis/CHANGELOG.md index 0b4e099be..51c43f131 100644 --- a/wdl-analysis/CHANGELOG.md +++ b/wdl-analysis/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added analysis support for the WDL 1.2 `env` declaration modifier ([#296](https://github.com/stjude-rust-labs/wdl/pull/296)). * Fixed missing diagnostic for unknown local name when using the abbreviated syntax for specifying a call input ([#292](https://github.com/stjude-rust-labs/wdl/pull/292)) * Added functions for getting type information of task requirements and hints ([#241](https://github.com/stjude-rust-labs/wdl/pull/241)). diff --git a/wdl-analysis/src/document/v1.rs b/wdl-analysis/src/document/v1.rs index c2ea5b08b..1618a935e 100644 --- a/wdl-analysis/src/document/v1.rs +++ b/wdl-analysis/src/document/v1.rs @@ -19,7 +19,6 @@ use wdl_ast::Span; use wdl_ast::SupportedVersion; use wdl_ast::SyntaxNode; use wdl_ast::SyntaxNodeExt; -use wdl_ast::ToSpan; use wdl_ast::TokenStrHash; use wdl_ast::Version; use wdl_ast::v1::Ast; @@ -269,7 +268,7 @@ fn add_namespace( }; // Check for conflicting namespaces - let span = import.uri().syntax().text_range().to_span(); + let span = import.uri().span(); let ns = match import.namespace() { Some((ns, span)) => { if let Some(prev) = document.namespaces.get(&ns) { @@ -589,10 +588,12 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, definition: &Tas // Check for unused input if let Some(severity) = config.unused_input { let name = decl.name(); - if graph - .edges_directed(index, Direction::Outgoing) - .next() - .is_none() + // Don't warn for environment variables as they are always implicitly used + if decl.env().is_none() + && graph + .edges_directed(index, Direction::Outgoing) + .next() + .is_none() { // Determine if the input is really used based on its name and type if is_input_used(name.as_str(), &task.inputs[name.as_str()].ty) { @@ -621,10 +622,12 @@ fn add_task(config: DiagnosticsConfig, document: &mut Document, definition: &Tas // Check for unused declaration if let Some(severity) = config.unused_declaration { let name = decl.name(); - if graph - .edges_directed(index, Direction::Outgoing) - .next() - .is_none() + // Don't warn for environment variables as they are always implicitly used + if decl.env().is_none() + && graph + .edges_directed(index, Direction::Outgoing) + .next() + .is_none() && !decl.syntax().is_rule_excepted(UNUSED_DECL_RULE_ID) { document.diagnostics.push( @@ -1319,7 +1322,7 @@ fn resolve_import( importer_version: &Version, ) -> Result<(Arc, Arc), Option> { let uri = stmt.uri(); - let span = uri.syntax().text_range().to_span(); + let span = uri.span(); let text = match uri.text() { Some(text) => text, None => { diff --git a/wdl-analysis/src/eval/v1.rs b/wdl-analysis/src/eval/v1.rs index 5c040bea9..6dad24bc5 100644 --- a/wdl-analysis/src/eval/v1.rs +++ b/wdl-analysis/src/eval/v1.rs @@ -201,7 +201,8 @@ impl TaskGraphBuilder { // Add reference edges again, but only for the output declaration nodes self.add_reference_edges(version, Some(count), &mut graph, diagnostics); - // Finally, add edges from the command to runtime/requirements/hints + // Finally, add edges from the command to runtime/requirements/hints and + // environment variables if let Some(command) = self.command { if let Some(runtime) = self.runtime { graph.update_edge(runtime, command, ()); @@ -214,6 +215,19 @@ impl TaskGraphBuilder { if let Some(hints) = self.hints { graph.update_edge(hints, command, ()); } + + // As environment variables are implicitly used by commands, add edges from the + // command to the environment variable declarations + for index in self.names.values() { + match &graph[*index] { + TaskGraphNode::Input(decl) | TaskGraphNode::Decl(decl) + if decl.env().is_some() => + { + graph.update_edge(*index, command, ()); + } + _ => continue, + } + } } graph diff --git a/wdl-analysis/tests/analysis/env-vars/source.diagnostics b/wdl-analysis/tests/analysis/env-vars/source.diagnostics new file mode 100644 index 000000000..7a7ebb775 --- /dev/null +++ b/wdl-analysis/tests/analysis/env-vars/source.diagnostics @@ -0,0 +1,6 @@ +warning[UnusedDeclaration]: unused declaration `d` + ┌─ tests/analysis/env-vars/source.wdl:13:12 + │ +13 │ String d = "" + │ ^ + diff --git a/wdl-analysis/tests/analysis/env-vars/source.wdl b/wdl-analysis/tests/analysis/env-vars/source.wdl new file mode 100644 index 000000000..66a85048f --- /dev/null +++ b/wdl-analysis/tests/analysis/env-vars/source.wdl @@ -0,0 +1,24 @@ +## This is a test of using task environment variables in WDL 1.2 + +version 1.2 + +task test { + input { + String a + env String b + env String c = "" + } + + # This is unused because it is not referenced + String d = "" + + # This is *not* unused because it is an environment variable, + # regardless of whether or not it's referenced + env String e = "" + + command <<< + ~{a} + ~{b} + $c + >>> +} diff --git a/wdl-ast/CHANGELOG.md b/wdl-ast/CHANGELOG.md index 97f6d567d..c502aee0b 100644 --- a/wdl-ast/CHANGELOG.md +++ b/wdl-ast/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added AST support for the WDL 1.2 `env` declaration modifier ([#296](https://github.com/stjude-rust-labs/wdl/pull/296)). * Added `braced_scope_span` and `heredoc_scope_span` methods to `AstNodeExt` ([#292](https://github.com/stjude-rust-labs/wdl/pull/292)) * Added constants for the task variable fields, task requirement names, and task hint names ([#265](https://github.com/stjude-rust-labs/wdl/pull/265)). diff --git a/wdl-ast/src/element.rs b/wdl-ast/src/element.rs index e578d4695..b095df467 100644 --- a/wdl-ast/src/element.rs +++ b/wdl-ast/src/element.rs @@ -447,6 +447,8 @@ pub enum Token { DoubleQuote(DoubleQuote), /// The `else` keyword. ElseKeyword(ElseKeyword), + /// The `env` keyword. + EnvKeyword(EnvKeyword), /// The `==` symbol. Equal(Equal), /// The `!` symbol. @@ -585,6 +587,7 @@ ast_element_impl!( dot(): Dot => Dot => Dot, double_quote(): DoubleQuote => DoubleQuote => DoubleQuote, else_keyword(): ElseKeyword => ElseKeyword => ElseKeyword, + env_keyword(): EnvKeyword => EnvKeyword => EnvKeyword, equal(): Equal => Equal => Equal, exclaimation(): Exclamation => Exclamation => Exclamation, exponentiation(): Exponentiation => Exponentiation => Exponentiation, diff --git a/wdl-ast/src/v1/decls.rs b/wdl-ast/src/v1/decls.rs index 72dcb2e19..91ce81769 100644 --- a/wdl-ast/src/v1/decls.rs +++ b/wdl-ast/src/v1/decls.rs @@ -2,6 +2,7 @@ use std::fmt; +use super::EnvKeyword; use super::Expr; use crate::AstNode; use crate::AstToken; @@ -763,6 +764,13 @@ impl fmt::Display for Type { pub struct UnboundDecl(pub(crate) SyntaxNode); impl UnboundDecl { + /// Gets the `env` token, if present. + /// + /// This may only return a token for task inputs (WDL 1.2+). + pub fn env(&self) -> Option { + token(&self.0) + } + /// Gets the type of the declaration. pub fn ty(&self) -> Type { Type::child(&self.0).expect("unbound declaration should have a type") @@ -804,6 +812,14 @@ impl AstNode for UnboundDecl { pub struct BoundDecl(pub(crate) SyntaxNode); impl BoundDecl { + /// Gets the `env` token, if present. + /// + /// This may only return a token for task inputs and private declarations + /// (WDL 1.2+). + pub fn env(&self) -> Option { + token(&self.0) + } + /// Gets the type of the declaration. pub fn ty(&self) -> Type { Type::child(&self.0).expect("bound declaration should have a type") @@ -889,6 +905,17 @@ impl Decl { } } + /// Gets the `env` token, if present. + /// + /// This may only return a token for task inputs and private declarations + /// (WDL 1.2+). + pub fn env(&self) -> Option { + match self { + Self::Bound(d) => d.env(), + Self::Unbound(d) => d.env(), + } + } + /// Gets the type of the declaration. pub fn ty(&self) -> Type { match self { diff --git a/wdl-ast/src/v1/import.rs b/wdl-ast/src/v1/import.rs index 98bf70cfb..9e291a563 100644 --- a/wdl-ast/src/v1/import.rs +++ b/wdl-ast/src/v1/import.rs @@ -13,13 +13,13 @@ use super::ImportKeyword; use super::LiteralString; use crate::AstChildren; use crate::AstNode; +use crate::AstNodeExt; use crate::AstToken; use crate::Ident; use crate::Span; use crate::SyntaxElement; use crate::SyntaxKind; use crate::SyntaxNode; -use crate::ToSpan; use crate::WorkflowDescriptionLanguage; use crate::support::child; use crate::support::children; @@ -92,7 +92,7 @@ impl ImportStatement { _ => return None, } - Some((stem.to_string(), uri.syntax().text_range().to_span())) + Some((stem.to_string(), uri.span())) } } diff --git a/wdl-ast/src/v1/tokens.rs b/wdl-ast/src/v1/tokens.rs index ffeaa016c..4b60f976a 100644 --- a/wdl-ast/src/v1/tokens.rs +++ b/wdl-ast/src/v1/tokens.rs @@ -82,6 +82,7 @@ define_token!( define_token!(Dot, "the `.` symbol", "."); define_token!(DoubleQuote, "the `\"` symbol", "\""); define_token!(ElseKeyword, "the `else` keyword", "else"); +define_token!(EnvKeyword, "the `env` keyword", "env"); define_token!(Equal, "the `=` symbol", "="); define_token!(Exclamation, "the `!` symbol", "!"); define_token!(Exponentiation, "the `**` symbol", "**"); diff --git a/wdl-ast/src/validation.rs b/wdl-ast/src/validation.rs index 9588090bb..263c4120e 100644 --- a/wdl-ast/src/validation.rs +++ b/wdl-ast/src/validation.rs @@ -15,6 +15,7 @@ use crate::VersionStatement; use crate::Visitor; mod counts; +mod env; mod exprs; mod imports; mod keys; @@ -138,6 +139,7 @@ impl Default for Validator { Box::::default(), Box::::default(), Box::::default(), + Box::::default(), ], } } diff --git a/wdl-ast/src/validation/env.rs b/wdl-ast/src/validation/env.rs new file mode 100644 index 000000000..56be62c3a --- /dev/null +++ b/wdl-ast/src/validation/env.rs @@ -0,0 +1,109 @@ +//! Validation of `env` declarations. + +use crate::AstNodeExt; +use crate::AstToken; +use crate::Diagnostic; +use crate::Diagnostics; +use crate::Document; +use crate::Span; +use crate::SupportedVersion; +use crate::VisitReason; +use crate::Visitor; +use crate::v1; +use crate::version::V1; + +/// Creates an "env type not primitive" diagnostic. +fn env_type_not_primitive(env_span: Span, ty: &v1::Type, ty_span: Span) -> Diagnostic { + Diagnostic::error("environment variable modifier can only be used on primitive types") + .with_label( + format!("type `{ty}` cannot be used as an environment variable"), + ty_span, + ) + .with_label( + "declaration is an environment variable due to this modifier", + env_span, + ) +} + +/// Checks the type to see if it is legal as an environment variable. +/// +/// Returns `None` if the type is legal otherwise it returns the span of the +/// type. +fn check_type(ty: &v1::Type) -> Option { + match ty { + v1::Type::Map(ty) => Some(ty.span()), + v1::Type::Array(ty) => Some(ty.span()), + v1::Type::Pair(ty) => Some(ty.span()), + v1::Type::Object(ty) => Some(ty.span()), + v1::Type::Ref(ty) => Some(ty.span()), + v1::Type::Primitive(_) => None, + } +} + +/// An AST visitor that ensures that environment variable modifiers only exist +/// on primitive type declarations. +#[derive(Debug, Default)] +pub struct EnvVisitor { + /// The version of the document we're currently visiting. + version: Option, +} + +impl Visitor for EnvVisitor { + 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 bound_decl(&mut self, state: &mut Self::State, reason: VisitReason, decl: &v1::BoundDecl) { + // Only visit decls for WDL >=1.2 + if self.version.expect("should have a version") < SupportedVersion::V1(V1::Two) { + return; + } + + if reason == VisitReason::Exit { + return; + } + + if let Some(env_span) = decl.env().map(|t| t.span()) { + let ty = decl.ty(); + if let Some(span) = check_type(&ty) { + state.add(env_type_not_primitive(env_span, &ty, span)); + } + } + } + + fn unbound_decl( + &mut self, + state: &mut Self::State, + reason: VisitReason, + decl: &v1::UnboundDecl, + ) { + // Only visit decls for WDL >=1.2 + if self.version.expect("should have a version") < SupportedVersion::V1(V1::Two) { + return; + } + + if reason == VisitReason::Exit { + return; + } + + if let Some(env_span) = decl.env().map(|t| t.span()) { + let ty = decl.ty(); + if let Some(span) = check_type(&ty) { + state.add(env_type_not_primitive(env_span, &ty, span)); + } + } + } +} diff --git a/wdl-ast/src/validation/imports.rs b/wdl-ast/src/validation/imports.rs index ce6c49507..fa87fbd84 100644 --- a/wdl-ast/src/validation/imports.rs +++ b/wdl-ast/src/validation/imports.rs @@ -1,12 +1,11 @@ //! Validation of imports. -use crate::AstNode; +use crate::AstNodeExt; use crate::Diagnostic; use crate::Diagnostics; use crate::Document; use crate::Span; use crate::SupportedVersion; -use crate::ToSpan; use crate::VisitReason; use crate::Visitor; use crate::v1; @@ -64,7 +63,7 @@ impl Visitor for ImportsVisitor { let uri = stmt.uri(); if uri.is_empty() { - state.add(empty_import(uri.syntax().text_range().to_span())); + state.add(empty_import(uri.span())); return; } @@ -73,7 +72,7 @@ impl Visitor for ImportsVisitor { .parts() .find_map(|p| match p { StringPart::Text(_) => None, - StringPart::Placeholder(p) => Some(p.syntax().text_range().to_span()), + StringPart::Placeholder(p) => Some(p.span()), }) .expect("should have a placeholder span"); @@ -82,9 +81,7 @@ impl Visitor for ImportsVisitor { } if stmt.namespace().is_none() { - state.add(invalid_import_namespace( - uri.syntax().text_range().to_span(), - )); + state.add(invalid_import_namespace(uri.span())); } } } diff --git a/wdl-ast/src/validation/strings.rs b/wdl-ast/src/validation/strings.rs index 9a1974962..69f1c7cdc 100644 --- a/wdl-ast/src/validation/strings.rs +++ b/wdl-ast/src/validation/strings.rs @@ -3,10 +3,10 @@ use rowan::ast::AstChildren; use rowan::ast::AstNode; use rowan::ast::support::children; -use wdl_grammar::ToSpan; use wdl_grammar::lexer::v1::EscapeToken; use wdl_grammar::lexer::v1::Logos; +use crate::AstNodeExt; use crate::AstToken; use crate::Diagnostic; use crate::Diagnostics; @@ -190,8 +190,8 @@ impl Visitor for LiteralTextVisitor { if let Some(first) = placeholders.next() { for additional in placeholders { state.add(multiple_placeholder_options( - first.syntax().text_range().to_span(), - additional.syntax().text_range().to_span(), + first.span(), + additional.span(), )); } } diff --git a/wdl-ast/src/validation/version.rs b/wdl-ast/src/validation/version.rs index 9f5771624..ffca95e9a 100644 --- a/wdl-ast/src/validation/version.rs +++ b/wdl-ast/src/validation/version.rs @@ -5,6 +5,8 @@ use wdl_grammar::ToSpan; use wdl_grammar::version::V1; use crate::AstNode; +use crate::AstNodeExt; +use crate::AstToken; use crate::Diagnostic; use crate::Diagnostics; use crate::Document; @@ -58,6 +60,12 @@ fn struct_metadata_requirement(kind: &str, span: Span) -> Diagnostic { .with_highlight(span) } +/// Creates an "env var" requirement diagnostic. +fn env_var_requirement(span: Span) -> Diagnostic { + Diagnostic::error("use of environment variable declarations 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)] @@ -168,9 +176,7 @@ impl Visitor for VersionVisitor { if version < SupportedVersion::V1(V1::Two) && s.kind() == v1::LiteralStringKind::Multiline => { - state.add(multiline_string_requirement( - s.syntax().text_range().to_span(), - )); + state.add(multiline_string_requirement(s.span())); } _ => {} } @@ -183,13 +189,17 @@ impl Visitor for VersionVisitor { } if let Some(version) = self.version { + if let Some(env) = decl.env() { + if version < SupportedVersion::V1(V1::Two) { + state.add(env_var_requirement(env.span())); + } + } + 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(), - )); + state.add(directory_type_requirement(ty.span())); } } } @@ -206,13 +216,17 @@ impl Visitor for VersionVisitor { } if let Some(version) = self.version { + if let Some(env) = decl.env() { + if version < SupportedVersion::V1(V1::Two) { + state.add(env_var_requirement(env.span())); + } + } + 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(), - )); + state.add(directory_type_requirement(ty.span())); } } } @@ -234,9 +248,7 @@ impl Visitor for VersionVisitor { 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(), - )); + state.add(input_keyword_requirement(input.span())); } } } diff --git a/wdl-ast/tests/registry.rs b/wdl-ast/tests/registry.rs index 49df23f82..16624e4e3 100644 --- a/wdl-ast/tests/registry.rs +++ b/wdl-ast/tests/registry.rs @@ -88,6 +88,7 @@ static REGISTRY: LazyLock>> = LazyLock:: v1::Dot::register(), v1::DoubleQuote::register(), v1::ElseKeyword::register(), + v1::EnvKeyword::register(), v1::Equal::register(), v1::EqualityExpr::register(), v1::Exclamation::register(), diff --git a/wdl-ast/tests/validation/env-vars-unsupported/source.errors b/wdl-ast/tests/validation/env-vars-unsupported/source.errors new file mode 100644 index 000000000..e830c9433 --- /dev/null +++ b/wdl-ast/tests/validation/env-vars-unsupported/source.errors @@ -0,0 +1,12 @@ +error: use of environment variable declarations requires WDL version 1.2 + ┌─ tests/validation/env-vars-unsupported/source.wdl:8:9 + │ +8 │ env String b + │ ^^^ + +error: use of environment variable declarations requires WDL version 1.2 + ┌─ tests/validation/env-vars-unsupported/source.wdl:12:5 + │ +12 │ env String d = "" + │ ^^^ + diff --git a/wdl-ast/tests/validation/env-vars-unsupported/source.wdl b/wdl-ast/tests/validation/env-vars-unsupported/source.wdl new file mode 100644 index 000000000..9163720b3 --- /dev/null +++ b/wdl-ast/tests/validation/env-vars-unsupported/source.wdl @@ -0,0 +1,15 @@ +## This is a test of using environment variable declarations in < 1.2 + +version 1.1 + +task test { + input { + String a + env String b + } + + String c = "" + env String d = "" + + command <<<>>> +} diff --git a/wdl-ast/tests/validation/env-vars/source.errors b/wdl-ast/tests/validation/env-vars/source.errors new file mode 100644 index 000000000..e69de29bb diff --git a/wdl-ast/tests/validation/env-vars/source.wdl b/wdl-ast/tests/validation/env-vars/source.wdl new file mode 100644 index 000000000..58b4c9eaa --- /dev/null +++ b/wdl-ast/tests/validation/env-vars/source.wdl @@ -0,0 +1,16 @@ +## This is a test of using environment variable declarations in 1.2 +## No diagnostics should be emitted + +version 1.2 + +task test { + input { + String a + env String b + } + + String c = "" + env String d = "" + + command <<<>>> +} diff --git a/wdl-ast/tests/validation/invalid-use-type/source.errors b/wdl-ast/tests/validation/invalid-use-type/source.errors new file mode 100644 index 000000000..eaad61701 --- /dev/null +++ b/wdl-ast/tests/validation/invalid-use-type/source.errors @@ -0,0 +1,16 @@ +error: environment variable modifier can only be used on primitive types + ┌─ tests/validation/invalid-use-type/source.wdl:15:13 + │ +15 │ env Array[String] g + │ --- ^^^^^^^^^^^^^ type `Array[String]` cannot be used as an environment variable + │ │ + │ declaration is an environment variable due to this modifier + +error: environment variable modifier can only be used on primitive types + ┌─ tests/validation/invalid-use-type/source.wdl:26:9 + │ +26 │ env Array[String] n = [1, 2, 3] + │ --- ^^^^^^^^^^^^^ type `Array[String]` cannot be used as an environment variable + │ │ + │ declaration is an environment variable due to this modifier + diff --git a/wdl-ast/tests/validation/invalid-use-type/source.wdl b/wdl-ast/tests/validation/invalid-use-type/source.wdl new file mode 100644 index 000000000..1e66ad8c6 --- /dev/null +++ b/wdl-ast/tests/validation/invalid-use-type/source.wdl @@ -0,0 +1,29 @@ +## This is a test of using the `env` keyword on a non-primitive type. + +version 1.2 + +task test { + input { + env String a + env Float b + env Int c + env File d + env Directory e + env Boolean f + + # NOT OK + env Array[String] g + } + + env String h = "" + env Float i = 1.0 + env Int j = 1 + env File k = "" + env Directory l = "" + env Boolean m = "" + + # NOT OK + env Array[String] n = [1, 2, 3] + + command <<<>>> +} diff --git a/wdl-engine/CHANGELOG.md b/wdl-engine/CHANGELOG.md index 017a6eac9..1909e6589 100644 --- a/wdl-engine/CHANGELOG.md +++ b/wdl-engine/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +* Added evaluation support for the WDL 1.2 `env` declaration modifier ([#296](https://github.com/stjude-rust-labs/wdl/pull/296)). * Implemented workflow evaluation ([#292](https://github.com/stjude-rust-labs/wdl/pull/292)) * Reduced size of the `Value` type ([#277](https://github.com/stjude-rust-labs/wdl/pull/277)). * Implement task evaluation with local execution and remaining WDL 1.2 diff --git a/wdl-engine/src/backend.rs b/wdl-engine/src/backend.rs index d8cf194b1..649be9a89 100644 --- a/wdl-engine/src/backend.rs +++ b/wdl-engine/src/backend.rs @@ -103,6 +103,7 @@ pub trait TaskExecution: Send { command: &str, requirements: &HashMap, hints: &HashMap, + env: &[(String, String)], ) -> Result>>; } diff --git a/wdl-engine/src/backend/local.rs b/wdl-engine/src/backend/local.rs index 6ae2ecaa5..f73d49f4d 100644 --- a/wdl-engine/src/backend/local.rs +++ b/wdl-engine/src/backend/local.rs @@ -1,6 +1,7 @@ //! Implementation of the local backend. use std::collections::HashMap; +use std::ffi::OsStr; use std::fs; use std::fs::File; use std::path::Path; @@ -181,6 +182,7 @@ impl TaskExecution for LocalTaskExecution { command: &str, _: &HashMap, _: &HashMap, + envs: &[(String, String)], ) -> Result>> { // Recreate the working directory if self.work_dir.exists() { @@ -231,6 +233,7 @@ impl TaskExecution for LocalTaskExecution { .stdin(Stdio::null()) .stdout(stdout) .stderr(stderr) + .envs(envs.iter().map(|(k, v)| (OsStr::new(k), OsStr::new(v)))) .kill_on_drop(true); // Set an environment variable on Windows to get consistent PATH searching diff --git a/wdl-engine/src/eval/v1/task.rs b/wdl-engine/src/eval/v1/task.rs index 6069d29c1..79d18cf0b 100644 --- a/wdl-engine/src/eval/v1/task.rs +++ b/wdl-engine/src/eval/v1/task.rs @@ -282,16 +282,36 @@ impl<'a> TaskEvaluator<'a> { ); let mut state = State::new(document, task, self.backend.create_execution(root)?); - + let mut envs = Vec::new(); let nodes = toposort(&graph, None).expect("graph should be acyclic"); let mut current = 0; while current < nodes.len() { match &graph[nodes[current]] { TaskGraphNode::Input(decl) => { - self.evaluate_input(&mut state, decl, inputs)?; + let value = self.evaluate_input(&mut state, decl, inputs)?; + if decl.env().is_some() { + envs.push(( + decl.name().as_str().to_string(), + value + .as_primitive() + .expect("value should be primitive") + .raw() + .to_string(), + )); + } } TaskGraphNode::Decl(decl) => { - self.evaluate_decl(&mut state, decl)?; + let value = self.evaluate_decl(&mut state, decl)?; + if decl.env().is_some() { + envs.push(( + decl.name().as_str().to_string(), + value + .as_primitive() + .expect("value should be primitive") + .raw() + .to_string(), + )); + } } TaskGraphNode::Output(_) => { // Stop at the first output; at this point the task can be executed @@ -361,7 +381,7 @@ impl<'a> TaskEvaluator<'a> { let status_code = state .execution - .spawn(&state.command, &state.requirements, &state.hints)? + .spawn(&state.command, &state.requirements, &state.hints, &envs)? .await?; // TODO: support retrying the task if it fails @@ -420,7 +440,7 @@ impl<'a> TaskEvaluator<'a> { state: &mut State<'_>, decl: &Decl, inputs: &TaskInputs, - ) -> EvaluationResult<()> { + ) -> EvaluationResult { let name = decl.name(); let decl_ty = decl.ty(); let ty = crate::convert_ast_type_v1(state.document, &decl_ty)?; @@ -450,12 +470,12 @@ impl<'a> TaskEvaluator<'a> { let value = value .coerce(&ty) .map_err(|e| runtime_type_mismatch(e, &ty, name.span(), &value.ty(), span))?; - state.scopes[ROOT_SCOPE_INDEX.0].insert(name.as_str(), value); - Ok(()) + state.scopes[ROOT_SCOPE_INDEX.0].insert(name.as_str(), value.clone()); + Ok(value) } /// Evaluates a task private declaration. - fn evaluate_decl(&mut self, state: &mut State<'_>, decl: &Decl) -> EvaluationResult<()> { + fn evaluate_decl(&mut self, state: &mut State<'_>, decl: &Decl) -> EvaluationResult { let name = decl.name(); debug!( "evaluating private declaration `{name}` for task `{task}` in `{uri}`", @@ -474,8 +494,8 @@ impl<'a> TaskEvaluator<'a> { let value = value .coerce(&ty) .map_err(|e| runtime_type_mismatch(e, &ty, name.span(), &value.ty(), expr.span()))?; - state.scopes[ROOT_SCOPE_INDEX.0].insert(name.as_str(), value); - Ok(()) + state.scopes[ROOT_SCOPE_INDEX.0].insert(name.as_str(), value.clone()); + Ok(value) } /// Evaluates the runtime section. diff --git a/wdl-engine/src/value.rs b/wdl-engine/src/value.rs index ae167c44d..bf382b024 100644 --- a/wdl-engine/src/value.rs +++ b/wdl-engine/src/value.rs @@ -131,6 +131,26 @@ impl Value { matches!(self, Self::None) } + /// Gets the value as a primitive value. + /// + /// Returns `None` if the value is not a primitive value. + pub fn as_primitive(&self) -> Option<&PrimitiveValue> { + match self { + Self::Primitive(v) => Some(v), + _ => None, + } + } + + /// Gets the value as a compound value. + /// + /// Returns `None` if the value is not a compound value. + pub fn as_compound(&self) -> Option<&CompoundValue> { + match self { + Self::Compound(v) => Some(v), + _ => None, + } + } + /// Gets the value as a `Boolean`. /// /// Returns `None` if the value is not a `Boolean`. diff --git a/wdl-engine/tests/workflows/env-vars/inputs.json b/wdl-engine/tests/workflows/env-vars/inputs.json new file mode 100644 index 000000000..b7a118ae1 --- /dev/null +++ b/wdl-engine/tests/workflows/env-vars/inputs.json @@ -0,0 +1,3 @@ +{ + "environment_variable_should_echo.greeting": "hello world" +} \ No newline at end of file diff --git a/wdl-engine/tests/workflows/env-vars/outputs.json b/wdl-engine/tests/workflows/env-vars/outputs.json new file mode 100644 index 000000000..06fc6d29a --- /dev/null +++ b/wdl-engine/tests/workflows/env-vars/outputs.json @@ -0,0 +1,3 @@ +{ + "environment_variable_should_echo.out": "hello world" +} \ No newline at end of file diff --git a/wdl-engine/tests/workflows/env-vars/source.wdl b/wdl-engine/tests/workflows/env-vars/source.wdl new file mode 100644 index 000000000..15e638947 --- /dev/null +++ b/wdl-engine/tests/workflows/env-vars/source.wdl @@ -0,0 +1,27 @@ +version 1.2 + +task test { + input { + env String greeting + } + + command <<< + echo $greeting + >>> + + output { + String out = read_string(stdout()) + } +} + +workflow environment_variable_should_echo { + input { + String greeting + } + + call test { greeting } + + output { + String out = test.out + } +} diff --git a/wdl-format/tests/format.rs b/wdl-format/tests/format.rs index cbc0681f2..5e20f7ff8 100644 --- a/wdl-format/tests/format.rs +++ b/wdl-format/tests/format.rs @@ -120,13 +120,13 @@ fn compare_result(path: &Path, result: &str) -> Result<(), String> { /// Parses source string into a document FormatElement fn prepare_document(source: &str, path: &Path) -> Result { - let (document, diagnostics) = Document::parse(&source); + let (document, diagnostics) = Document::parse(source); if !diagnostics.is_empty() { return Err(format!( "failed to parse `{path}` {e}", path = path.display(), - e = format_diagnostics(&diagnostics, path, &source) + e = format_diagnostics(&diagnostics, path, source) )); }; @@ -135,7 +135,7 @@ fn prepare_document(source: &str, path: &Path) -> Result /// Parses and formats source string fn format(source: &str, path: &Path) -> Result { - let document = prepare_document(&source, &path)?; + let document = prepare_document(source, path)?; let formatted = match Formatter::default().format(&document) { Ok(formatted) => formatted, Err(e) => { diff --git a/wdl-format/tests/format/env-vars/source.formatted.wdl b/wdl-format/tests/format/env-vars/source.formatted.wdl new file mode 100644 index 000000000..93e11cc80 --- /dev/null +++ b/wdl-format/tests/format/env-vars/source.formatted.wdl @@ -0,0 +1,27 @@ +version 1.2 + +task test { + input { + env String greeting + } + + command <<< + echo $greeting + >>> + + output { + String out = read_string(stdout()) + } +} + +workflow environment_variable_should_echo { + input { + String greeting + } + + call test { greeting } + + output { + String out = test.out + } +} diff --git a/wdl-format/tests/format/env-vars/source.wdl b/wdl-format/tests/format/env-vars/source.wdl new file mode 100644 index 000000000..467654a00 --- /dev/null +++ b/wdl-format/tests/format/env-vars/source.wdl @@ -0,0 +1,28 @@ +version 1.2 + +task test { + input { + env String + greeting + } + + command <<< + echo $greeting + >>> + + output { + String out=read_string(stdout()) + } +} + +workflow environment_variable_should_echo { + input { String greeting + } + + call test { + greeting } + + output { + String out=test.out + } +} diff --git a/wdl-grammar/CHANGELOG.md b/wdl-grammar/CHANGELOG.md index bed927f12..173a074e1 100644 --- a/wdl-grammar/CHANGELOG.md +++ b/wdl-grammar/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* Added parsing support for the WDL 1.2 `env` declaration modifier ([#296](https://github.com/stjude-rust-labs/wdl/pull/296)). + ### Changed * Made construction of a CST from a list of parser events public via the diff --git a/wdl-grammar/src/grammar/v1.rs b/wdl-grammar/src/grammar/v1.rs index b2c085515..f86855e82 100644 --- a/wdl-grammar/src/grammar/v1.rs +++ b/wdl-grammar/src/grammar/v1.rs @@ -323,6 +323,7 @@ const ANY_IDENT: TokenSet = TokenSet::new(&[ Token::CallKeyword as u8, Token::CommandKeyword as u8, Token::ElseKeyword as u8, + Token::EnvKeyword as u8, Token::FalseKeyword as u8, Token::HintsKeyword as u8, Token::IfKeyword as u8, @@ -646,7 +647,7 @@ fn primitive_type(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker /// Parses an item in a task definition. fn task_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Diagnostic)> { match parser.peek() { - Some((Token::InputKeyword, _)) => input_section(parser, marker), + Some((Token::InputKeyword, _)) => input_section(parser, marker, true), Some((Token::CommandKeyword, _)) => command_section(parser, marker), Some((Token::OutputKeyword, _)) => output_section(parser, marker), Some((Token::RuntimeKeyword, _)) => runtime_section(parser, marker), @@ -654,8 +655,8 @@ fn task_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Dia Some((Token::HintsKeyword, _)) => task_hints_section(parser, marker), Some((Token::MetaKeyword, _)) => metadata_section(parser, marker), Some((Token::ParameterMetaKeyword, _)) => parameter_metadata_section(parser, marker), - Some((t, _)) if TYPE_EXPECTED_SET.contains(t.into_raw()) => { - bound_decl(parser, marker, false) + Some((t, _)) if t == Token::EnvKeyword || TYPE_EXPECTED_SET.contains(t.into_raw()) => { + bound_decl(parser, marker, false, true) } found => { let (found, span) = found @@ -672,7 +673,7 @@ fn task_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Dia /// Parses an item in a workflow definition. fn workflow_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Diagnostic)> { match parser.peek() { - Some((Token::InputKeyword, _)) => input_section(parser, marker), + Some((Token::InputKeyword, _)) => input_section(parser, marker, false), Some((Token::OutputKeyword, _)) => output_section(parser, marker), Some((Token::MetaKeyword, _)) => metadata_section(parser, marker), Some((Token::ParameterMetaKeyword, _)) => parameter_metadata_section(parser, marker), @@ -681,7 +682,7 @@ fn workflow_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Some((Token::ScatterKeyword, _)) => scatter_statement(parser, marker), Some((Token::CallKeyword, _)) => call_statement(parser, marker), Some((t, _)) if TYPE_EXPECTED_SET.contains(t.into_raw()) => { - bound_decl(parser, marker, false) + bound_decl(parser, marker, false, false) } found => { let (found, span) = found @@ -702,7 +703,7 @@ fn workflow_statement(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Ma Some((Token::ScatterKeyword, _)) => scatter_statement(parser, marker), Some((Token::CallKeyword, _)) => call_statement(parser, marker), Some((t, _)) if TYPE_EXPECTED_SET.contains(t.into_raw()) => { - bound_decl(parser, marker, false) + bound_decl(parser, marker, false, false) } found => { let (found, span) = found @@ -714,15 +715,33 @@ fn workflow_statement(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Ma } /// Parses an input section in a task or workflow. -fn input_section(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Diagnostic)> { +fn input_section( + parser: &mut Parser<'_>, + marker: Marker, + allow_env: bool, +) -> Result<(), (Marker, Diagnostic)> { parser.require(Token::InputKeyword); - braced_items!(parser, marker, None, INPUT_ITEM_RECOVERY_SET, input_item); + braced_items!( + parser, + marker, + None, + INPUT_ITEM_RECOVERY_SET, + |parser, marker| input_item(parser, marker, allow_env) + ); marker.complete(parser, SyntaxKind::InputSectionNode); Ok(()) } /// Parses an input item. -fn input_item(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker, Diagnostic)> { +fn input_item( + parser: &mut Parser<'_>, + marker: Marker, + allow_env: bool, +) -> Result<(), (Marker, Diagnostic)> { + if allow_env { + parser.next_if(Token::EnvKeyword); + } + expected_fn!(parser, marker, ty); expected_in!(parser, marker, ANY_IDENT, "input name"); parser.update_last_token_kind(SyntaxKind::Ident); @@ -991,7 +1010,7 @@ fn output_section(parser: &mut Parser<'_>, marker: Marker) -> Result<(), (Marker marker, None, OUTPUT_ITEM_RECOVERY_SET, - |parser, marker| bound_decl(parser, marker, true) + |parser, marker| bound_decl(parser, marker, true, false) ); marker.complete(parser, SyntaxKind::OutputSectionNode); Ok(()) @@ -1693,7 +1712,12 @@ fn bound_decl( parser: &mut Parser<'_>, marker: Marker, output: bool, + allow_env: bool, ) -> Result<(), (Marker, Diagnostic)> { + if allow_env { + parser.next_if(Token::EnvKeyword); + } + expected_fn!(parser, marker, ty); if output { diff --git a/wdl-grammar/src/lexer/v1.rs b/wdl-grammar/src/lexer/v1.rs index 55f824499..e9ae4b776 100644 --- a/wdl-grammar/src/lexer/v1.rs +++ b/wdl-grammar/src/lexer/v1.rs @@ -381,6 +381,9 @@ pub enum Token { /// The `else` keyword. #[token("else")] ElseKeyword, + /// The `env` keyword. + #[token("env")] + EnvKeyword, /// The `false` keyword. #[token("false")] FalseKeyword, @@ -560,6 +563,7 @@ impl<'a> ParserToken<'a> for Token { Self::CallKeyword => SyntaxKind::CallKeyword, Self::CommandKeyword => SyntaxKind::CommandKeyword, Self::ElseKeyword => SyntaxKind::ElseKeyword, + Self::EnvKeyword => SyntaxKind::EnvKeyword, Self::FalseKeyword => SyntaxKind::FalseKeyword, Self::HintsKeyword => SyntaxKind::HintsKeyword, Self::IfKeyword => SyntaxKind::IfKeyword, @@ -646,6 +650,7 @@ impl<'a> ParserToken<'a> for Token { Self::CallKeyword => "`call` keyword", Self::CommandKeyword => "`command` keyword", Self::ElseKeyword => "`else` keyword", + Self::EnvKeyword => "`env` keyword", Self::FalseKeyword => "`false` keyword", Self::HintsKeyword => "`hints` keyword", Self::IfKeyword => "`if` keyword", @@ -1408,7 +1413,8 @@ task then true version -workflow"#, +workflow +env"#, ); let tokens: Vec<_> = lexer.map(map).collect(); assert_eq!(tokens, &[ @@ -1486,6 +1492,8 @@ workflow"#, (Ok(VersionKeyword), 222..229), (Ok(Whitespace), 229..230), (Ok(WorkflowKeyword), 230..238), + (Ok(Whitespace), 238..239), + (Ok(EnvKeyword), 239..242), ],); } diff --git a/wdl-grammar/src/tree.rs b/wdl-grammar/src/tree.rs index 12f24349f..438096f25 100644 --- a/wdl-grammar/src/tree.rs +++ b/wdl-grammar/src/tree.rs @@ -86,6 +86,8 @@ pub enum SyntaxKind { CommandKeyword, /// The `else` keyword token. ElseKeyword, + /// The `env` keyword token. + EnvKeyword, /// The `false` keyword token. FalseKeyword, /// The `if` keyword token. @@ -387,197 +389,194 @@ impl SyntaxKind { pub fn is_symbolic(&self) -> bool { matches!( self, - SyntaxKind::Abandoned | SyntaxKind::Unknown | SyntaxKind::Unparsed | SyntaxKind::MAX + Self::Abandoned | Self::Unknown | Self::Unparsed | Self::MAX ) } -} - -/// Every [`SyntaxKind`] variant. -pub static ALL_SYNTAX_KIND: &[SyntaxKind] = SyntaxKind::VARIANTS; -impl From for rowan::SyntaxKind { - fn from(kind: SyntaxKind) -> Self { - rowan::SyntaxKind(kind as u16) - } -} - -impl SyntaxKind { /// Describes the syntax kind. pub fn describe(&self) -> &'static str { match self { - SyntaxKind::Unknown => unreachable!(), - SyntaxKind::Unparsed => unreachable!(), - SyntaxKind::Whitespace => "whitespace", - SyntaxKind::Comment => "comment", - SyntaxKind::Version => "version", - SyntaxKind::Float => "float", - SyntaxKind::Integer => "integer", - SyntaxKind::Ident => "identifier", - SyntaxKind::SingleQuote => "single quote", - SyntaxKind::DoubleQuote => "double quote", - SyntaxKind::OpenHeredoc => "open heredoc", - SyntaxKind::CloseHeredoc => "close heredoc", - SyntaxKind::ArrayTypeKeyword => "`Array` type keyword", - SyntaxKind::BooleanTypeKeyword => "`Boolean` type keyword", - SyntaxKind::FileTypeKeyword => "`File` type keyword", - SyntaxKind::FloatTypeKeyword => "`Float` type keyword", - SyntaxKind::IntTypeKeyword => "`Int` type keyword", - SyntaxKind::MapTypeKeyword => "`Map` type keyword", - SyntaxKind::ObjectTypeKeyword => "`Object` type keyword", - SyntaxKind::PairTypeKeyword => "`Pair` type keyword", - SyntaxKind::StringTypeKeyword => "`String` type keyword", - SyntaxKind::AfterKeyword => "`after` keyword", - SyntaxKind::AliasKeyword => "`alias` keyword", - SyntaxKind::AsKeyword => "`as` keyword", - SyntaxKind::CallKeyword => "`call` keyword", - SyntaxKind::CommandKeyword => "`command` keyword", - SyntaxKind::ElseKeyword => "`else` keyword", - SyntaxKind::FalseKeyword => "`false` keyword", - SyntaxKind::IfKeyword => "`if` keyword", - SyntaxKind::InKeyword => "`in` keyword", - SyntaxKind::ImportKeyword => "`import` keyword", - SyntaxKind::InputKeyword => "`input` keyword", - SyntaxKind::MetaKeyword => "`meta` keyword", - SyntaxKind::NoneKeyword => "`None` keyword", - SyntaxKind::NullKeyword => "`null` keyword", - SyntaxKind::ObjectKeyword => "`object` keyword", - SyntaxKind::OutputKeyword => "`output` keyword", - SyntaxKind::ParameterMetaKeyword => "`parameter_meta` keyword", - SyntaxKind::RuntimeKeyword => "`runtime` keyword", - SyntaxKind::ScatterKeyword => "`scatter` keyword", - SyntaxKind::StructKeyword => "`struct` keyword", - SyntaxKind::TaskKeyword => "`task` keyword", - SyntaxKind::ThenKeyword => "`then` keyword", - SyntaxKind::TrueKeyword => "`true` keyword", - SyntaxKind::VersionKeyword => "`version` keyword", - SyntaxKind::WorkflowKeyword => "`workflow` keyword", - SyntaxKind::DirectoryTypeKeyword => "`Directory` type keyword", - SyntaxKind::HintsKeyword => "`hints` keyword", - SyntaxKind::RequirementsKeyword => "`requirements` keyword", - SyntaxKind::OpenBrace => "`{` symbol", - SyntaxKind::CloseBrace => "`}` symbol", - SyntaxKind::OpenBracket => "`[` symbol", - SyntaxKind::CloseBracket => "`]` symbol", - SyntaxKind::Assignment => "`=` symbol", - SyntaxKind::Colon => "`:` symbol", - SyntaxKind::Comma => "`,` symbol", - SyntaxKind::OpenParen => "`(` symbol", - SyntaxKind::CloseParen => "`)` symbol", - SyntaxKind::QuestionMark => "`?` symbol", - SyntaxKind::Exclamation => "`!` symbol", - SyntaxKind::Plus => "`+` symbol", - SyntaxKind::Minus => "`-` symbol", - SyntaxKind::LogicalOr => "`||` symbol", - SyntaxKind::LogicalAnd => "`&&` symbol", - SyntaxKind::Asterisk => "`*` symbol", - SyntaxKind::Exponentiation => "`**` symbol", - SyntaxKind::Slash => "`/` symbol", - SyntaxKind::Percent => "`%` symbol", - SyntaxKind::Equal => "`==` symbol", - SyntaxKind::NotEqual => "`!=` symbol", - SyntaxKind::LessEqual => "`<=` symbol", - SyntaxKind::GreaterEqual => "`>=` symbol", - SyntaxKind::Less => "`<` symbol", - SyntaxKind::Greater => "`>` symbol", - SyntaxKind::Dot => "`.` symbol", - SyntaxKind::LiteralStringText => "literal string text", - SyntaxKind::LiteralCommandText => "literal command text", - SyntaxKind::PlaceholderOpen => "placeholder open", - SyntaxKind::Abandoned => unreachable!(), - SyntaxKind::RootNode => "root node", - SyntaxKind::VersionStatementNode => "version statement", - SyntaxKind::ImportStatementNode => "import statement", - SyntaxKind::ImportAliasNode => "import alias", - SyntaxKind::StructDefinitionNode => "struct definition", - SyntaxKind::TaskDefinitionNode => "task definition", - SyntaxKind::WorkflowDefinitionNode => "workflow definition", - SyntaxKind::UnboundDeclNode => "declaration without assignment", - SyntaxKind::BoundDeclNode => "declaration with assignment", - SyntaxKind::InputSectionNode => "input section", - SyntaxKind::OutputSectionNode => "output section", - SyntaxKind::CommandSectionNode => "command section", - SyntaxKind::RequirementsSectionNode => "requirements section", - SyntaxKind::RequirementsItemNode => "requirements item", - SyntaxKind::TaskHintsSectionNode | SyntaxKind::WorkflowHintsSectionNode => { - "hints section" - } - SyntaxKind::TaskHintsItemNode | SyntaxKind::WorkflowHintsItemNode => "hints item", - SyntaxKind::WorkflowHintsObjectNode => "literal object", - SyntaxKind::WorkflowHintsObjectItemNode => "literal object item", - SyntaxKind::WorkflowHintsArrayNode => "literal array", - SyntaxKind::RuntimeSectionNode => "runtime section", - SyntaxKind::RuntimeItemNode => "runtime item", - SyntaxKind::PrimitiveTypeNode => "primitive type", - SyntaxKind::MapTypeNode => "map type", - SyntaxKind::ArrayTypeNode => "array type", - SyntaxKind::PairTypeNode => "pair type", - SyntaxKind::ObjectTypeNode => "object type", - SyntaxKind::TypeRefNode => "type reference", - SyntaxKind::MetadataSectionNode => "metadata section", - SyntaxKind::ParameterMetadataSectionNode => "parameter metadata section", - SyntaxKind::MetadataObjectItemNode => "metadata object item", - SyntaxKind::MetadataObjectNode => "metadata object", - SyntaxKind::MetadataArrayNode => "metadata array", - SyntaxKind::LiteralIntegerNode => "literal integer", - SyntaxKind::LiteralFloatNode => "literal float", - SyntaxKind::LiteralBooleanNode => "literal boolean", - SyntaxKind::LiteralNoneNode => "literal `None`", - SyntaxKind::LiteralNullNode => "literal null", - SyntaxKind::LiteralStringNode => "literal string", - SyntaxKind::LiteralPairNode => "literal pair", - SyntaxKind::LiteralArrayNode => "literal array", - SyntaxKind::LiteralMapNode => "literal map", - SyntaxKind::LiteralMapItemNode => "literal map item", - SyntaxKind::LiteralObjectNode => "literal object", - SyntaxKind::LiteralObjectItemNode => "literal object item", - SyntaxKind::LiteralStructNode => "literal struct", - SyntaxKind::LiteralStructItemNode => "literal struct item", - SyntaxKind::LiteralHintsNode => "literal hints", - SyntaxKind::LiteralHintsItemNode => "literal hints item", - SyntaxKind::LiteralInputNode => "literal input", - SyntaxKind::LiteralInputItemNode => "literal input item", - SyntaxKind::LiteralOutputNode => "literal output", - SyntaxKind::LiteralOutputItemNode => "literal output item", - SyntaxKind::ParenthesizedExprNode => "parenthesized expression", - SyntaxKind::NameRefNode => "name reference", - SyntaxKind::IfExprNode => "`if` expression", - SyntaxKind::LogicalNotExprNode => "logical not expression", - SyntaxKind::NegationExprNode => "negation expression", - SyntaxKind::LogicalOrExprNode => "logical OR expression", - SyntaxKind::LogicalAndExprNode => "logical AND expression", - SyntaxKind::EqualityExprNode => "equality expression", - SyntaxKind::InequalityExprNode => "inequality expression", - SyntaxKind::LessExprNode => "less than expression", - SyntaxKind::LessEqualExprNode => "less than or equal to expression", - SyntaxKind::GreaterExprNode => "greater than expression", - SyntaxKind::GreaterEqualExprNode => "greater than or equal to expression", - SyntaxKind::AdditionExprNode => "addition expression", - SyntaxKind::SubtractionExprNode => "subtraction expression", - SyntaxKind::MultiplicationExprNode => "multiplication expression", - SyntaxKind::DivisionExprNode => "division expression", - SyntaxKind::ModuloExprNode => "modulo expression", - SyntaxKind::ExponentiationExprNode => "exponentiation expression", - SyntaxKind::CallExprNode => "call expression", - SyntaxKind::IndexExprNode => "index expression", - SyntaxKind::AccessExprNode => "access expression", - SyntaxKind::PlaceholderNode => "placeholder", - SyntaxKind::PlaceholderSepOptionNode => "placeholder `sep` option", - SyntaxKind::PlaceholderDefaultOptionNode => "placeholder `default` option", - SyntaxKind::PlaceholderTrueFalseOptionNode => "placeholder `true`/`false` option", - SyntaxKind::ConditionalStatementNode => "conditional statement", - SyntaxKind::ScatterStatementNode => "scatter statement", - SyntaxKind::CallStatementNode => "call statement", - SyntaxKind::CallTargetNode => "call target", - SyntaxKind::CallAliasNode => "call alias", - SyntaxKind::CallAfterNode => "call `after` clause", - SyntaxKind::CallInputItemNode => "call input item", - SyntaxKind::MAX => unreachable!(), + Self::Unknown => unreachable!(), + Self::Unparsed => unreachable!(), + Self::Whitespace => "whitespace", + Self::Comment => "comment", + Self::Version => "version", + Self::Float => "float", + Self::Integer => "integer", + Self::Ident => "identifier", + Self::SingleQuote => "single quote", + Self::DoubleQuote => "double quote", + Self::OpenHeredoc => "open heredoc", + Self::CloseHeredoc => "close heredoc", + Self::ArrayTypeKeyword => "`Array` type keyword", + Self::BooleanTypeKeyword => "`Boolean` type keyword", + Self::FileTypeKeyword => "`File` type keyword", + Self::FloatTypeKeyword => "`Float` type keyword", + Self::IntTypeKeyword => "`Int` type keyword", + Self::MapTypeKeyword => "`Map` type keyword", + Self::ObjectTypeKeyword => "`Object` type keyword", + Self::PairTypeKeyword => "`Pair` type keyword", + Self::StringTypeKeyword => "`String` type keyword", + Self::AfterKeyword => "`after` keyword", + Self::AliasKeyword => "`alias` keyword", + Self::AsKeyword => "`as` keyword", + Self::CallKeyword => "`call` keyword", + Self::CommandKeyword => "`command` keyword", + Self::ElseKeyword => "`else` keyword", + Self::EnvKeyword => "`env` keyword", + Self::FalseKeyword => "`false` keyword", + Self::IfKeyword => "`if` keyword", + Self::InKeyword => "`in` keyword", + Self::ImportKeyword => "`import` keyword", + Self::InputKeyword => "`input` keyword", + Self::MetaKeyword => "`meta` keyword", + Self::NoneKeyword => "`None` keyword", + Self::NullKeyword => "`null` keyword", + Self::ObjectKeyword => "`object` keyword", + Self::OutputKeyword => "`output` keyword", + Self::ParameterMetaKeyword => "`parameter_meta` keyword", + Self::RuntimeKeyword => "`runtime` keyword", + Self::ScatterKeyword => "`scatter` keyword", + Self::StructKeyword => "`struct` keyword", + Self::TaskKeyword => "`task` keyword", + Self::ThenKeyword => "`then` keyword", + Self::TrueKeyword => "`true` keyword", + Self::VersionKeyword => "`version` keyword", + Self::WorkflowKeyword => "`workflow` keyword", + Self::DirectoryTypeKeyword => "`Directory` type keyword", + Self::HintsKeyword => "`hints` keyword", + Self::RequirementsKeyword => "`requirements` keyword", + Self::OpenBrace => "`{` symbol", + Self::CloseBrace => "`}` symbol", + Self::OpenBracket => "`[` symbol", + Self::CloseBracket => "`]` symbol", + Self::Assignment => "`=` symbol", + Self::Colon => "`:` symbol", + Self::Comma => "`,` symbol", + Self::OpenParen => "`(` symbol", + Self::CloseParen => "`)` symbol", + Self::QuestionMark => "`?` symbol", + Self::Exclamation => "`!` symbol", + Self::Plus => "`+` symbol", + Self::Minus => "`-` symbol", + Self::LogicalOr => "`||` symbol", + Self::LogicalAnd => "`&&` symbol", + Self::Asterisk => "`*` symbol", + Self::Exponentiation => "`**` symbol", + Self::Slash => "`/` symbol", + Self::Percent => "`%` symbol", + Self::Equal => "`==` symbol", + Self::NotEqual => "`!=` symbol", + Self::LessEqual => "`<=` symbol", + Self::GreaterEqual => "`>=` symbol", + Self::Less => "`<` symbol", + Self::Greater => "`>` symbol", + Self::Dot => "`.` symbol", + Self::LiteralStringText => "literal string text", + Self::LiteralCommandText => "literal command text", + Self::PlaceholderOpen => "placeholder open", + Self::Abandoned => unreachable!(), + Self::RootNode => "root node", + Self::VersionStatementNode => "version statement", + Self::ImportStatementNode => "import statement", + Self::ImportAliasNode => "import alias", + Self::StructDefinitionNode => "struct definition", + Self::TaskDefinitionNode => "task definition", + Self::WorkflowDefinitionNode => "workflow definition", + Self::UnboundDeclNode => "declaration without assignment", + Self::BoundDeclNode => "declaration with assignment", + Self::InputSectionNode => "input section", + Self::OutputSectionNode => "output section", + Self::CommandSectionNode => "command section", + Self::RequirementsSectionNode => "requirements section", + Self::RequirementsItemNode => "requirements item", + Self::TaskHintsSectionNode | Self::WorkflowHintsSectionNode => "hints section", + Self::TaskHintsItemNode | Self::WorkflowHintsItemNode => "hints item", + Self::WorkflowHintsObjectNode => "literal object", + Self::WorkflowHintsObjectItemNode => "literal object item", + Self::WorkflowHintsArrayNode => "literal array", + Self::RuntimeSectionNode => "runtime section", + Self::RuntimeItemNode => "runtime item", + Self::PrimitiveTypeNode => "primitive type", + Self::MapTypeNode => "map type", + Self::ArrayTypeNode => "array type", + Self::PairTypeNode => "pair type", + Self::ObjectTypeNode => "object type", + Self::TypeRefNode => "type reference", + Self::MetadataSectionNode => "metadata section", + Self::ParameterMetadataSectionNode => "parameter metadata section", + Self::MetadataObjectItemNode => "metadata object item", + Self::MetadataObjectNode => "metadata object", + Self::MetadataArrayNode => "metadata array", + Self::LiteralIntegerNode => "literal integer", + Self::LiteralFloatNode => "literal float", + Self::LiteralBooleanNode => "literal boolean", + Self::LiteralNoneNode => "literal `None`", + Self::LiteralNullNode => "literal null", + Self::LiteralStringNode => "literal string", + Self::LiteralPairNode => "literal pair", + Self::LiteralArrayNode => "literal array", + Self::LiteralMapNode => "literal map", + Self::LiteralMapItemNode => "literal map item", + Self::LiteralObjectNode => "literal object", + Self::LiteralObjectItemNode => "literal object item", + Self::LiteralStructNode => "literal struct", + Self::LiteralStructItemNode => "literal struct item", + Self::LiteralHintsNode => "literal hints", + Self::LiteralHintsItemNode => "literal hints item", + Self::LiteralInputNode => "literal input", + Self::LiteralInputItemNode => "literal input item", + Self::LiteralOutputNode => "literal output", + Self::LiteralOutputItemNode => "literal output item", + Self::ParenthesizedExprNode => "parenthesized expression", + Self::NameRefNode => "name reference", + Self::IfExprNode => "`if` expression", + Self::LogicalNotExprNode => "logical not expression", + Self::NegationExprNode => "negation expression", + Self::LogicalOrExprNode => "logical OR expression", + Self::LogicalAndExprNode => "logical AND expression", + Self::EqualityExprNode => "equality expression", + Self::InequalityExprNode => "inequality expression", + Self::LessExprNode => "less than expression", + Self::LessEqualExprNode => "less than or equal to expression", + Self::GreaterExprNode => "greater than expression", + Self::GreaterEqualExprNode => "greater than or equal to expression", + Self::AdditionExprNode => "addition expression", + Self::SubtractionExprNode => "subtraction expression", + Self::MultiplicationExprNode => "multiplication expression", + Self::DivisionExprNode => "division expression", + Self::ModuloExprNode => "modulo expression", + Self::ExponentiationExprNode => "exponentiation expression", + Self::CallExprNode => "call expression", + Self::IndexExprNode => "index expression", + Self::AccessExprNode => "access expression", + Self::PlaceholderNode => "placeholder", + Self::PlaceholderSepOptionNode => "placeholder `sep` option", + Self::PlaceholderDefaultOptionNode => "placeholder `default` option", + Self::PlaceholderTrueFalseOptionNode => "placeholder `true`/`false` option", + Self::ConditionalStatementNode => "conditional statement", + Self::ScatterStatementNode => "scatter statement", + Self::CallStatementNode => "call statement", + Self::CallTargetNode => "call target", + Self::CallAliasNode => "call alias", + Self::CallAfterNode => "call `after` clause", + Self::CallInputItemNode => "call input item", + Self::MAX => unreachable!(), } } /// Returns whether the [`SyntaxKind`] is trivia. pub fn is_trivia(&self) -> bool { - matches!(self, SyntaxKind::Whitespace | SyntaxKind::Comment) + matches!(self, Self::Whitespace | Self::Comment) + } +} + +/// Every [`SyntaxKind`] variant. +pub static ALL_SYNTAX_KIND: &[SyntaxKind] = SyntaxKind::VARIANTS; + +impl From for rowan::SyntaxKind { + fn from(kind: SyntaxKind) -> Self { + rowan::SyntaxKind(kind as u16) } } diff --git a/wdl-grammar/tests/parsing/env-vars/source.errors b/wdl-grammar/tests/parsing/env-vars/source.errors new file mode 100644 index 000000000..1c297f870 --- /dev/null +++ b/wdl-grammar/tests/parsing/env-vars/source.errors @@ -0,0 +1,24 @@ +error: expected type, but found `env` keyword + ┌─ tests/parsing/env-vars/source.wdl:12:9 + │ +12 │ env String d = "" + │ ^^^ unexpected `env` keyword + +error: expected type, but found `env` keyword + ┌─ tests/parsing/env-vars/source.wdl:19:9 + │ +19 │ env String f + │ ^^^ unexpected `env` keyword + +error: expected input section, output section, runtime section, metadata section, parameter metadata section, conditional statement, scatter statement, call statement, or private declaration, but found `env` keyword + ┌─ tests/parsing/env-vars/source.wdl:22:5 + │ +22 │ env String g = "" + │ ^^^ unexpected `env` keyword + +error: expected type, but found `env` keyword + ┌─ tests/parsing/env-vars/source.wdl:25:9 + │ +25 │ env String h = "" + │ ^^^ unexpected `env` keyword + diff --git a/wdl-grammar/tests/parsing/env-vars/source.tree b/wdl-grammar/tests/parsing/env-vars/source.tree new file mode 100644 index 000000000..1c1eda8e4 --- /dev/null +++ b/wdl-grammar/tests/parsing/env-vars/source.tree @@ -0,0 +1,135 @@ +RootNode@0..293 + VersionStatementNode@0..11 + VersionKeyword@0..7 "version" + Whitespace@7..8 " " + Version@8..11 "1.2" + Whitespace@11..13 "\n\n" + TaskDefinitionNode@13..150 + TaskKeyword@13..17 "task" + Whitespace@17..18 " " + Ident@18..21 "foo" + Whitespace@21..22 " " + OpenBrace@22..23 "{" + Whitespace@23..28 "\n " + InputSectionNode@28..79 + InputKeyword@28..33 "input" + Whitespace@33..34 " " + OpenBrace@34..35 "{" + Whitespace@35..44 "\n " + UnboundDeclNode@44..52 + PrimitiveTypeNode@44..50 + StringTypeKeyword@44..50 "String" + Whitespace@50..51 " " + Ident@51..52 "a" + Whitespace@52..61 "\n " + UnboundDeclNode@61..73 + EnvKeyword@61..64 "env" + Whitespace@64..65 " " + PrimitiveTypeNode@65..71 + StringTypeKeyword@65..71 "String" + Whitespace@71..72 " " + Ident@72..73 "b" + Whitespace@73..78 "\n " + CloseBrace@78..79 "}" + Whitespace@79..85 "\n\n " + BoundDeclNode@85..102 + EnvKeyword@85..88 "env" + Whitespace@88..89 " " + PrimitiveTypeNode@89..95 + StringTypeKeyword@89..95 "String" + Whitespace@95..96 " " + Ident@96..97 "c" + Whitespace@97..98 " " + Assignment@98..99 "=" + Whitespace@99..100 " " + LiteralStringNode@100..102 + DoubleQuote@100..101 "\"" + DoubleQuote@101..102 "\"" + Whitespace@102..108 "\n\n " + OutputSectionNode@108..148 + OutputKeyword@108..114 "output" + Whitespace@114..115 " " + OpenBrace@115..116 "{" + Whitespace@116..125 "\n " + EnvKeyword@125..128 "env" + Whitespace@128..129 " " + BoundDeclNode@129..142 + PrimitiveTypeNode@129..135 + StringTypeKeyword@129..135 "String" + Whitespace@135..136 " " + Ident@136..137 "d" + Whitespace@137..138 " " + Assignment@138..139 "=" + Whitespace@139..140 " " + LiteralStringNode@140..142 + DoubleQuote@140..141 "\"" + DoubleQuote@141..142 "\"" + Whitespace@142..147 "\n " + CloseBrace@147..148 "}" + Whitespace@148..149 "\n" + CloseBrace@149..150 "}" + Whitespace@150..152 "\n\n" + WorkflowDefinitionNode@152..293 + WorkflowKeyword@152..160 "workflow" + Whitespace@160..161 " " + Ident@161..164 "bar" + Whitespace@164..165 " " + OpenBrace@165..166 "{" + Whitespace@166..171 "\n " + InputSectionNode@171..222 + InputKeyword@171..176 "input" + Whitespace@176..177 " " + OpenBrace@177..178 "{" + Whitespace@178..187 "\n " + UnboundDeclNode@187..195 + PrimitiveTypeNode@187..193 + StringTypeKeyword@187..193 "String" + Whitespace@193..194 " " + Ident@194..195 "e" + Whitespace@195..204 "\n " + EnvKeyword@204..207 "env" + Whitespace@207..208 " " + UnboundDeclNode@208..216 + PrimitiveTypeNode@208..214 + StringTypeKeyword@208..214 "String" + Whitespace@214..215 " " + Ident@215..216 "f" + Whitespace@216..221 "\n " + CloseBrace@221..222 "}" + Whitespace@222..228 "\n\n " + EnvKeyword@228..231 "env" + Whitespace@231..232 " " + BoundDeclNode@232..245 + PrimitiveTypeNode@232..238 + StringTypeKeyword@232..238 "String" + Whitespace@238..239 " " + Ident@239..240 "g" + Whitespace@240..241 " " + Assignment@241..242 "=" + Whitespace@242..243 " " + LiteralStringNode@243..245 + DoubleQuote@243..244 "\"" + DoubleQuote@244..245 "\"" + Whitespace@245..251 "\n\n " + OutputSectionNode@251..291 + OutputKeyword@251..257 "output" + Whitespace@257..258 " " + OpenBrace@258..259 "{" + Whitespace@259..268 "\n " + EnvKeyword@268..271 "env" + Whitespace@271..272 " " + BoundDeclNode@272..285 + PrimitiveTypeNode@272..278 + StringTypeKeyword@272..278 "String" + Whitespace@278..279 " " + Ident@279..280 "h" + Whitespace@280..281 " " + Assignment@281..282 "=" + Whitespace@282..283 " " + LiteralStringNode@283..285 + DoubleQuote@283..284 "\"" + DoubleQuote@284..285 "\"" + Whitespace@285..290 "\n " + CloseBrace@290..291 "}" + Whitespace@291..292 "\n" + CloseBrace@292..293 "}" diff --git a/wdl-grammar/tests/parsing/env-vars/source.wdl b/wdl-grammar/tests/parsing/env-vars/source.wdl new file mode 100644 index 000000000..833f0cf6f --- /dev/null +++ b/wdl-grammar/tests/parsing/env-vars/source.wdl @@ -0,0 +1,27 @@ +version 1.2 + +task foo { + input { + String a + env String b + } + + env String c = "" + + output { + env String d = "" + } +} + +workflow bar { + input { + String e + env String f + } + + env String g = "" + + output { + env String h = "" + } +} \ No newline at end of file diff --git a/wdl-lint/src/rules/expression_spacing.rs b/wdl-lint/src/rules/expression_spacing.rs index cf32f3208..a814a5af7 100644 --- a/wdl-lint/src/rules/expression_spacing.rs +++ b/wdl-lint/src/rules/expression_spacing.rs @@ -2,6 +2,7 @@ use rowan::Direction; use wdl_ast::AstNode; +use wdl_ast::AstNodeExt; use wdl_ast::Diagnostic; use wdl_ast::Diagnostics; use wdl_ast::Document; @@ -239,7 +240,7 @@ impl Visitor for ExpressionSpacingRule { > 0 { state.exceptable_add( - prefix_whitespace(expr.syntax().text_range().to_span()), + prefix_whitespace(expr.span()), SyntaxElement::from(expr.syntax().clone()), &self.exceptable_nodes(), ); diff --git a/wdl-lint/src/rules/import_placement.rs b/wdl-lint/src/rules/import_placement.rs index 35806f3c6..07e09dc75 100644 --- a/wdl-lint/src/rules/import_placement.rs +++ b/wdl-lint/src/rules/import_placement.rs @@ -1,6 +1,7 @@ //! A lint rule for import placements. use wdl_ast::AstNode; +use wdl_ast::AstNodeExt; use wdl_ast::Diagnostic; use wdl_ast::Diagnostics; use wdl_ast::Document; @@ -8,7 +9,6 @@ use wdl_ast::Span; use wdl_ast::SupportedVersion; use wdl_ast::SyntaxElement; use wdl_ast::SyntaxKind; -use wdl_ast::ToSpan; use wdl_ast::VisitReason; use wdl_ast::Visitor; use wdl_ast::v1::ImportStatement; @@ -97,7 +97,7 @@ impl Visitor for ImportPlacementRule { if self.invalid { state.exceptable_add( - misplaced_import(stmt.syntax().text_range().to_span()), + misplaced_import(stmt.span()), SyntaxElement::from(stmt.syntax().clone()), &self.exceptable_nodes(), ); diff --git a/wdl-lint/src/rules/key_value_pairs.rs b/wdl-lint/src/rules/key_value_pairs.rs index 4027792c4..ad00862c8 100644 --- a/wdl-lint/src/rules/key_value_pairs.rs +++ b/wdl-lint/src/rules/key_value_pairs.rs @@ -1,6 +1,7 @@ //! A lint rule for key-value pairs to ensure each element is on a newline. use wdl_ast::AstNode; +use wdl_ast::AstNodeExt; use wdl_ast::Diagnostic; use wdl_ast::Diagnostics; use wdl_ast::Document; @@ -145,7 +146,7 @@ impl Visitor for KeyValuePairsRule { if !item.syntax().to_string().contains('\n') { state.exceptable_add( - all_on_one_line(item.syntax().text_range().to_span()), + all_on_one_line(item.span()), SyntaxElement::from(item.syntax().clone()), &self.exceptable_nodes(), ); @@ -176,7 +177,7 @@ impl Visitor for KeyValuePairsRule { let (next_newline, _newline_is_next) = find_next_newline(child.syntax()); if next_newline.is_none() { // No newline found, report missing - let s = child.syntax().text_range().to_span(); + let s = child.span(); let end = if let Some(next) = find_next_comma(child.syntax()).0 { next.text_range().end() } else { @@ -266,7 +267,7 @@ impl Visitor for KeyValuePairsRule { // If the array is all on one line, report that if !item.syntax().to_string().contains('\n') { state.exceptable_add( - all_on_one_line(item.syntax().text_range().to_span()), + all_on_one_line(item.span()), SyntaxElement::from(item.syntax().clone()), &self.exceptable_nodes(), ); @@ -297,7 +298,7 @@ impl Visitor for KeyValuePairsRule { let (next_newline, _newline_is_next) = find_next_newline(child.syntax()); if next_newline.is_none() { // No newline found, report missing - let s = child.syntax().text_range().to_span(); + let s = child.span(); let end = if let Some(next) = find_next_comma(child.syntax()).0 { next.text_range().end() } else { diff --git a/wdl-lint/src/rules/nonmatching_output.rs b/wdl-lint/src/rules/nonmatching_output.rs index 0d5530a48..00f4c8d26 100755 --- a/wdl-lint/src/rules/nonmatching_output.rs +++ b/wdl-lint/src/rules/nonmatching_output.rs @@ -2,6 +2,7 @@ use indexmap::IndexMap; use wdl_ast::AstNode; +use wdl_ast::AstNodeExt; use wdl_ast::AstToken; use wdl_ast::Diagnostic; use wdl_ast::Diagnostics; @@ -348,10 +349,8 @@ impl Visitor for NonmatchingOutputRule<'_> { decl: &wdl_ast::v1::BoundDecl, ) { if reason == VisitReason::Enter && self.in_output { - self.output_keys.insert( - decl.name().as_str().to_string(), - decl.name().syntax().text_range().to_span(), - ); + self.output_keys + .insert(decl.name().as_str().to_string(), decl.name().span()); } } @@ -374,13 +373,13 @@ impl Visitor for NonmatchingOutputRule<'_> { VisitReason::Enter => { if let Some(_meta_span) = self.current_meta_span { if item.name().as_str() == "outputs" { - self.current_meta_outputs_span = Some(item.syntax().text_range().to_span()); + self.current_meta_outputs_span = Some(item.span()); match item.value() { MetadataValue::Object(_) => {} _ => { state.exceptable_add( non_object_meta_outputs( - item.syntax().text_range().to_span(), + item.span(), self.name.as_deref().expect("should have a name"), self.ty.expect("should have a type"), ), @@ -390,7 +389,7 @@ impl Visitor for NonmatchingOutputRule<'_> { } } } else if let Some(meta_outputs_span) = self.current_meta_outputs_span { - let span = item.syntax().text_range().to_span(); + let span = item.span(); if span.start() > meta_outputs_span.start() && span.end() < meta_outputs_span.end() && self @@ -399,10 +398,8 @@ impl Visitor for NonmatchingOutputRule<'_> { .expect("should have seen `meta.outputs`") == "outputs" { - self.meta_outputs_keys.insert( - item.name().as_str().to_string(), - item.syntax().text_range().to_span(), - ); + self.meta_outputs_keys + .insert(item.name().as_str().to_string(), item.span()); } } }