wdl_ast/v1/
task.rs

1//! V1 AST representation for task definitions.
2
3use std::fmt;
4
5use rowan::NodeOrToken;
6
7use super::BoundDecl;
8use super::Decl;
9use super::Expr;
10use super::LiteralBoolean;
11use super::LiteralFloat;
12use super::LiteralInteger;
13use super::LiteralString;
14use super::OpenHeredoc;
15use super::Placeholder;
16use super::StructDefinition;
17use super::WorkflowDefinition;
18use crate::AstNode;
19use crate::AstToken;
20use crate::Ident;
21use crate::SyntaxKind;
22use crate::SyntaxNode;
23use crate::SyntaxToken;
24use crate::TreeNode;
25use crate::TreeToken;
26use crate::v1::display::write_input_section;
27use crate::v1::display::write_output_section;
28
29pub mod common;
30pub mod requirements;
31pub mod runtime;
32
33/// The set of all valid task fields and their descriptions for the implicit
34/// `task` variable.
35pub const TASK_FIELDS: &[(&str, &str)] = &[
36    (TASK_FIELD_NAME, "The task name."),
37    (
38        TASK_FIELD_ID,
39        "A String with the unique ID of the task. The execution engine may choose the format for \
40         this ID, but it is suggested to include at least the following information:\nThe task \
41         name\nThe task alias, if it differs from the task name\nThe index of the task instance, \
42         if it is within a scatter statement",
43    ),
44    (
45        TASK_FIELD_CONTAINER,
46        "The URI String of the container in which the task is executing, or None if the task is \
47         being executed in the host environment.",
48    ),
49    (
50        TASK_FIELD_CPU,
51        "The allocated number of cpus as a Float. Must be greater than 0.",
52    ),
53    (
54        TASK_FIELD_MEMORY,
55        "The allocated memory in bytes as an Int. Must be greater than 0.",
56    ),
57    (
58        TASK_FIELD_GPU,
59        "An Array[String] with one specification per allocated GPU. The specification is \
60         execution engine-specific. If no GPUs were allocated, then the value must be an empty \
61         array.",
62    ),
63    (
64        TASK_FIELD_FPGA,
65        "An Array[String] with one specification per allocated FPGA. The specification is \
66         execution engine-specific. If no FPGAs were allocated, then the value must be an empty \
67         array.",
68    ),
69    (
70        TASK_FIELD_DISKS,
71        "A Map[String, Int] with one entry for each disk mount point. The key is the mount point \
72         and the value is the initial amount of disk space allocated, in bytes. The execution \
73         engine must, at a minimum, provide one entry for each disk mount point requested, but \
74         may provide more. The amount of disk space available for a given mount point may \
75         increase during the lifetime of the task (e.g., autoscaling volumes provided by some \
76         cloud services).",
77    ),
78    (
79        TASK_FIELD_ATTEMPT,
80        "The current task attempt. The value must be 0 the first time the task is executed, and \
81         incremented by 1 each time the task is retried (if any).",
82    ),
83    (
84        TASK_FIELD_END_TIME,
85        "An Int? whose value is the time by which the task must be completed, as a Unix time \
86         stamp. A value of 0 means that the execution engine does not impose a time limit. A \
87         value of None means that the execution engine cannot determine whether the runtime of \
88         the task is limited. A positive value is a guarantee that the task will be preempted at \
89         the specified time, but is not a guarantee that the task won't be preempted earlier.",
90    ),
91    (
92        TASK_FIELD_RETURN_CODE,
93        "An Int? whose value is initially None and is set to the value of the command's return \
94         code. The value is only guaranteed to be defined in the output section.",
95    ),
96    (
97        TASK_FIELD_META,
98        "An Object containing a copy of the task's meta section, or the empty Object if there is \
99         no meta section or if it is empty.",
100    ),
101    (
102        TASK_FIELD_PARAMETER_META,
103        "An Object containing a copy of the task's parameter_meta section, or the empty Object if \
104         there is no parameter_meta section or if it is empty.",
105    ),
106    (
107        TASK_FIELD_EXT,
108        "An Object containing execution engine-specific attributes, or the empty Object if there \
109         aren't any. Members of ext should be considered optional. It is recommended to only \
110         access a member of ext using string interpolation to avoid an error if it is not defined.",
111    ),
112];
113
114/// The set of all valid runtime section keys and their descriptions.
115pub const RUNTIME_KEYS: &[(&str, &str)] = &[
116    (
117        TASK_REQUIREMENT_CONTAINER,
118        "Specifies the container image (e.g., Docker, Singularity) to use for the task.",
119    ),
120    (
121        TASK_REQUIREMENT_CPU,
122        "The number of CPU cores required for the task.",
123    ),
124    (
125        TASK_REQUIREMENT_MEMORY,
126        "The amount of memory required, specified as a string with units (e.g., '2 GiB').",
127    ),
128    (
129        TASK_REQUIREMENT_DISKS,
130        "Specifies the disk requirements for the task.",
131    ),
132    (TASK_REQUIREMENT_GPU, "Specifies GPU requirements."),
133];
134
135/// The set of all valid requirements section keys and their descriptions.
136pub const REQUIREMENTS_KEY: &[(&str, &str)] = &[
137    (
138        TASK_REQUIREMENT_CONTAINER,
139        "Specifies a list of allowed container images. Use `*` to allow any POSIX environment.",
140    ),
141    (
142        TASK_REQUIREMENT_CPU,
143        "The minimum number of CPU cores required.",
144    ),
145    (
146        TASK_REQUIREMENT_MEMORY,
147        "The minimum amount of memory required.",
148    ),
149    (TASK_REQUIREMENT_GPU, "The minimum GPU requirements."),
150    (TASK_REQUIREMENT_FPGA, "The minimum FPGA requirements."),
151    (TASK_REQUIREMENT_DISKS, "The minimum disk requirements."),
152    (
153        TASK_REQUIREMENT_MAX_RETRIES,
154        "The maximum number of times the task can be retried.",
155    ),
156    (
157        TASK_REQUIREMENT_RETURN_CODES,
158        "A list of acceptable return codes from the command.",
159    ),
160];
161
162/// The set of all valid task hints section keys and their descriptions.
163pub const TASK_HINT_KEYS: &[(&str, &str)] = &[
164    (
165        TASK_HINT_DISKS,
166        "A hint to the execution engine to mount disks with specific attributes. The value of \
167         this hint can be a String with a specification that applies to all mount points, or a \
168         Map with the key being the mount point and the value being a String with the \
169         specification for that mount point.",
170    ),
171    (
172        TASK_HINT_GPU,
173        "A hint to the execution engine to provision hardware accelerators with specific \
174         attributes. Accelerator specifications are left intentionally vague as they are \
175         primarily intended to be used in the context of a specific compute environment.",
176    ),
177    (
178        TASK_HINT_FPGA,
179        "A hint to the execution engine to provision hardware accelerators with specific \
180         attributes. Accelerator specifications are left intentionally vague as they are \
181         primarily intended to be used in the context of a specific compute environment.",
182    ),
183    (
184        TASK_HINT_INPUTS,
185        "Provides input-specific hints. Each key must refer to a parameter defined in the task's \
186         input section. A key may also used dotted notation to refer to a specific member of a \
187         struct input.",
188    ),
189    (
190        TASK_HINT_LOCALIZATION_OPTIONAL,
191        "A hint to the execution engine about whether the File inputs for this task need to be \
192         localized prior to executing the task. The value of this hint is a Boolean for which \
193         true indicates that the contents of the File inputs may be streamed on demand.",
194    ),
195    (
196        TASK_HINT_MAX_CPU,
197        "A hint to the execution engine that the task expects to use no more than the specified \
198         number of CPUs. The value of this hint has the same specification as requirements.cpu.",
199    ),
200    (
201        TASK_HINT_MAX_MEMORY,
202        "A hint to the execution engine that the task expects to use no more than the specified \
203         amount of memory. The value of this hint has the same specification as \
204         requirements.memory.",
205    ),
206    (
207        TASK_HINT_OUTPUTS,
208        "Provides output-specific hints. Each key must refer to a parameter defined in the task's \
209         output section. A key may also use dotted notation to refer to a specific member of a \
210         struct output.",
211    ),
212    (
213        TASK_HINT_SHORT_TASK,
214        "A hint to the execution engine about the expected duration of this task. The value of \
215         this hint is a Boolean for which true indicates that that this task is not expected to \
216         take long to execute, which the execution engine can interpret as permission to optimize \
217         the execution of the task.",
218    ),
219];
220
221/// The name of the `name` task variable field.
222pub const TASK_FIELD_NAME: &str = "name";
223/// The name of the `id` task variable field.
224pub const TASK_FIELD_ID: &str = "id";
225/// The name of the `container` task variable field.
226pub const TASK_FIELD_CONTAINER: &str = "container";
227/// The name of the `cpu` task variable field.
228pub const TASK_FIELD_CPU: &str = "cpu";
229/// The name of the `memory` task variable field.
230pub const TASK_FIELD_MEMORY: &str = "memory";
231/// The name of the `attempt` task variable field.
232pub const TASK_FIELD_ATTEMPT: &str = "attempt";
233/// The name of the `gpu` task variable field.
234pub const TASK_FIELD_GPU: &str = "gpu";
235/// The name of the `fpga` task variable field.
236pub const TASK_FIELD_FPGA: &str = "fpga";
237/// The name of the `disks` task variable field.
238pub const TASK_FIELD_DISKS: &str = "disks";
239/// The name of the `end_time` task variable field.
240pub const TASK_FIELD_END_TIME: &str = "end_time";
241/// The name of the `return_code` task variable field.
242pub const TASK_FIELD_RETURN_CODE: &str = "return_code";
243/// The name of the `meta` task variable field.
244pub const TASK_FIELD_META: &str = "meta";
245/// The name of the `parameter_meta` task variable field.
246pub const TASK_FIELD_PARAMETER_META: &str = "parameter_meta";
247/// The name of the `ext` task variable field.
248pub const TASK_FIELD_EXT: &str = "ext";
249
250/// The name of the `container` task requirement.
251pub const TASK_REQUIREMENT_CONTAINER: &str = "container";
252/// The alias of the `container` task requirement (i.e. `docker`).
253pub const TASK_REQUIREMENT_CONTAINER_ALIAS: &str = "docker";
254/// The name of the `cpu` task requirement.
255pub const TASK_REQUIREMENT_CPU: &str = "cpu";
256/// The name of the `disks` task requirement.
257pub const TASK_REQUIREMENT_DISKS: &str = "disks";
258/// The name of the `gpu` task requirement.
259pub const TASK_REQUIREMENT_GPU: &str = "gpu";
260/// The name of the `fpga` task requirement.
261pub const TASK_REQUIREMENT_FPGA: &str = "fpga";
262/// The name of the `max_retries` task requirement.
263pub const TASK_REQUIREMENT_MAX_RETRIES: &str = "max_retries";
264/// The alias of the `max_retries` task requirement (i.e. `maxRetries``).
265pub const TASK_REQUIREMENT_MAX_RETRIES_ALIAS: &str = "maxRetries";
266/// The name of the `memory` task requirement.
267pub const TASK_REQUIREMENT_MEMORY: &str = "memory";
268/// The name of the `return_codes` task requirement.
269pub const TASK_REQUIREMENT_RETURN_CODES: &str = "return_codes";
270/// The alias of the `return_codes` task requirement (i.e. `returnCodes`).
271pub const TASK_REQUIREMENT_RETURN_CODES_ALIAS: &str = "returnCodes";
272
273/// The name of the `disks` task hint.
274pub const TASK_HINT_DISKS: &str = "disks";
275/// The name of the `gpu` task hint.
276pub const TASK_HINT_GPU: &str = "gpu";
277/// The name of the `fpga` task hint.
278pub const TASK_HINT_FPGA: &str = "fpga";
279/// The name of the `inputs` task hint.
280pub const TASK_HINT_INPUTS: &str = "inputs";
281/// The name of the `localization_optional` task hint.
282pub const TASK_HINT_LOCALIZATION_OPTIONAL: &str = "localization_optional";
283/// The alias of the `localization_optional` task hint (i.e.
284/// `localizationOptional`).
285pub const TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS: &str = "localizationOptional";
286/// The name of the `max_cpu` task hint.
287pub const TASK_HINT_MAX_CPU: &str = "max_cpu";
288/// The alias of the `max_cpu` task hint (i.e. `maxCpu`).
289pub const TASK_HINT_MAX_CPU_ALIAS: &str = "maxCpu";
290/// The name of the `max_memory` task hint.
291pub const TASK_HINT_MAX_MEMORY: &str = "max_memory";
292/// The alias of the `max_memory` task hin (e.g. `maxMemory`).
293pub const TASK_HINT_MAX_MEMORY_ALIAS: &str = "maxMemory";
294/// The name of the `outputs` task hint.
295pub const TASK_HINT_OUTPUTS: &str = "outputs";
296/// The name of the `short_task` task hint.
297pub const TASK_HINT_SHORT_TASK: &str = "short_task";
298/// The alias of the `short_task` task hint (e.g. `shortTask`).
299pub const TASK_HINT_SHORT_TASK_ALIAS: &str = "shortTask";
300
301/// Unescapes command text.
302fn unescape_command_text(s: &str, heredoc: bool, buffer: &mut String) {
303    let mut chars = s.chars().peekable();
304    while let Some(c) = chars.next() {
305        match c {
306            '\\' => match chars.peek() {
307                Some('\\') | Some('~') => {
308                    buffer.push(chars.next().unwrap());
309                }
310                Some('>') if heredoc => {
311                    buffer.push(chars.next().unwrap());
312                }
313                Some('$') | Some('}') if !heredoc => {
314                    buffer.push(chars.next().unwrap());
315                }
316                _ => {
317                    buffer.push('\\');
318                }
319            },
320            _ => {
321                buffer.push(c);
322            }
323        }
324    }
325}
326
327/// Represents a task definition.
328#[derive(Clone, Debug, PartialEq, Eq)]
329pub struct TaskDefinition<N: TreeNode = SyntaxNode>(N);
330
331impl<N: TreeNode> TaskDefinition<N> {
332    /// Gets the name of the task.
333    pub fn name(&self) -> Ident<N::Token> {
334        self.token().expect("task should have a name")
335    }
336
337    /// Gets the items of the task.
338    pub fn items(&self) -> impl Iterator<Item = TaskItem<N>> + use<'_, N> {
339        TaskItem::children(&self.0)
340    }
341
342    /// Gets the input section of the task.
343    pub fn input(&self) -> Option<InputSection<N>> {
344        self.child()
345    }
346
347    /// Gets the output section of the task.
348    pub fn output(&self) -> Option<OutputSection<N>> {
349        self.child()
350    }
351
352    /// Gets the command section of the task.
353    pub fn command(&self) -> Option<CommandSection<N>> {
354        self.child()
355    }
356
357    /// Gets the requirements sections of the task.
358    pub fn requirements(&self) -> Option<RequirementsSection<N>> {
359        self.child()
360    }
361
362    /// Gets the hints section of the task.
363    pub fn hints(&self) -> Option<TaskHintsSection<N>> {
364        self.child()
365    }
366
367    /// Gets the runtime section of the task.
368    pub fn runtime(&self) -> Option<RuntimeSection<N>> {
369        self.child()
370    }
371
372    /// Gets the metadata section of the task.
373    pub fn metadata(&self) -> Option<MetadataSection<N>> {
374        self.child()
375    }
376
377    /// Gets the parameter section of the task.
378    pub fn parameter_metadata(&self) -> Option<ParameterMetadataSection<N>> {
379        self.child()
380    }
381
382    /// Gets the private declarations of the task.
383    pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
384        self.children()
385    }
386
387    /// Writes a Markdown formatted description of the task.
388    pub fn markdown_description(&self, f: &mut impl fmt::Write) -> fmt::Result {
389        writeln!(f, "```wdl\ntask {}\n```\n---", self.name().text())?;
390
391        if let Some(meta) = self.metadata()
392            && let Some(desc) = meta.items().find(|i| i.name().text() == "description")
393            && let MetadataValue::String(s) = desc.value()
394            && let Some(text) = s.text()
395        {
396            writeln!(f, "{}\n", text.text())?;
397        }
398
399        write_input_section(f, self.input().as_ref(), self.parameter_metadata().as_ref())?;
400        write_output_section(
401            f,
402            self.output().as_ref(),
403            self.parameter_metadata().as_ref(),
404        )?;
405
406        Ok(())
407    }
408}
409
410impl<N: TreeNode> AstNode<N> for TaskDefinition<N> {
411    fn can_cast(kind: SyntaxKind) -> bool {
412        kind == SyntaxKind::TaskDefinitionNode
413    }
414
415    fn cast(inner: N) -> Option<Self> {
416        match inner.kind() {
417            SyntaxKind::TaskDefinitionNode => Some(Self(inner)),
418            _ => None,
419        }
420    }
421
422    fn inner(&self) -> &N {
423        &self.0
424    }
425}
426
427/// Represents an item in a task definition.
428#[derive(Clone, Debug, PartialEq, Eq)]
429pub enum TaskItem<N: TreeNode = SyntaxNode> {
430    /// The item is an input section.
431    Input(InputSection<N>),
432    /// The item is an output section.
433    Output(OutputSection<N>),
434    /// The item is a command section.
435    Command(CommandSection<N>),
436    /// The item is a requirements section.
437    Requirements(RequirementsSection<N>),
438    /// The item is a task hints section.
439    Hints(TaskHintsSection<N>),
440    /// The item is a runtime section.
441    Runtime(RuntimeSection<N>),
442    /// The item is a metadata section.
443    Metadata(MetadataSection<N>),
444    /// The item is a parameter meta section.
445    ParameterMetadata(ParameterMetadataSection<N>),
446    /// The item is a private bound declaration.
447    Declaration(BoundDecl<N>),
448}
449
450impl<N: TreeNode> TaskItem<N> {
451    /// Returns whether or not the given syntax kind can be cast to
452    /// [`TaskItem`].
453    pub fn can_cast(kind: SyntaxKind) -> bool {
454        matches!(
455            kind,
456            SyntaxKind::InputSectionNode
457                | SyntaxKind::OutputSectionNode
458                | SyntaxKind::CommandSectionNode
459                | SyntaxKind::RequirementsSectionNode
460                | SyntaxKind::TaskHintsSectionNode
461                | SyntaxKind::RuntimeSectionNode
462                | SyntaxKind::MetadataSectionNode
463                | SyntaxKind::ParameterMetadataSectionNode
464                | SyntaxKind::BoundDeclNode
465        )
466    }
467
468    /// Casts the given node to [`TaskItem`].
469    ///
470    /// Returns `None` if the node cannot be cast.
471    pub fn cast(inner: N) -> Option<Self> {
472        match inner.kind() {
473            SyntaxKind::InputSectionNode => Some(Self::Input(
474                InputSection::cast(inner).expect("input section to cast"),
475            )),
476            SyntaxKind::OutputSectionNode => Some(Self::Output(
477                OutputSection::cast(inner).expect("output section to cast"),
478            )),
479            SyntaxKind::CommandSectionNode => Some(Self::Command(
480                CommandSection::cast(inner).expect("command section to cast"),
481            )),
482            SyntaxKind::RequirementsSectionNode => Some(Self::Requirements(
483                RequirementsSection::cast(inner).expect("requirements section to cast"),
484            )),
485            SyntaxKind::RuntimeSectionNode => Some(Self::Runtime(
486                RuntimeSection::cast(inner).expect("runtime section to cast"),
487            )),
488            SyntaxKind::MetadataSectionNode => Some(Self::Metadata(
489                MetadataSection::cast(inner).expect("metadata section to cast"),
490            )),
491            SyntaxKind::ParameterMetadataSectionNode => Some(Self::ParameterMetadata(
492                ParameterMetadataSection::cast(inner).expect("parameter metadata section to cast"),
493            )),
494            SyntaxKind::TaskHintsSectionNode => Some(Self::Hints(
495                TaskHintsSection::cast(inner).expect("task hints section to cast"),
496            )),
497            SyntaxKind::BoundDeclNode => Some(Self::Declaration(
498                BoundDecl::cast(inner).expect("bound decl to cast"),
499            )),
500            _ => None,
501        }
502    }
503
504    /// Gets a reference to the inner node.
505    pub fn inner(&self) -> &N {
506        match self {
507            Self::Input(element) => element.inner(),
508            Self::Output(element) => element.inner(),
509            Self::Command(element) => element.inner(),
510            Self::Requirements(element) => element.inner(),
511            Self::Hints(element) => element.inner(),
512            Self::Runtime(element) => element.inner(),
513            Self::Metadata(element) => element.inner(),
514            Self::ParameterMetadata(element) => element.inner(),
515            Self::Declaration(element) => element.inner(),
516        }
517    }
518
519    /// Attempts to get a reference to the inner [`InputSection`].
520    ///
521    /// * If `self` is a [`TaskItem::Input`], then a reference to the inner
522    ///   [`InputSection`] is returned wrapped in [`Some`].
523    /// * Else, [`None`] is returned.
524    pub fn as_input_section(&self) -> Option<&InputSection<N>> {
525        match self {
526            Self::Input(s) => Some(s),
527            _ => None,
528        }
529    }
530
531    /// Consumes `self` and attempts to return the inner [`InputSection`].
532    ///
533    /// * If `self` is a [`TaskItem::Input`], then the inner [`InputSection`] is
534    ///   returned wrapped in [`Some`].
535    /// * Else, [`None`] is returned.
536    pub fn into_input_section(self) -> Option<InputSection<N>> {
537        match self {
538            Self::Input(s) => Some(s),
539            _ => None,
540        }
541    }
542
543    /// Attempts to get a reference to the inner [`OutputSection`].
544    ///
545    /// * If `self` is a [`TaskItem::Output`], then a reference to the inner
546    ///   [`OutputSection`] is returned wrapped in [`Some`].
547    /// * Else, [`None`] is returned.
548    pub fn as_output_section(&self) -> Option<&OutputSection<N>> {
549        match self {
550            Self::Output(s) => Some(s),
551            _ => None,
552        }
553    }
554
555    /// Consumes `self` and attempts to return the inner [`OutputSection`].
556    ///
557    /// * If `self` is a [`TaskItem::Output`], then the inner [`OutputSection`]
558    ///   is returned wrapped in [`Some`].
559    /// * Else, [`None`] is returned.
560    pub fn into_output_section(self) -> Option<OutputSection<N>> {
561        match self {
562            Self::Output(s) => Some(s),
563            _ => None,
564        }
565    }
566
567    /// Attempts to get a reference to the inner [`CommandSection`].
568    ///
569    /// * If `self` is a [`TaskItem::Command`], then a reference to the inner
570    ///   [`CommandSection`] is returned wrapped in [`Some`].
571    /// * Else, [`None`] is returned.
572    pub fn as_command_section(&self) -> Option<&CommandSection<N>> {
573        match self {
574            Self::Command(s) => Some(s),
575            _ => None,
576        }
577    }
578
579    /// Consumes `self` and attempts to return the inner [`CommandSection`].
580    ///
581    /// * If `self` is a [`TaskItem::Command`], then the inner
582    ///   [`CommandSection`] is returned wrapped in [`Some`].
583    /// * Else, [`None`] is returned.
584    pub fn into_command_section(self) -> Option<CommandSection<N>> {
585        match self {
586            Self::Command(s) => Some(s),
587            _ => None,
588        }
589    }
590
591    /// Attempts to get a reference to the inner [`RequirementsSection`].
592    ///
593    /// * If `self` is a [`TaskItem::Requirements`], then a reference to the
594    ///   inner [`RequirementsSection`] is returned wrapped in [`Some`].
595    /// * Else, [`None`] is returned.
596    pub fn as_requirements_section(&self) -> Option<&RequirementsSection<N>> {
597        match self {
598            Self::Requirements(s) => Some(s),
599            _ => None,
600        }
601    }
602
603    /// Consumes `self` and attempts to return the inner
604    /// [`RequirementsSection`].
605    ///
606    /// * If `self` is a [`TaskItem::Requirements`], then the inner
607    ///   [`RequirementsSection`] is returned wrapped in [`Some`].
608    /// * Else, [`None`] is returned.
609    pub fn into_requirements_section(self) -> Option<RequirementsSection<N>> {
610        match self {
611            Self::Requirements(s) => Some(s),
612            _ => None,
613        }
614    }
615
616    /// Attempts to get a reference to the inner [`TaskHintsSection`].
617    ///
618    /// * If `self` is a [`TaskItem::Hints`], then a reference to the inner
619    ///   [`TaskHintsSection`] is returned wrapped in [`Some`].
620    /// * Else, [`None`] is returned.
621    pub fn as_hints_section(&self) -> Option<&TaskHintsSection<N>> {
622        match self {
623            Self::Hints(s) => Some(s),
624            _ => None,
625        }
626    }
627
628    /// Consumes `self` and attempts to return the inner [`TaskHintsSection`].
629    ///
630    /// * If `self` is a [`TaskItem::Hints`], then the inner
631    ///   [`TaskHintsSection`] is returned wrapped in [`Some`].
632    /// * Else, [`None`] is returned.
633    pub fn into_hints_section(self) -> Option<TaskHintsSection<N>> {
634        match self {
635            Self::Hints(s) => Some(s),
636            _ => None,
637        }
638    }
639
640    /// Attempts to get a reference to the inner [`RuntimeSection`].
641    ///
642    /// * If `self` is a [`TaskItem::Runtime`], then a reference to the inner
643    ///   [`RuntimeSection`] is returned wrapped in [`Some`].
644    /// * Else, [`None`] is returned.
645    pub fn as_runtime_section(&self) -> Option<&RuntimeSection<N>> {
646        match self {
647            Self::Runtime(s) => Some(s),
648            _ => None,
649        }
650    }
651
652    /// Consumes `self` and attempts to return the inner [`RuntimeSection`].
653    ///
654    /// * If `self` is a [`TaskItem::Runtime`], then the inner
655    ///   [`RuntimeSection`] is returned wrapped in [`Some`].
656    /// * Else, [`None`] is returned.
657    pub fn into_runtime_section(self) -> Option<RuntimeSection<N>> {
658        match self {
659            Self::Runtime(s) => Some(s),
660            _ => None,
661        }
662    }
663
664    /// Attempts to get a reference to the inner [`MetadataSection`].
665    ///
666    /// * If `self` is a [`TaskItem::Metadata`], then a reference to the inner
667    ///   [`MetadataSection`] is returned wrapped in [`Some`].
668    /// * Else, [`None`] is returned.
669    pub fn as_metadata_section(&self) -> Option<&MetadataSection<N>> {
670        match self {
671            Self::Metadata(s) => Some(s),
672            _ => None,
673        }
674    }
675
676    /// Consumes `self` and attempts to return the inner [`MetadataSection`].
677    ///
678    /// * If `self` is a [`TaskItem::Metadata`], then the inner
679    ///   [`MetadataSection`] is returned wrapped in [`Some`].
680    /// * Else, [`None`] is returned.
681    pub fn into_metadata_section(self) -> Option<MetadataSection<N>> {
682        match self {
683            Self::Metadata(s) => Some(s),
684            _ => None,
685        }
686    }
687
688    /// Attempts to get a reference to the inner [`ParameterMetadataSection`].
689    ///
690    /// * If `self` is a [`TaskItem::ParameterMetadata`], then a reference to
691    ///   the inner [`ParameterMetadataSection`] is returned wrapped in
692    ///   [`Some`].
693    /// * Else, [`None`] is returned.
694    pub fn as_parameter_metadata_section(&self) -> Option<&ParameterMetadataSection<N>> {
695        match self {
696            Self::ParameterMetadata(s) => Some(s),
697            _ => None,
698        }
699    }
700
701    /// Consumes `self` and attempts to return the inner
702    /// [`ParameterMetadataSection`].
703    ///
704    /// * If `self` is a [`TaskItem::ParameterMetadata`], then the inner
705    ///   [`ParameterMetadataSection`] is returned wrapped in [`Some`].
706    /// * Else, [`None`] is returned.
707    pub fn into_parameter_metadata_section(self) -> Option<ParameterMetadataSection<N>> {
708        match self {
709            Self::ParameterMetadata(s) => Some(s),
710            _ => None,
711        }
712    }
713
714    /// Attempts to get a reference to the inner [`BoundDecl`].
715    ///
716    /// * If `self` is a [`TaskItem::Declaration`], then a reference to the
717    ///   inner [`BoundDecl`] is returned wrapped in [`Some`].
718    /// * Else, [`None`] is returned.
719    pub fn as_declaration(&self) -> Option<&BoundDecl<N>> {
720        match self {
721            Self::Declaration(d) => Some(d),
722            _ => None,
723        }
724    }
725
726    /// Consumes `self` and attempts to return the inner [`BoundDecl`].
727    ///
728    /// * If `self` is a [`TaskItem::Declaration`], then the inner [`BoundDecl`]
729    ///   is returned wrapped in [`Some`].
730    /// * Else, [`None`] is returned.
731    pub fn into_declaration(self) -> Option<BoundDecl<N>> {
732        match self {
733            Self::Declaration(d) => Some(d),
734            _ => None,
735        }
736    }
737
738    /// Finds the first child that can be cast to a [`TaskItem`].
739    pub fn child(node: &N) -> Option<Self> {
740        node.children().find_map(Self::cast)
741    }
742
743    /// Finds all children that can be cast to a [`TaskItem`].
744    pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
745        node.children().filter_map(Self::cast)
746    }
747}
748
749/// Represents the parent of a section.
750#[derive(Clone, Debug, PartialEq, Eq)]
751pub enum SectionParent<N: TreeNode = SyntaxNode> {
752    /// The parent is a task.
753    Task(TaskDefinition<N>),
754    /// The parent is a workflow.
755    Workflow(WorkflowDefinition<N>),
756    /// The parent is a struct.
757    Struct(StructDefinition<N>),
758}
759
760impl<N: TreeNode> SectionParent<N> {
761    /// Returns whether or not the given syntax kind can be cast to
762    /// [`SectionParent`].
763    pub fn can_cast(kind: SyntaxKind) -> bool {
764        matches!(
765            kind,
766            SyntaxKind::TaskDefinitionNode
767                | SyntaxKind::WorkflowDefinitionNode
768                | SyntaxKind::StructDefinitionNode
769        )
770    }
771
772    /// Casts the given node to [`SectionParent`].
773    ///
774    /// Returns `None` if the node cannot be cast.
775    pub fn cast(inner: N) -> Option<Self> {
776        match inner.kind() {
777            SyntaxKind::TaskDefinitionNode => Some(Self::Task(
778                TaskDefinition::cast(inner).expect("task definition to cast"),
779            )),
780            SyntaxKind::WorkflowDefinitionNode => Some(Self::Workflow(
781                WorkflowDefinition::cast(inner).expect("workflow definition to cast"),
782            )),
783            SyntaxKind::StructDefinitionNode => Some(Self::Struct(
784                StructDefinition::cast(inner).expect("struct definition to cast"),
785            )),
786            _ => None,
787        }
788    }
789
790    /// Gets a reference to the inner node.
791    pub fn inner(&self) -> &N {
792        match self {
793            Self::Task(element) => element.inner(),
794            Self::Workflow(element) => element.inner(),
795            Self::Struct(element) => element.inner(),
796        }
797    }
798
799    /// Gets the name of the section parent.
800    pub fn name(&self) -> Ident<N::Token> {
801        match self {
802            Self::Task(t) => t.name(),
803            Self::Workflow(w) => w.name(),
804            Self::Struct(s) => s.name(),
805        }
806    }
807
808    /// Attempts to get a reference to the inner [`TaskDefinition`].
809    ///
810    /// * If `self` is a [`SectionParent::Task`], then a reference to the inner
811    ///   [`TaskDefinition`] is returned wrapped in [`Some`].
812    /// * Else, [`None`] is returned.
813    pub fn as_task(&self) -> Option<&TaskDefinition<N>> {
814        match self {
815            Self::Task(task) => Some(task),
816            _ => None,
817        }
818    }
819
820    /// Consumes `self` and attempts to return the inner [`TaskDefinition`].
821    ///
822    /// * If `self` is a [`SectionParent::Task`], then the inner
823    ///   [`TaskDefinition`] is returned wrapped in [`Some`].
824    /// * Else, [`None`] is returned.
825    pub fn into_task(self) -> Option<TaskDefinition<N>> {
826        match self {
827            Self::Task(task) => Some(task),
828            _ => None,
829        }
830    }
831
832    /// Unwraps to a task definition.
833    ///
834    /// # Panics
835    ///
836    /// Panics if it is not a task definition.
837    pub fn unwrap_task(self) -> TaskDefinition<N> {
838        match self {
839            Self::Task(task) => task,
840            _ => panic!("not a task definition"),
841        }
842    }
843
844    /// Attempts to get a reference to the inner [`WorkflowDefinition`].
845    ///
846    /// * If `self` is a [`SectionParent::Workflow`], then a reference to the
847    ///   inner [`WorkflowDefinition`] is returned wrapped in [`Some`].
848    /// * Else, [`None`] is returned.
849    pub fn as_workflow(&self) -> Option<&WorkflowDefinition<N>> {
850        match self {
851            Self::Workflow(workflow) => Some(workflow),
852            _ => None,
853        }
854    }
855
856    /// Consumes `self` and attempts to return the inner [`WorkflowDefinition`].
857    ///
858    /// * If `self` is a [`SectionParent::Workflow`], then the inner
859    ///   [`WorkflowDefinition`] is returned wrapped in [`Some`].
860    /// * Else, [`None`] is returned.
861    pub fn into_workflow(self) -> Option<WorkflowDefinition<N>> {
862        match self {
863            Self::Workflow(workflow) => Some(workflow),
864            _ => None,
865        }
866    }
867
868    /// Unwraps to a workflow definition.
869    ///
870    /// # Panics
871    ///
872    /// Panics if it is not a workflow definition.
873    pub fn unwrap_workflow(self) -> WorkflowDefinition<N> {
874        match self {
875            Self::Workflow(workflow) => workflow,
876            _ => panic!("not a workflow definition"),
877        }
878    }
879
880    /// Attempts to get a reference to the inner [`StructDefinition`].
881    ///
882    /// * If `self` is a [`SectionParent::Struct`], then a reference to the
883    ///   inner [`StructDefinition`] is returned wrapped in [`Some`].
884    /// * Else, [`None`] is returned.
885    pub fn as_struct(&self) -> Option<&StructDefinition<N>> {
886        match self {
887            Self::Struct(r#struct) => Some(r#struct),
888            _ => None,
889        }
890    }
891
892    /// Consumes `self` and attempts to return the inner [`StructDefinition`].
893    ///
894    /// * If `self` is a [`SectionParent::Struct`], then the inner
895    ///   [`StructDefinition`] is returned wrapped in [`Some`].
896    /// * Else, [`None`] is returned.
897    pub fn into_struct(self) -> Option<StructDefinition<N>> {
898        match self {
899            Self::Struct(r#struct) => Some(r#struct),
900            _ => None,
901        }
902    }
903
904    /// Unwraps to a struct definition.
905    ///
906    /// # Panics
907    ///
908    /// Panics if it is not a struct definition.
909    pub fn unwrap_struct(self) -> StructDefinition<N> {
910        match self {
911            Self::Struct(def) => def,
912            _ => panic!("not a struct definition"),
913        }
914    }
915
916    /// Finds the first child that can be cast to a [`SectionParent`].
917    pub fn child(node: &N) -> Option<Self> {
918        node.children().find_map(Self::cast)
919    }
920
921    /// Finds all children that can be cast to a [`SectionParent`].
922    pub fn children(node: &N) -> impl Iterator<Item = Self> + use<'_, N> {
923        node.children().filter_map(Self::cast)
924    }
925}
926
927/// Represents an input section in a task or workflow definition.
928#[derive(Clone, Debug, PartialEq, Eq)]
929pub struct InputSection<N: TreeNode = SyntaxNode>(N);
930
931impl<N: TreeNode> InputSection<N> {
932    /// Gets the declarations of the input section.
933    pub fn declarations(&self) -> impl Iterator<Item = Decl<N>> + use<'_, N> {
934        Decl::children(&self.0)
935    }
936
937    /// Gets the parent of the input section.
938    pub fn parent(&self) -> SectionParent<N> {
939        SectionParent::cast(self.0.parent().expect("should have a parent"))
940            .expect("parent should cast")
941    }
942}
943
944impl<N: TreeNode> AstNode<N> for InputSection<N> {
945    fn can_cast(kind: SyntaxKind) -> bool {
946        kind == SyntaxKind::InputSectionNode
947    }
948
949    fn cast(inner: N) -> Option<Self> {
950        match inner.kind() {
951            SyntaxKind::InputSectionNode => Some(Self(inner)),
952            _ => None,
953        }
954    }
955
956    fn inner(&self) -> &N {
957        &self.0
958    }
959}
960
961/// Represents an output section in a task or workflow definition.
962#[derive(Clone, Debug, PartialEq, Eq)]
963pub struct OutputSection<N: TreeNode = SyntaxNode>(N);
964
965impl<N: TreeNode> OutputSection<N> {
966    /// Gets the declarations of the output section.
967    pub fn declarations(&self) -> impl Iterator<Item = BoundDecl<N>> + use<'_, N> {
968        self.children()
969    }
970
971    /// Gets the parent of the output section.
972    pub fn parent(&self) -> SectionParent<N> {
973        SectionParent::cast(self.0.parent().expect("should have a parent"))
974            .expect("parent should cast")
975    }
976}
977
978impl<N: TreeNode> AstNode<N> for OutputSection<N> {
979    fn can_cast(kind: SyntaxKind) -> bool {
980        kind == SyntaxKind::OutputSectionNode
981    }
982
983    fn cast(inner: N) -> Option<Self> {
984        match inner.kind() {
985            SyntaxKind::OutputSectionNode => Some(Self(inner)),
986            _ => None,
987        }
988    }
989
990    fn inner(&self) -> &N {
991        &self.0
992    }
993}
994
995/// A command part stripped of leading whitespace.
996///
997/// Placeholders are not changed and are copied as is.
998#[derive(Clone, Debug, PartialEq, Eq)]
999pub enum StrippedCommandPart<N: TreeNode = SyntaxNode> {
1000    /// A text part.
1001    Text(String),
1002    /// A placeholder part.
1003    Placeholder(Placeholder<N>),
1004}
1005
1006/// Represents a command section in a task definition.
1007#[derive(Clone, Debug, PartialEq, Eq)]
1008pub struct CommandSection<N: TreeNode = SyntaxNode>(N);
1009
1010impl<N: TreeNode> CommandSection<N> {
1011    /// Gets whether or not the command section is a heredoc command.
1012    pub fn is_heredoc(&self) -> bool {
1013        self.token::<OpenHeredoc<N::Token>>().is_some()
1014    }
1015
1016    /// Gets the parts of the command.
1017    pub fn parts(&self) -> impl Iterator<Item = CommandPart<N>> + use<'_, N> {
1018        self.0.children_with_tokens().filter_map(CommandPart::cast)
1019    }
1020
1021    /// Gets the command text if the command is not interpolated (i.e. has no
1022    /// placeholders).
1023    ///
1024    /// Returns `None` if the command is interpolated, as interpolated commands
1025    /// cannot be represented as a single span of text.
1026    pub fn text(&self) -> Option<CommandText<N::Token>> {
1027        let mut parts = self.parts();
1028        if let Some(CommandPart::Text(text)) = parts.next()
1029            && parts.next().is_none()
1030        {
1031            return Some(text);
1032        }
1033
1034        None
1035    }
1036
1037    /// Counts the leading whitespace of the command.
1038    ///
1039    /// If the command has mixed indentation, this will return None.
1040    pub fn count_whitespace(&self) -> Option<usize> {
1041        let mut min_leading_spaces = usize::MAX;
1042        let mut min_leading_tabs = usize::MAX;
1043        let mut parsing_leading_whitespace = false; // init to false so that the first line is skipped
1044
1045        let mut leading_spaces = 0;
1046        let mut leading_tabs = 0;
1047        for part in self.parts() {
1048            match part {
1049                CommandPart::Text(text) => {
1050                    for c in text.text().chars() {
1051                        match c {
1052                            ' ' if parsing_leading_whitespace => {
1053                                leading_spaces += 1;
1054                            }
1055                            '\t' if parsing_leading_whitespace => {
1056                                leading_tabs += 1;
1057                            }
1058                            '\n' => {
1059                                parsing_leading_whitespace = true;
1060                                leading_spaces = 0;
1061                                leading_tabs = 0;
1062                            }
1063                            '\r' => {}
1064                            _ => {
1065                                if parsing_leading_whitespace {
1066                                    parsing_leading_whitespace = false;
1067                                    if leading_spaces == 0 && leading_tabs == 0 {
1068                                        min_leading_spaces = 0;
1069                                        min_leading_tabs = 0;
1070                                        continue;
1071                                    }
1072                                    if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1073                                        min_leading_spaces = leading_spaces;
1074                                    }
1075                                    if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1076                                        min_leading_tabs = leading_tabs;
1077                                    }
1078                                }
1079                            }
1080                        }
1081                    }
1082                    // The last line is intentionally skipped.
1083                }
1084                CommandPart::Placeholder(_) => {
1085                    if parsing_leading_whitespace {
1086                        parsing_leading_whitespace = false;
1087                        if leading_spaces == 0 && leading_tabs == 0 {
1088                            min_leading_spaces = 0;
1089                            min_leading_tabs = 0;
1090                            continue;
1091                        }
1092                        if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1093                            min_leading_spaces = leading_spaces;
1094                        }
1095                        if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1096                            min_leading_tabs = leading_tabs;
1097                        }
1098                    }
1099                }
1100            }
1101        }
1102
1103        // Check for no indentation or all whitespace, in which case we're done
1104        if (min_leading_spaces == 0 && min_leading_tabs == 0)
1105            || (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX)
1106        {
1107            return Some(0);
1108        }
1109
1110        // Check for mixed indentation
1111        if (min_leading_spaces > 0 && min_leading_spaces != usize::MAX)
1112            && (min_leading_tabs > 0 && min_leading_tabs != usize::MAX)
1113        {
1114            return None;
1115        }
1116
1117        // Exactly one of the two will be equal to usize::MAX because it never appeared.
1118        // The other will be the number of leading spaces or tabs to strip.
1119        let final_leading_whitespace = if min_leading_spaces < min_leading_tabs {
1120            min_leading_spaces
1121        } else {
1122            min_leading_tabs
1123        };
1124
1125        Some(final_leading_whitespace)
1126    }
1127
1128    /// Strips leading whitespace from the command.
1129    ///
1130    /// If the command has mixed indentation, this will return `None`.
1131    pub fn strip_whitespace(&self) -> Option<Vec<StrippedCommandPart<N>>> {
1132        let mut result = Vec::new();
1133        let heredoc = self.is_heredoc();
1134        for part in self.parts() {
1135            match part {
1136                CommandPart::Text(text) => {
1137                    let mut s = String::new();
1138                    unescape_command_text(text.text(), heredoc, &mut s);
1139                    result.push(StrippedCommandPart::Text(s));
1140                }
1141                CommandPart::Placeholder(p) => {
1142                    result.push(StrippedCommandPart::Placeholder(p));
1143                }
1144            }
1145        }
1146
1147        // Trim the first line
1148        let mut whole_first_line_trimmed = false;
1149        if let Some(StrippedCommandPart::Text(text)) = result.first_mut() {
1150            let end_of_first_line = text.find('\n').map(|p| p + 1).unwrap_or(text.len());
1151            let line = &text[..end_of_first_line];
1152            let len = line.len() - line.trim_start().len();
1153            whole_first_line_trimmed = len == line.len();
1154            text.replace_range(..len, "");
1155        }
1156
1157        // Trim the last line
1158        if let Some(StrippedCommandPart::Text(text)) = result.last_mut() {
1159            if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) {
1160                text.truncate(index + 1);
1161            } else {
1162                text.clear();
1163            }
1164
1165            if text.ends_with('\n') {
1166                text.pop();
1167            }
1168
1169            if text.ends_with('\r') {
1170                text.pop();
1171            }
1172        }
1173
1174        // Return immediately if command contains mixed indentation
1175        let num_stripped_chars = self.count_whitespace()?;
1176
1177        // If there is no leading whitespace, we're done
1178        if num_stripped_chars == 0 {
1179            return Some(result);
1180        }
1181
1182        // Finally, strip the leading whitespace on each line
1183        // This is done in place using the `replace_range` method; the method will
1184        // internally do moves without allocations
1185        let mut strip_leading_whitespace = whole_first_line_trimmed;
1186        for part in &mut result {
1187            match part {
1188                StrippedCommandPart::Text(text) => {
1189                    let mut offset = 0;
1190                    while let Some(next) = text[offset..].find('\n') {
1191                        let next = next + offset;
1192                        if offset > 0 {
1193                            strip_leading_whitespace = true;
1194                        }
1195
1196                        if !strip_leading_whitespace {
1197                            offset = next + 1;
1198                            continue;
1199                        }
1200
1201                        let line = &text[offset..next];
1202                        let line = line.strip_suffix('\r').unwrap_or(line);
1203                        let len = line.len().min(num_stripped_chars);
1204                        text.replace_range(offset..offset + len, "");
1205                        offset = next + 1 - len;
1206                    }
1207
1208                    // Replace any remaining text
1209                    if strip_leading_whitespace || offset > 0 {
1210                        let line = &text[offset..];
1211                        let line = line.strip_suffix('\r').unwrap_or(line);
1212                        let len = line.len().min(num_stripped_chars);
1213                        text.replace_range(offset..offset + len, "");
1214                    }
1215                }
1216                StrippedCommandPart::Placeholder(_) => {
1217                    strip_leading_whitespace = false;
1218                }
1219            }
1220        }
1221
1222        Some(result)
1223    }
1224
1225    /// Gets the parent of the command section.
1226    pub fn parent(&self) -> SectionParent<N> {
1227        SectionParent::cast(self.0.parent().expect("should have a parent"))
1228            .expect("parent should cast")
1229    }
1230}
1231
1232impl<N: TreeNode> AstNode<N> for CommandSection<N> {
1233    fn can_cast(kind: SyntaxKind) -> bool {
1234        kind == SyntaxKind::CommandSectionNode
1235    }
1236
1237    fn cast(inner: N) -> Option<Self> {
1238        match inner.kind() {
1239            SyntaxKind::CommandSectionNode => Some(Self(inner)),
1240            _ => None,
1241        }
1242    }
1243
1244    fn inner(&self) -> &N {
1245        &self.0
1246    }
1247}
1248
1249/// Represents a textual part of a command.
1250#[derive(Clone, Debug, PartialEq, Eq)]
1251pub struct CommandText<T: TreeToken = SyntaxToken>(T);
1252
1253impl<T: TreeToken> CommandText<T> {
1254    /// Unescapes the command text to the given buffer.
1255    ///
1256    /// When `heredoc` is true, only heredoc escape sequences are allowed.
1257    ///
1258    /// Otherwise, brace command sequences are accepted.
1259    pub fn unescape_to(&self, heredoc: bool, buffer: &mut String) {
1260        unescape_command_text(self.text(), heredoc, buffer);
1261    }
1262}
1263
1264impl<T: TreeToken> AstToken<T> for CommandText<T> {
1265    fn can_cast(kind: SyntaxKind) -> bool {
1266        kind == SyntaxKind::LiteralCommandText
1267    }
1268
1269    fn cast(inner: T) -> Option<Self> {
1270        match inner.kind() {
1271            SyntaxKind::LiteralCommandText => Some(Self(inner)),
1272            _ => None,
1273        }
1274    }
1275
1276    fn inner(&self) -> &T {
1277        &self.0
1278    }
1279}
1280
1281/// Represents a part of a command.
1282#[derive(Clone, Debug, PartialEq, Eq)]
1283pub enum CommandPart<N: TreeNode = SyntaxNode> {
1284    /// A textual part of the command.
1285    Text(CommandText<N::Token>),
1286    /// A placeholder encountered in the command.
1287    Placeholder(Placeholder<N>),
1288}
1289
1290impl<N: TreeNode> CommandPart<N> {
1291    /// Unwraps the command part into text.
1292    ///
1293    /// # Panics
1294    ///
1295    /// Panics if the command part is not text.
1296    pub fn unwrap_text(self) -> CommandText<N::Token> {
1297        match self {
1298            Self::Text(text) => text,
1299            _ => panic!("not string text"),
1300        }
1301    }
1302
1303    /// Unwraps the command part into a placeholder.
1304    ///
1305    /// # Panics
1306    ///
1307    /// Panics if the command part is not a placeholder.
1308    pub fn unwrap_placeholder(self) -> Placeholder<N> {
1309        match self {
1310            Self::Placeholder(p) => p,
1311            _ => panic!("not a placeholder"),
1312        }
1313    }
1314
1315    /// Casts the given [`NodeOrToken`] to [`CommandPart`].
1316    ///
1317    /// Returns `None` if it cannot case cannot be cast.
1318    fn cast(element: NodeOrToken<N, N::Token>) -> Option<Self> {
1319        match element {
1320            NodeOrToken::Node(n) => Some(Self::Placeholder(Placeholder::cast(n)?)),
1321            NodeOrToken::Token(t) => Some(Self::Text(CommandText::cast(t)?)),
1322        }
1323    }
1324}
1325
1326/// Represents a requirements section in a task definition.
1327#[derive(Clone, Debug, PartialEq, Eq)]
1328pub struct RequirementsSection<N: TreeNode = SyntaxNode>(N);
1329
1330impl<N: TreeNode> RequirementsSection<N> {
1331    /// Gets the items in the requirements section.
1332    pub fn items(&self) -> impl Iterator<Item = RequirementsItem<N>> + use<'_, N> {
1333        self.children()
1334    }
1335
1336    /// Gets the parent of the requirements section.
1337    pub fn parent(&self) -> SectionParent<N> {
1338        SectionParent::cast(self.0.parent().expect("should have a parent"))
1339            .expect("parent should cast")
1340    }
1341
1342    /// Gets the `container` item as a
1343    /// [`Container`](requirements::item::Container) (if it exists).
1344    pub fn container(&self) -> Option<requirements::item::Container<N>> {
1345        // NOTE: validation should ensure that, at most, one `container` item exists in
1346        // the `requirements` section.
1347        self.child()
1348    }
1349}
1350
1351impl<N: TreeNode> AstNode<N> for RequirementsSection<N> {
1352    fn can_cast(kind: SyntaxKind) -> bool {
1353        kind == SyntaxKind::RequirementsSectionNode
1354    }
1355
1356    fn cast(inner: N) -> Option<Self> {
1357        match inner.kind() {
1358            SyntaxKind::RequirementsSectionNode => Some(Self(inner)),
1359            _ => None,
1360        }
1361    }
1362
1363    fn inner(&self) -> &N {
1364        &self.0
1365    }
1366}
1367
1368/// Represents an item in a requirements section.
1369#[derive(Clone, Debug, PartialEq, Eq)]
1370pub struct RequirementsItem<N: TreeNode = SyntaxNode>(N);
1371
1372impl<N: TreeNode> RequirementsItem<N> {
1373    /// Gets the name of the requirements item.
1374    pub fn name(&self) -> Ident<N::Token> {
1375        self.token().expect("expected an item name")
1376    }
1377
1378    /// Gets the expression of the requirements item.
1379    pub fn expr(&self) -> Expr<N> {
1380        Expr::child(&self.0).expect("expected an item expression")
1381    }
1382
1383    /// Consumes `self` and attempts to cast the requirements item to a
1384    /// [`Container`](requirements::item::Container).
1385    pub fn into_container(self) -> Option<requirements::item::Container<N>> {
1386        requirements::item::Container::try_from(self).ok()
1387    }
1388}
1389
1390impl<N: TreeNode> AstNode<N> for RequirementsItem<N> {
1391    fn can_cast(kind: SyntaxKind) -> bool {
1392        kind == SyntaxKind::RequirementsItemNode
1393    }
1394
1395    fn cast(inner: N) -> Option<Self> {
1396        match inner.kind() {
1397            SyntaxKind::RequirementsItemNode => Some(Self(inner)),
1398            _ => None,
1399        }
1400    }
1401
1402    fn inner(&self) -> &N {
1403        &self.0
1404    }
1405}
1406
1407/// Represents a hints section in a task definition.
1408#[derive(Clone, Debug, PartialEq, Eq)]
1409pub struct TaskHintsSection<N: TreeNode = SyntaxNode>(N);
1410
1411impl<N: TreeNode> TaskHintsSection<N> {
1412    /// Gets the items in the hints section.
1413    pub fn items(&self) -> impl Iterator<Item = TaskHintsItem<N>> + use<'_, N> {
1414        self.children()
1415    }
1416
1417    /// Gets the parent of the hints section.
1418    pub fn parent(&self) -> TaskDefinition<N> {
1419        TaskDefinition::cast(self.0.parent().expect("should have a parent"))
1420            .expect("parent should cast")
1421    }
1422}
1423
1424impl<N: TreeNode> AstNode<N> for TaskHintsSection<N> {
1425    fn can_cast(kind: SyntaxKind) -> bool {
1426        kind == SyntaxKind::TaskHintsSectionNode
1427    }
1428
1429    fn cast(inner: N) -> Option<Self> {
1430        match inner.kind() {
1431            SyntaxKind::TaskHintsSectionNode => Some(Self(inner)),
1432            _ => None,
1433        }
1434    }
1435
1436    fn inner(&self) -> &N {
1437        &self.0
1438    }
1439}
1440
1441/// Represents an item in a task hints section.
1442#[derive(Clone, Debug, PartialEq, Eq)]
1443pub struct TaskHintsItem<N: TreeNode = SyntaxNode>(N);
1444
1445impl<N: TreeNode> TaskHintsItem<N> {
1446    /// Gets the name of the hints item.
1447    pub fn name(&self) -> Ident<N::Token> {
1448        self.token().expect("expected an item name")
1449    }
1450
1451    /// Gets the expression of the hints item.
1452    pub fn expr(&self) -> Expr<N> {
1453        Expr::child(&self.0).expect("expected an item expression")
1454    }
1455}
1456
1457impl<N: TreeNode> AstNode<N> for TaskHintsItem<N> {
1458    fn can_cast(kind: SyntaxKind) -> bool {
1459        kind == SyntaxKind::TaskHintsItemNode
1460    }
1461
1462    fn cast(inner: N) -> Option<Self> {
1463        match inner.kind() {
1464            SyntaxKind::TaskHintsItemNode => Some(Self(inner)),
1465            _ => None,
1466        }
1467    }
1468
1469    fn inner(&self) -> &N {
1470        &self.0
1471    }
1472}
1473
1474/// Represents a runtime section in a task definition.
1475#[derive(Clone, Debug, PartialEq, Eq)]
1476pub struct RuntimeSection<N: TreeNode = SyntaxNode>(N);
1477
1478impl<N: TreeNode> RuntimeSection<N> {
1479    /// Gets the items in the runtime section.
1480    pub fn items(&self) -> impl Iterator<Item = RuntimeItem<N>> + use<'_, N> {
1481        self.children()
1482    }
1483
1484    /// Gets the parent of the runtime section.
1485    pub fn parent(&self) -> SectionParent<N> {
1486        SectionParent::cast(self.0.parent().expect("should have a parent"))
1487            .expect("parent should cast")
1488    }
1489
1490    /// Gets the `container` item as a [`Container`](runtime::item::Container)
1491    /// (if it exists).
1492    pub fn container(&self) -> Option<runtime::item::Container<N>> {
1493        // NOTE: validation should ensure that, at most, one `container`/`docker` item
1494        // exists in the `runtime` section.
1495        self.child()
1496    }
1497}
1498
1499impl<N: TreeNode> AstNode<N> for RuntimeSection<N> {
1500    fn can_cast(kind: SyntaxKind) -> bool {
1501        kind == SyntaxKind::RuntimeSectionNode
1502    }
1503
1504    fn cast(inner: N) -> Option<Self> {
1505        match inner.kind() {
1506            SyntaxKind::RuntimeSectionNode => Some(Self(inner)),
1507            _ => None,
1508        }
1509    }
1510
1511    fn inner(&self) -> &N {
1512        &self.0
1513    }
1514}
1515
1516/// Represents an item in a runtime section.
1517#[derive(Clone, Debug, PartialEq, Eq)]
1518pub struct RuntimeItem<N: TreeNode = SyntaxNode>(N);
1519
1520impl<N: TreeNode> RuntimeItem<N> {
1521    /// Gets the name of the runtime item.
1522    pub fn name(&self) -> Ident<N::Token> {
1523        self.token().expect("expected an item name")
1524    }
1525
1526    /// Gets the expression of the runtime item.
1527    pub fn expr(&self) -> Expr<N> {
1528        Expr::child(&self.0).expect("expected an item expression")
1529    }
1530
1531    /// Consumes `self` and attempts to cast the runtime item to a
1532    /// [`Container`](runtime::item::Container).
1533    pub fn into_container(self) -> Option<runtime::item::Container<N>> {
1534        runtime::item::Container::try_from(self).ok()
1535    }
1536}
1537
1538impl<N: TreeNode> AstNode<N> for RuntimeItem<N> {
1539    fn can_cast(kind: SyntaxKind) -> bool {
1540        kind == SyntaxKind::RuntimeItemNode
1541    }
1542
1543    fn cast(inner: N) -> Option<Self> {
1544        match inner.kind() {
1545            SyntaxKind::RuntimeItemNode => Some(Self(inner)),
1546            _ => None,
1547        }
1548    }
1549
1550    fn inner(&self) -> &N {
1551        &self.0
1552    }
1553}
1554
1555/// Represents a metadata section in a task or workflow definition.
1556#[derive(Clone, Debug, PartialEq, Eq)]
1557pub struct MetadataSection<N: TreeNode = SyntaxNode>(N);
1558
1559impl<N: TreeNode> MetadataSection<N> {
1560    /// Gets the items of the metadata section.
1561    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1562        self.children()
1563    }
1564
1565    /// Gets the parent of the metadata section.
1566    pub fn parent(&self) -> SectionParent<N> {
1567        SectionParent::cast(self.0.parent().expect("should have a parent"))
1568            .expect("parent should cast")
1569    }
1570}
1571
1572impl<N: TreeNode> AstNode<N> for MetadataSection<N> {
1573    fn can_cast(kind: SyntaxKind) -> bool {
1574        kind == SyntaxKind::MetadataSectionNode
1575    }
1576
1577    fn cast(inner: N) -> Option<Self> {
1578        match inner.kind() {
1579            SyntaxKind::MetadataSectionNode => Some(Self(inner)),
1580            _ => None,
1581        }
1582    }
1583
1584    fn inner(&self) -> &N {
1585        &self.0
1586    }
1587}
1588
1589/// Represents a metadata object item.
1590#[derive(Clone, Debug, PartialEq, Eq)]
1591pub struct MetadataObjectItem<N: TreeNode = SyntaxNode>(N);
1592
1593impl<N: TreeNode> MetadataObjectItem<N> {
1594    /// Gets the name of the item.
1595    pub fn name(&self) -> Ident<N::Token> {
1596        self.token().expect("expected a name")
1597    }
1598
1599    /// Gets the value of the item.
1600    pub fn value(&self) -> MetadataValue<N> {
1601        self.child().expect("expected a value")
1602    }
1603}
1604
1605impl<N: TreeNode> AstNode<N> for MetadataObjectItem<N> {
1606    fn can_cast(kind: SyntaxKind) -> bool {
1607        kind == SyntaxKind::MetadataObjectItemNode
1608    }
1609
1610    fn cast(inner: N) -> Option<Self> {
1611        match inner.kind() {
1612            SyntaxKind::MetadataObjectItemNode => Some(Self(inner)),
1613            _ => None,
1614        }
1615    }
1616
1617    fn inner(&self) -> &N {
1618        &self.0
1619    }
1620}
1621
1622/// Represents a metadata value.
1623#[derive(Clone, Debug, PartialEq, Eq)]
1624pub enum MetadataValue<N: TreeNode = SyntaxNode> {
1625    /// The value is a literal boolean.
1626    Boolean(LiteralBoolean<N>),
1627    /// The value is a literal integer.
1628    Integer(LiteralInteger<N>),
1629    /// The value is a literal float.
1630    Float(LiteralFloat<N>),
1631    /// The value is a literal string.
1632    String(LiteralString<N>),
1633    /// The value is a literal null.
1634    Null(LiteralNull<N>),
1635    /// The value is a metadata object.
1636    Object(MetadataObject<N>),
1637    /// The value is a metadata array.
1638    Array(MetadataArray<N>),
1639}
1640
1641impl<N: TreeNode> MetadataValue<N> {
1642    /// Unwraps the metadata value into a boolean.
1643    ///
1644    /// # Panics
1645    ///
1646    /// Panics if the metadata value is not a boolean.
1647    pub fn unwrap_boolean(self) -> LiteralBoolean<N> {
1648        match self {
1649            Self::Boolean(b) => b,
1650            _ => panic!("not a boolean"),
1651        }
1652    }
1653
1654    /// Unwraps the metadata value into an integer.
1655    ///
1656    /// # Panics
1657    ///
1658    /// Panics if the metadata value is not an integer.
1659    pub fn unwrap_integer(self) -> LiteralInteger<N> {
1660        match self {
1661            Self::Integer(i) => i,
1662            _ => panic!("not an integer"),
1663        }
1664    }
1665
1666    /// Unwraps the metadata value into a float.
1667    ///
1668    /// # Panics
1669    ///
1670    /// Panics if the metadata value is not a float.
1671    pub fn unwrap_float(self) -> LiteralFloat<N> {
1672        match self {
1673            Self::Float(f) => f,
1674            _ => panic!("not a float"),
1675        }
1676    }
1677
1678    /// Unwraps the metadata value into a string.
1679    ///
1680    /// # Panics
1681    ///
1682    /// Panics if the metadata value is not a string.
1683    pub fn unwrap_string(self) -> LiteralString<N> {
1684        match self {
1685            Self::String(s) => s,
1686            _ => panic!("not a string"),
1687        }
1688    }
1689
1690    /// Unwraps the metadata value into a null.
1691    ///
1692    /// # Panics
1693    ///
1694    /// Panics if the metadata value is not a null.
1695    pub fn unwrap_null(self) -> LiteralNull<N> {
1696        match self {
1697            Self::Null(n) => n,
1698            _ => panic!("not a null"),
1699        }
1700    }
1701
1702    /// Unwraps the metadata value into an object.
1703    ///
1704    /// # Panics
1705    ///
1706    /// Panics if the metadata value is not an object.
1707    pub fn unwrap_object(self) -> MetadataObject<N> {
1708        match self {
1709            Self::Object(o) => o,
1710            _ => panic!("not an object"),
1711        }
1712    }
1713
1714    /// Unwraps the metadata value into an array.
1715    ///
1716    /// # Panics
1717    ///
1718    /// Panics if the metadata value is not an array.
1719    pub fn unwrap_array(self) -> MetadataArray<N> {
1720        match self {
1721            Self::Array(a) => a,
1722            _ => panic!("not an array"),
1723        }
1724    }
1725}
1726
1727impl<N: TreeNode> AstNode<N> for MetadataValue<N> {
1728    fn can_cast(kind: SyntaxKind) -> bool {
1729        matches!(
1730            kind,
1731            SyntaxKind::LiteralBooleanNode
1732                | SyntaxKind::LiteralIntegerNode
1733                | SyntaxKind::LiteralFloatNode
1734                | SyntaxKind::LiteralStringNode
1735                | SyntaxKind::LiteralNullNode
1736                | SyntaxKind::MetadataObjectNode
1737                | SyntaxKind::MetadataArrayNode
1738        )
1739    }
1740
1741    fn cast(inner: N) -> Option<Self> {
1742        match inner.kind() {
1743            SyntaxKind::LiteralBooleanNode => Some(Self::Boolean(LiteralBoolean(inner))),
1744            SyntaxKind::LiteralIntegerNode => Some(Self::Integer(LiteralInteger(inner))),
1745            SyntaxKind::LiteralFloatNode => Some(Self::Float(LiteralFloat(inner))),
1746            SyntaxKind::LiteralStringNode => Some(Self::String(LiteralString(inner))),
1747            SyntaxKind::LiteralNullNode => Some(Self::Null(LiteralNull(inner))),
1748            SyntaxKind::MetadataObjectNode => Some(Self::Object(MetadataObject(inner))),
1749            SyntaxKind::MetadataArrayNode => Some(Self::Array(MetadataArray(inner))),
1750            _ => None,
1751        }
1752    }
1753
1754    fn inner(&self) -> &N {
1755        match self {
1756            Self::Boolean(b) => &b.0,
1757            Self::Integer(i) => &i.0,
1758            Self::Float(f) => &f.0,
1759            Self::String(s) => &s.0,
1760            Self::Null(n) => &n.0,
1761            Self::Object(o) => &o.0,
1762            Self::Array(a) => &a.0,
1763        }
1764    }
1765}
1766
1767/// Represents a literal null.
1768#[derive(Clone, Debug, PartialEq, Eq)]
1769pub struct LiteralNull<N: TreeNode = SyntaxNode>(N);
1770
1771impl<N: TreeNode> AstNode<N> for LiteralNull<N> {
1772    fn can_cast(kind: SyntaxKind) -> bool {
1773        kind == SyntaxKind::LiteralNullNode
1774    }
1775
1776    fn cast(inner: N) -> Option<Self> {
1777        match inner.kind() {
1778            SyntaxKind::LiteralNullNode => Some(Self(inner)),
1779            _ => None,
1780        }
1781    }
1782
1783    fn inner(&self) -> &N {
1784        &self.0
1785    }
1786}
1787
1788/// Represents a metadata object.
1789#[derive(Clone, Debug, PartialEq, Eq)]
1790pub struct MetadataObject<N: TreeNode = SyntaxNode>(N);
1791
1792impl<N: TreeNode> MetadataObject<N> {
1793    /// Gets the items of the metadata object.
1794    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1795        self.children()
1796    }
1797}
1798
1799impl<N: TreeNode> AstNode<N> for MetadataObject<N> {
1800    fn can_cast(kind: SyntaxKind) -> bool {
1801        kind == SyntaxKind::MetadataObjectNode
1802    }
1803
1804    fn cast(inner: N) -> Option<Self> {
1805        match inner.kind() {
1806            SyntaxKind::MetadataObjectNode => Some(Self(inner)),
1807            _ => None,
1808        }
1809    }
1810
1811    fn inner(&self) -> &N {
1812        &self.0
1813    }
1814}
1815
1816/// Represents a metadata array.
1817#[derive(Clone, Debug, PartialEq, Eq)]
1818pub struct MetadataArray<N: TreeNode = SyntaxNode>(N);
1819
1820impl<N: TreeNode> MetadataArray<N> {
1821    /// Gets the elements of the metadata array.
1822    pub fn elements(&self) -> impl Iterator<Item = MetadataValue<N>> + use<'_, N> {
1823        self.children()
1824    }
1825}
1826
1827impl<N: TreeNode> AstNode<N> for MetadataArray<N> {
1828    fn can_cast(kind: SyntaxKind) -> bool {
1829        kind == SyntaxKind::MetadataArrayNode
1830    }
1831
1832    fn cast(inner: N) -> Option<Self> {
1833        match inner.kind() {
1834            SyntaxKind::MetadataArrayNode => Some(Self(inner)),
1835            _ => None,
1836        }
1837    }
1838
1839    fn inner(&self) -> &N {
1840        &self.0
1841    }
1842}
1843
1844/// Represents a parameter metadata section in a task or workflow definition.
1845#[derive(Clone, Debug, PartialEq, Eq)]
1846pub struct ParameterMetadataSection<N: TreeNode = SyntaxNode>(N);
1847
1848impl<N: TreeNode> ParameterMetadataSection<N> {
1849    /// Gets the items of the parameter metadata section.
1850    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1851        self.children()
1852    }
1853
1854    /// Gets the parent of the parameter metadata section.
1855    pub fn parent(&self) -> SectionParent<N> {
1856        SectionParent::cast(self.0.parent().expect("should have a parent"))
1857            .expect("parent should cast")
1858    }
1859}
1860
1861impl<N: TreeNode> AstNode<N> for ParameterMetadataSection<N> {
1862    fn can_cast(kind: SyntaxKind) -> bool {
1863        kind == SyntaxKind::ParameterMetadataSectionNode
1864    }
1865
1866    fn cast(inner: N) -> Option<Self> {
1867        match inner.kind() {
1868            SyntaxKind::ParameterMetadataSectionNode => Some(Self(inner)),
1869            _ => None,
1870        }
1871    }
1872
1873    fn inner(&self) -> &N {
1874        &self.0
1875    }
1876}
1877
1878#[cfg(test)]
1879mod test {
1880    use pretty_assertions::assert_eq;
1881
1882    use super::*;
1883    use crate::Document;
1884
1885    #[test]
1886    fn tasks() {
1887        let (document, diagnostics) = Document::parse(
1888            r#"
1889version 1.2
1890
1891task test {
1892    input {
1893        String name
1894    }
1895
1896    output {
1897        String output = stdout()
1898    }
1899
1900    command <<<
1901        printf "hello, ~{name}!
1902    >>>
1903
1904    requirements {
1905        container: "baz/qux"
1906    }
1907
1908    hints {
1909        foo: "bar"
1910    }
1911
1912    runtime {
1913        container: "foo/bar"
1914    }
1915
1916    meta {
1917        description: "a test"
1918        foo: null
1919    }
1920
1921    parameter_meta {
1922        name: {
1923            help: "a name to greet"
1924        }
1925    }
1926
1927    String x = "private"
1928}
1929"#,
1930        );
1931
1932        assert!(diagnostics.is_empty());
1933        let ast = document.ast();
1934        let ast = ast.as_v1().expect("should be a V1 AST");
1935        let tasks: Vec<_> = ast.tasks().collect();
1936        assert_eq!(tasks.len(), 1);
1937        assert_eq!(tasks[0].name().text(), "test");
1938
1939        // Task input
1940        let input = tasks[0].input().expect("should have an input section");
1941        assert_eq!(input.parent().unwrap_task().name().text(), "test");
1942        let decls: Vec<_> = input.declarations().collect();
1943        assert_eq!(decls.len(), 1);
1944        assert_eq!(
1945            decls[0].clone().unwrap_unbound_decl().ty().to_string(),
1946            "String"
1947        );
1948        assert_eq!(decls[0].clone().unwrap_unbound_decl().name().text(), "name");
1949
1950        // Task output
1951        let output = tasks[0].output().expect("should have an output section");
1952        assert_eq!(output.parent().unwrap_task().name().text(), "test");
1953        let decls: Vec<_> = output.declarations().collect();
1954        assert_eq!(decls.len(), 1);
1955        assert_eq!(decls[0].ty().to_string(), "String");
1956        assert_eq!(decls[0].name().text(), "output");
1957        assert_eq!(decls[0].expr().unwrap_call().target().text(), "stdout");
1958
1959        // Task command
1960        let command = tasks[0].command().expect("should have a command section");
1961        assert_eq!(command.parent().name().text(), "test");
1962        assert!(command.is_heredoc());
1963        let parts: Vec<_> = command.parts().collect();
1964        assert_eq!(parts.len(), 3);
1965        assert_eq!(
1966            parts[0].clone().unwrap_text().text(),
1967            "\n        printf \"hello, "
1968        );
1969        assert_eq!(
1970            parts[1]
1971                .clone()
1972                .unwrap_placeholder()
1973                .expr()
1974                .unwrap_name_ref()
1975                .name()
1976                .text(),
1977            "name"
1978        );
1979        assert_eq!(parts[2].clone().unwrap_text().text(), "!\n    ");
1980
1981        // Task requirements
1982        let requirements = tasks[0]
1983            .requirements()
1984            .expect("should have a requirements section");
1985        assert_eq!(requirements.parent().name().text(), "test");
1986        let items: Vec<_> = requirements.items().collect();
1987        assert_eq!(items.len(), 1);
1988        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
1989        assert_eq!(
1990            items[0]
1991                .expr()
1992                .unwrap_literal()
1993                .unwrap_string()
1994                .text()
1995                .unwrap()
1996                .text(),
1997            "baz/qux"
1998        );
1999
2000        // Task hints
2001        let hints = tasks[0].hints().expect("should have a hints section");
2002        assert_eq!(hints.parent().name().text(), "test");
2003        let items: Vec<_> = hints.items().collect();
2004        assert_eq!(items.len(), 1);
2005        assert_eq!(items[0].name().text(), "foo");
2006        assert_eq!(
2007            items[0]
2008                .expr()
2009                .unwrap_literal()
2010                .unwrap_string()
2011                .text()
2012                .unwrap()
2013                .text(),
2014            "bar"
2015        );
2016
2017        // Task runtimes
2018        let runtime = tasks[0].runtime().expect("should have a runtime section");
2019        assert_eq!(runtime.parent().name().text(), "test");
2020        let items: Vec<_> = runtime.items().collect();
2021        assert_eq!(items.len(), 1);
2022        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
2023        assert_eq!(
2024            items[0]
2025                .expr()
2026                .unwrap_literal()
2027                .unwrap_string()
2028                .text()
2029                .unwrap()
2030                .text(),
2031            "foo/bar"
2032        );
2033
2034        // Task metadata
2035        let metadata = tasks[0].metadata().expect("should have a metadata section");
2036        assert_eq!(metadata.parent().unwrap_task().name().text(), "test");
2037        let items: Vec<_> = metadata.items().collect();
2038        assert_eq!(items.len(), 2);
2039        assert_eq!(items[0].name().text(), "description");
2040        assert_eq!(
2041            items[0].value().unwrap_string().text().unwrap().text(),
2042            "a test"
2043        );
2044
2045        // Second metadata
2046        assert_eq!(items[1].name().text(), "foo");
2047        items[1].value().unwrap_null();
2048
2049        // Task parameter metadata
2050        let param_meta = tasks[0]
2051            .parameter_metadata()
2052            .expect("should have a parameter metadata section");
2053        assert_eq!(param_meta.parent().unwrap_task().name().text(), "test");
2054        let items: Vec<_> = param_meta.items().collect();
2055        assert_eq!(items.len(), 1);
2056        assert_eq!(items[0].name().text(), "name");
2057        let items: Vec<_> = items[0].value().unwrap_object().items().collect();
2058        assert_eq!(items.len(), 1);
2059        assert_eq!(items[0].name().text(), "help");
2060        assert_eq!(
2061            items[0].value().unwrap_string().text().unwrap().text(),
2062            "a name to greet"
2063        );
2064
2065        // Task declarations
2066        let decls: Vec<_> = tasks[0].declarations().collect();
2067        assert_eq!(decls.len(), 1);
2068
2069        // First task declaration
2070        assert_eq!(decls[0].ty().to_string(), "String");
2071        assert_eq!(decls[0].name().text(), "x");
2072        assert_eq!(
2073            decls[0]
2074                .expr()
2075                .unwrap_literal()
2076                .unwrap_string()
2077                .text()
2078                .unwrap()
2079                .text(),
2080            "private"
2081        );
2082    }
2083
2084    #[test]
2085    fn whitespace_stripping_without_interpolation() {
2086        let (document, diagnostics) = Document::parse(
2087            r#"
2088version 1.2
2089
2090task test {
2091    command <<<
2092        echo "hello"
2093        echo "world"
2094        echo \
2095            "goodbye"
2096    >>>
2097}
2098"#,
2099        );
2100
2101        assert!(diagnostics.is_empty());
2102        let ast = document.ast();
2103        let ast = ast.as_v1().expect("should be a V1 AST");
2104        let tasks: Vec<_> = ast.tasks().collect();
2105        assert_eq!(tasks.len(), 1);
2106
2107        let command = tasks[0].command().expect("should have a command section");
2108
2109        let stripped = command.strip_whitespace().unwrap();
2110
2111        assert_eq!(stripped.len(), 1);
2112        let text = match &stripped[0] {
2113            StrippedCommandPart::Text(text) => text,
2114            _ => panic!("expected text"),
2115        };
2116        assert_eq!(
2117            text,
2118            "echo \"hello\"\necho \"world\"\necho \\\n    \"goodbye\""
2119        );
2120    }
2121
2122    #[test]
2123    fn whitespace_stripping_with_interpolation() {
2124        let (document, diagnostics) = Document::parse(
2125            r#"
2126version 1.2
2127
2128task test {
2129    input {
2130        String name
2131        Boolean flag
2132    }
2133
2134    command <<<
2135        echo "hello, ~{
2136if flag
2137then name
2138               else "Jerry"
2139    }!"
2140    >>>
2141}
2142    "#,
2143        );
2144
2145        assert!(diagnostics.is_empty());
2146        let ast = document.ast();
2147        let ast = ast.as_v1().expect("should be a V1 AST");
2148        let tasks: Vec<_> = ast.tasks().collect();
2149        assert_eq!(tasks.len(), 1);
2150
2151        let command = tasks[0].command().expect("should have a command section");
2152
2153        let stripped = command.strip_whitespace().unwrap();
2154        assert_eq!(stripped.len(), 3);
2155        let text = match &stripped[0] {
2156            StrippedCommandPart::Text(text) => text,
2157            _ => panic!("expected text"),
2158        };
2159        assert_eq!(text, "echo \"hello, ");
2160
2161        let _placeholder = match &stripped[1] {
2162            StrippedCommandPart::Placeholder(p) => p,
2163            _ => panic!("expected placeholder"),
2164        };
2165        // not testing anything with the placeholder, just making sure it's there
2166
2167        let text = match &stripped[2] {
2168            StrippedCommandPart::Text(text) => text,
2169            _ => panic!("expected text"),
2170        };
2171        assert_eq!(text, "!\"");
2172    }
2173
2174    #[test]
2175    fn whitespace_stripping_when_interpolation_starts_line() {
2176        let (document, diagnostics) = Document::parse(
2177            r#"
2178version 1.2
2179
2180task test {
2181    input {
2182      Int placeholder
2183    }
2184
2185    command <<<
2186            # other weird whitspace
2187      ~{placeholder} "$trailing_pholder" ~{placeholder}
2188      ~{placeholder} somecommand.py "$leading_pholder"
2189    >>>
2190}
2191"#,
2192        );
2193
2194        assert!(diagnostics.is_empty());
2195        let ast = document.ast();
2196        let ast = ast.as_v1().expect("should be a V1 AST");
2197        let tasks: Vec<_> = ast.tasks().collect();
2198        assert_eq!(tasks.len(), 1);
2199
2200        let command = tasks[0].command().expect("should have a command section");
2201
2202        let stripped = command.strip_whitespace().unwrap();
2203        assert_eq!(stripped.len(), 7);
2204        let text = match &stripped[0] {
2205            StrippedCommandPart::Text(text) => text,
2206            _ => panic!("expected text"),
2207        };
2208        assert_eq!(text, "      # other weird whitspace\n");
2209
2210        let _placeholder = match &stripped[1] {
2211            StrippedCommandPart::Placeholder(p) => p,
2212            _ => panic!("expected placeholder"),
2213        };
2214        // not testing anything with the placeholder, just making sure it's there
2215
2216        let text = match &stripped[2] {
2217            StrippedCommandPart::Text(text) => text,
2218            _ => panic!("expected text"),
2219        };
2220        assert_eq!(text, " \"$trailing_pholder\" ");
2221
2222        let _placeholder = match &stripped[3] {
2223            StrippedCommandPart::Placeholder(p) => p,
2224            _ => panic!("expected placeholder"),
2225        };
2226        // not testing anything with the placeholder, just making sure it's there
2227
2228        let text = match &stripped[4] {
2229            StrippedCommandPart::Text(text) => text,
2230            _ => panic!("expected text"),
2231        };
2232        assert_eq!(text, "\n");
2233
2234        let _placeholder = match &stripped[5] {
2235            StrippedCommandPart::Placeholder(p) => p,
2236            _ => panic!("expected placeholder"),
2237        };
2238        // not testing anything with the placeholder, just making sure it's there
2239
2240        let text = match &stripped[6] {
2241            StrippedCommandPart::Text(text) => text,
2242            _ => panic!("expected text"),
2243        };
2244        assert_eq!(text, " somecommand.py \"$leading_pholder\"");
2245    }
2246
2247    #[test]
2248    fn whitespace_stripping_when_command_is_empty() {
2249        let (document, diagnostics) = Document::parse(
2250            r#"
2251version 1.2
2252
2253task test {
2254    command <<<>>>
2255}
2256    "#,
2257        );
2258
2259        assert!(diagnostics.is_empty());
2260        let ast = document.ast();
2261        let ast = ast.as_v1().expect("should be a V1 AST");
2262        let tasks: Vec<_> = ast.tasks().collect();
2263        assert_eq!(tasks.len(), 1);
2264
2265        let command = tasks[0].command().expect("should have a command section");
2266
2267        let stripped = command.strip_whitespace().unwrap();
2268        assert_eq!(stripped.len(), 0);
2269    }
2270
2271    #[test]
2272    fn whitespace_stripping_when_command_is_one_line_of_whitespace() {
2273        let (document, diagnostics) = Document::parse(
2274            r#"
2275version 1.2
2276
2277task test {
2278    command <<<     >>>
2279}
2280    "#,
2281        );
2282
2283        assert!(diagnostics.is_empty());
2284        let ast = document.ast();
2285        let ast = ast.as_v1().expect("should be a V1 AST");
2286        let tasks: Vec<_> = ast.tasks().collect();
2287        assert_eq!(tasks.len(), 1);
2288
2289        let command = tasks[0].command().expect("should have a command section");
2290
2291        let stripped = command.strip_whitespace().unwrap();
2292        assert_eq!(stripped.len(), 1);
2293        let text = match &stripped[0] {
2294            StrippedCommandPart::Text(text) => text,
2295            _ => panic!("expected text"),
2296        };
2297        assert_eq!(text, "");
2298    }
2299
2300    #[test]
2301    fn whitespace_stripping_when_command_is_one_newline() {
2302        let (document, diagnostics) = Document::parse(
2303            r#"
2304version 1.2
2305
2306task test {
2307    command <<<
2308    >>>
2309}
2310    "#,
2311        );
2312
2313        assert!(diagnostics.is_empty());
2314        let ast = document.ast();
2315        let ast = ast.as_v1().expect("should be a V1 AST");
2316        let tasks: Vec<_> = ast.tasks().collect();
2317        assert_eq!(tasks.len(), 1);
2318
2319        let command = tasks[0].command().expect("should have a command section");
2320
2321        let stripped = command.strip_whitespace().unwrap();
2322        assert_eq!(stripped.len(), 1);
2323        let text = match &stripped[0] {
2324            StrippedCommandPart::Text(text) => text,
2325            _ => panic!("expected text"),
2326        };
2327        assert_eq!(text, "");
2328    }
2329
2330    #[test]
2331    fn whitespace_stripping_when_command_is_a_blank_line() {
2332        let (document, diagnostics) = Document::parse(
2333            r#"
2334version 1.2
2335
2336task test {
2337    command <<<
2338
2339    >>>
2340}
2341    "#,
2342        );
2343
2344        assert!(diagnostics.is_empty());
2345        let ast = document.ast();
2346        let ast = ast.as_v1().expect("should be a V1 AST");
2347        let tasks: Vec<_> = ast.tasks().collect();
2348        assert_eq!(tasks.len(), 1);
2349
2350        let command = tasks[0].command().expect("should have a command section");
2351
2352        let stripped = command.strip_whitespace().unwrap();
2353        assert_eq!(stripped.len(), 1);
2354        let text = match &stripped[0] {
2355            StrippedCommandPart::Text(text) => text,
2356            _ => panic!("expected text"),
2357        };
2358        assert_eq!(text, "");
2359    }
2360
2361    #[test]
2362    fn whitespace_stripping_when_command_is_a_blank_line_with_spaces() {
2363        let (document, diagnostics) = Document::parse(
2364            r#"
2365version 1.2
2366
2367task test {
2368    command <<<
2369    
2370    >>>
2371}
2372    "#,
2373        );
2374
2375        assert!(diagnostics.is_empty());
2376        let ast = document.ast();
2377        let ast = ast.as_v1().expect("should be a V1 AST");
2378        let tasks: Vec<_> = ast.tasks().collect();
2379        assert_eq!(tasks.len(), 1);
2380
2381        let command = tasks[0].command().expect("should have a command section");
2382
2383        let stripped = command.strip_whitespace().unwrap();
2384        assert_eq!(stripped.len(), 1);
2385        let text = match &stripped[0] {
2386            StrippedCommandPart::Text(text) => text,
2387            _ => panic!("expected text"),
2388        };
2389        assert_eq!(text, "    ");
2390    }
2391
2392    #[test]
2393    fn whitespace_stripping_with_mixed_indentation() {
2394        let (document, diagnostics) = Document::parse(
2395            r#"
2396version 1.2
2397
2398task test {
2399    command <<<
2400        echo "hello"
2401			echo "world"
2402        echo \
2403            "goodbye"
2404    >>>
2405        }"#,
2406        );
2407
2408        assert!(diagnostics.is_empty());
2409        let ast = document.ast();
2410        let ast = ast.as_v1().expect("should be a V1 AST");
2411        let tasks: Vec<_> = ast.tasks().collect();
2412        assert_eq!(tasks.len(), 1);
2413
2414        let command = tasks[0].command().expect("should have a command section");
2415
2416        let stripped = command.strip_whitespace();
2417        assert!(stripped.is_none());
2418    }
2419
2420    #[test]
2421    fn whitespace_stripping_with_funky_indentation() {
2422        let (document, diagnostics) = Document::parse(
2423            r#"
2424version 1.2
2425
2426task test {
2427    command <<<
2428    echo "hello"
2429        echo "world"
2430    echo \
2431            "goodbye"
2432                >>>
2433        }"#,
2434        );
2435
2436        assert!(diagnostics.is_empty());
2437        let ast = document.ast();
2438        let ast = ast.as_v1().expect("should be a V1 AST");
2439        let tasks: Vec<_> = ast.tasks().collect();
2440        assert_eq!(tasks.len(), 1);
2441
2442        let command = tasks[0].command().expect("should have a command section");
2443
2444        let stripped = command.strip_whitespace().unwrap();
2445        assert_eq!(stripped.len(), 1);
2446        let text = match &stripped[0] {
2447            StrippedCommandPart::Text(text) => text,
2448            _ => panic!("expected text"),
2449        };
2450        assert_eq!(
2451            text,
2452            "echo \"hello\"\n    echo \"world\"\necho \\\n        \"goodbye\""
2453        );
2454    }
2455
2456    /// Regression test for issue [#268](https://github.com/stjude-rust-labs/wdl/issues/268).
2457    #[test]
2458    fn whitespace_stripping_with_content_on_first_line() {
2459        let (document, diagnostics) = Document::parse(
2460            r#"
2461version 1.2
2462
2463task test {
2464    command <<<      weird stuff $firstlinelint
2465            # other weird whitespace
2466      somecommand.py $line120 ~{placeholder}
2467    >>>
2468        }"#,
2469        );
2470
2471        assert!(diagnostics.is_empty());
2472        let ast = document.ast();
2473        let ast = ast.as_v1().expect("should be a V1 AST");
2474        let tasks: Vec<_> = ast.tasks().collect();
2475        assert_eq!(tasks.len(), 1);
2476
2477        let command = tasks[0].command().expect("should have a command section");
2478
2479        let stripped = command.strip_whitespace().unwrap();
2480        assert_eq!(stripped.len(), 3);
2481        let text = match &stripped[0] {
2482            StrippedCommandPart::Text(text) => text,
2483            _ => panic!("expected text"),
2484        };
2485        assert_eq!(
2486            text,
2487            "weird stuff $firstlinelint\n      # other weird whitespace\nsomecommand.py $line120 "
2488        );
2489
2490        let _placeholder = match &stripped[1] {
2491            StrippedCommandPart::Placeholder(p) => p,
2492            _ => panic!("expected placeholder"),
2493        };
2494        // not testing anything with the placeholder, just making sure it's there
2495
2496        let text = match &stripped[2] {
2497            StrippedCommandPart::Text(text) => text,
2498            _ => panic!("expected text"),
2499        };
2500        assert_eq!(text, "");
2501    }
2502
2503    #[test]
2504    fn whitespace_stripping_on_windows() {
2505        let (document, diagnostics) = Document::parse(
2506            "version 1.2\r\ntask test {\r\n    command <<<\r\n        echo \"hello\"\r\n    \
2507             >>>\r\n}\r\n",
2508        );
2509
2510        assert!(diagnostics.is_empty());
2511        let ast = document.ast();
2512        let ast = ast.as_v1().expect("should be a V1 AST");
2513        let tasks: Vec<_> = ast.tasks().collect();
2514        assert_eq!(tasks.len(), 1);
2515
2516        let command = tasks[0].command().expect("should have a command section");
2517        let stripped = command.strip_whitespace().unwrap();
2518        assert_eq!(stripped.len(), 1);
2519        let text = match &stripped[0] {
2520            StrippedCommandPart::Text(text) => text,
2521            _ => panic!("expected text"),
2522        };
2523        assert_eq!(text, "echo \"hello\"");
2524    }
2525}