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    /// Counts the leading whitespace of the command.
1022    ///
1023    /// If the command has mixed indentation, this will return None.
1024    pub fn count_whitespace(&self) -> Option<usize> {
1025        let mut min_leading_spaces = usize::MAX;
1026        let mut min_leading_tabs = usize::MAX;
1027        let mut parsing_leading_whitespace = false; // init to false so that the first line is skipped
1028
1029        let mut leading_spaces = 0;
1030        let mut leading_tabs = 0;
1031        for part in self.parts() {
1032            match part {
1033                CommandPart::Text(text) => {
1034                    for c in text.text().chars() {
1035                        match c {
1036                            ' ' if parsing_leading_whitespace => {
1037                                leading_spaces += 1;
1038                            }
1039                            '\t' if parsing_leading_whitespace => {
1040                                leading_tabs += 1;
1041                            }
1042                            '\n' => {
1043                                parsing_leading_whitespace = true;
1044                                leading_spaces = 0;
1045                                leading_tabs = 0;
1046                            }
1047                            '\r' => {}
1048                            _ => {
1049                                if parsing_leading_whitespace {
1050                                    parsing_leading_whitespace = false;
1051                                    if leading_spaces == 0 && leading_tabs == 0 {
1052                                        min_leading_spaces = 0;
1053                                        min_leading_tabs = 0;
1054                                        continue;
1055                                    }
1056                                    if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1057                                        min_leading_spaces = leading_spaces;
1058                                    }
1059                                    if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1060                                        min_leading_tabs = leading_tabs;
1061                                    }
1062                                }
1063                            }
1064                        }
1065                    }
1066                    // The last line is intentionally skipped.
1067                }
1068                CommandPart::Placeholder(_) => {
1069                    if parsing_leading_whitespace {
1070                        parsing_leading_whitespace = false;
1071                        if leading_spaces == 0 && leading_tabs == 0 {
1072                            min_leading_spaces = 0;
1073                            min_leading_tabs = 0;
1074                            continue;
1075                        }
1076                        if leading_spaces < min_leading_spaces && leading_spaces > 0 {
1077                            min_leading_spaces = leading_spaces;
1078                        }
1079                        if leading_tabs < min_leading_tabs && leading_tabs > 0 {
1080                            min_leading_tabs = leading_tabs;
1081                        }
1082                    }
1083                }
1084            }
1085        }
1086
1087        // Check for no indentation or all whitespace, in which case we're done
1088        if (min_leading_spaces == 0 && min_leading_tabs == 0)
1089            || (min_leading_spaces == usize::MAX && min_leading_tabs == usize::MAX)
1090        {
1091            return Some(0);
1092        }
1093
1094        // Check for mixed indentation
1095        if (min_leading_spaces > 0 && min_leading_spaces != usize::MAX)
1096            && (min_leading_tabs > 0 && min_leading_tabs != usize::MAX)
1097        {
1098            return None;
1099        }
1100
1101        // Exactly one of the two will be equal to usize::MAX because it never appeared.
1102        // The other will be the number of leading spaces or tabs to strip.
1103        let final_leading_whitespace = if min_leading_spaces < min_leading_tabs {
1104            min_leading_spaces
1105        } else {
1106            min_leading_tabs
1107        };
1108
1109        Some(final_leading_whitespace)
1110    }
1111
1112    /// Strips leading whitespace from the command.
1113    ///
1114    /// If the command has mixed indentation, this will return `None`.
1115    pub fn strip_whitespace(&self) -> Option<Vec<StrippedCommandPart<N>>> {
1116        let mut result = Vec::new();
1117        let heredoc = self.is_heredoc();
1118        for part in self.parts() {
1119            match part {
1120                CommandPart::Text(text) => {
1121                    let mut s = String::new();
1122                    unescape_command_text(text.text(), heredoc, &mut s);
1123                    result.push(StrippedCommandPart::Text(s));
1124                }
1125                CommandPart::Placeholder(p) => {
1126                    result.push(StrippedCommandPart::Placeholder(p));
1127                }
1128            }
1129        }
1130
1131        // Trim the first line
1132        let mut whole_first_line_trimmed = false;
1133        if let Some(StrippedCommandPart::Text(text)) = result.first_mut() {
1134            let end_of_first_line = text.find('\n').map(|p| p + 1).unwrap_or(text.len());
1135            let line = &text[..end_of_first_line];
1136            let len = line.len() - line.trim_start().len();
1137            whole_first_line_trimmed = len == line.len();
1138            text.replace_range(..len, "");
1139        }
1140
1141        // Trim the last line
1142        if let Some(StrippedCommandPart::Text(text)) = result.last_mut() {
1143            if let Some(index) = text.rfind(|c| !matches!(c, ' ' | '\t')) {
1144                text.truncate(index + 1);
1145            } else {
1146                text.clear();
1147            }
1148
1149            if text.ends_with('\n') {
1150                text.pop();
1151            }
1152
1153            if text.ends_with('\r') {
1154                text.pop();
1155            }
1156        }
1157
1158        // Return immediately if command contains mixed indentation
1159        let num_stripped_chars = self.count_whitespace()?;
1160
1161        // If there is no leading whitespace, we're done
1162        if num_stripped_chars == 0 {
1163            return Some(result);
1164        }
1165
1166        // Finally, strip the leading whitespace on each line
1167        // This is done in place using the `replace_range` method; the method will
1168        // internally do moves without allocations
1169        let mut strip_leading_whitespace = whole_first_line_trimmed;
1170        for part in &mut result {
1171            match part {
1172                StrippedCommandPart::Text(text) => {
1173                    let mut offset = 0;
1174                    while let Some(next) = text[offset..].find('\n') {
1175                        let next = next + offset;
1176                        if offset > 0 {
1177                            strip_leading_whitespace = true;
1178                        }
1179
1180                        if !strip_leading_whitespace {
1181                            offset = next + 1;
1182                            continue;
1183                        }
1184
1185                        let line = &text[offset..next];
1186                        let line = line.strip_suffix('\r').unwrap_or(line);
1187                        let len = line.len().min(num_stripped_chars);
1188                        text.replace_range(offset..offset + len, "");
1189                        offset = next + 1 - len;
1190                    }
1191
1192                    // Replace any remaining text
1193                    if strip_leading_whitespace || offset > 0 {
1194                        let line = &text[offset..];
1195                        let line = line.strip_suffix('\r').unwrap_or(line);
1196                        let len = line.len().min(num_stripped_chars);
1197                        text.replace_range(offset..offset + len, "");
1198                    }
1199                }
1200                StrippedCommandPart::Placeholder(_) => {
1201                    strip_leading_whitespace = false;
1202                }
1203            }
1204        }
1205
1206        Some(result)
1207    }
1208
1209    /// Gets the parent of the command section.
1210    pub fn parent(&self) -> SectionParent<N> {
1211        SectionParent::cast(self.0.parent().expect("should have a parent"))
1212            .expect("parent should cast")
1213    }
1214}
1215
1216impl<N: TreeNode> AstNode<N> for CommandSection<N> {
1217    fn can_cast(kind: SyntaxKind) -> bool {
1218        kind == SyntaxKind::CommandSectionNode
1219    }
1220
1221    fn cast(inner: N) -> Option<Self> {
1222        match inner.kind() {
1223            SyntaxKind::CommandSectionNode => Some(Self(inner)),
1224            _ => None,
1225        }
1226    }
1227
1228    fn inner(&self) -> &N {
1229        &self.0
1230    }
1231}
1232
1233/// Represents a textual part of a command.
1234#[derive(Clone, Debug, PartialEq, Eq)]
1235pub struct CommandText<T: TreeToken = SyntaxToken>(T);
1236
1237impl<T: TreeToken> CommandText<T> {
1238    /// Unescapes the command text to the given buffer.
1239    ///
1240    /// When `heredoc` is true, only heredoc escape sequences are allowed.
1241    ///
1242    /// Otherwise, brace command sequences are accepted.
1243    pub fn unescape_to(&self, heredoc: bool, buffer: &mut String) {
1244        unescape_command_text(self.text(), heredoc, buffer);
1245    }
1246}
1247
1248impl<T: TreeToken> AstToken<T> for CommandText<T> {
1249    fn can_cast(kind: SyntaxKind) -> bool {
1250        kind == SyntaxKind::LiteralCommandText
1251    }
1252
1253    fn cast(inner: T) -> Option<Self> {
1254        match inner.kind() {
1255            SyntaxKind::LiteralCommandText => Some(Self(inner)),
1256            _ => None,
1257        }
1258    }
1259
1260    fn inner(&self) -> &T {
1261        &self.0
1262    }
1263}
1264
1265/// Represents a part of a command.
1266#[derive(Clone, Debug, PartialEq, Eq)]
1267pub enum CommandPart<N: TreeNode = SyntaxNode> {
1268    /// A textual part of the command.
1269    Text(CommandText<N::Token>),
1270    /// A placeholder encountered in the command.
1271    Placeholder(Placeholder<N>),
1272}
1273
1274impl<N: TreeNode> CommandPart<N> {
1275    /// Unwraps the command part into text.
1276    ///
1277    /// # Panics
1278    ///
1279    /// Panics if the command part is not text.
1280    pub fn unwrap_text(self) -> CommandText<N::Token> {
1281        match self {
1282            Self::Text(text) => text,
1283            _ => panic!("not string text"),
1284        }
1285    }
1286
1287    /// Unwraps the command part into a placeholder.
1288    ///
1289    /// # Panics
1290    ///
1291    /// Panics if the command part is not a placeholder.
1292    pub fn unwrap_placeholder(self) -> Placeholder<N> {
1293        match self {
1294            Self::Placeholder(p) => p,
1295            _ => panic!("not a placeholder"),
1296        }
1297    }
1298
1299    /// Casts the given [`NodeOrToken`] to [`CommandPart`].
1300    ///
1301    /// Returns `None` if it cannot case cannot be cast.
1302    fn cast(element: NodeOrToken<N, N::Token>) -> Option<Self> {
1303        match element {
1304            NodeOrToken::Node(n) => Some(Self::Placeholder(Placeholder::cast(n)?)),
1305            NodeOrToken::Token(t) => Some(Self::Text(CommandText::cast(t)?)),
1306        }
1307    }
1308}
1309
1310/// Represents a requirements section in a task definition.
1311#[derive(Clone, Debug, PartialEq, Eq)]
1312pub struct RequirementsSection<N: TreeNode = SyntaxNode>(N);
1313
1314impl<N: TreeNode> RequirementsSection<N> {
1315    /// Gets the items in the requirements section.
1316    pub fn items(&self) -> impl Iterator<Item = RequirementsItem<N>> + use<'_, N> {
1317        self.children()
1318    }
1319
1320    /// Gets the parent of the requirements section.
1321    pub fn parent(&self) -> SectionParent<N> {
1322        SectionParent::cast(self.0.parent().expect("should have a parent"))
1323            .expect("parent should cast")
1324    }
1325
1326    /// Gets the `container` item as a
1327    /// [`Container`](requirements::item::Container) (if it exists).
1328    pub fn container(&self) -> Option<requirements::item::Container<N>> {
1329        // NOTE: validation should ensure that, at most, one `container` item exists in
1330        // the `requirements` section.
1331        self.child()
1332    }
1333}
1334
1335impl<N: TreeNode> AstNode<N> for RequirementsSection<N> {
1336    fn can_cast(kind: SyntaxKind) -> bool {
1337        kind == SyntaxKind::RequirementsSectionNode
1338    }
1339
1340    fn cast(inner: N) -> Option<Self> {
1341        match inner.kind() {
1342            SyntaxKind::RequirementsSectionNode => Some(Self(inner)),
1343            _ => None,
1344        }
1345    }
1346
1347    fn inner(&self) -> &N {
1348        &self.0
1349    }
1350}
1351
1352/// Represents an item in a requirements section.
1353#[derive(Clone, Debug, PartialEq, Eq)]
1354pub struct RequirementsItem<N: TreeNode = SyntaxNode>(N);
1355
1356impl<N: TreeNode> RequirementsItem<N> {
1357    /// Gets the name of the requirements item.
1358    pub fn name(&self) -> Ident<N::Token> {
1359        self.token().expect("expected an item name")
1360    }
1361
1362    /// Gets the expression of the requirements item.
1363    pub fn expr(&self) -> Expr<N> {
1364        Expr::child(&self.0).expect("expected an item expression")
1365    }
1366
1367    /// Consumes `self` and attempts to cast the requirements item to a
1368    /// [`Container`](requirements::item::Container).
1369    pub fn into_container(self) -> Option<requirements::item::Container<N>> {
1370        requirements::item::Container::try_from(self).ok()
1371    }
1372}
1373
1374impl<N: TreeNode> AstNode<N> for RequirementsItem<N> {
1375    fn can_cast(kind: SyntaxKind) -> bool {
1376        kind == SyntaxKind::RequirementsItemNode
1377    }
1378
1379    fn cast(inner: N) -> Option<Self> {
1380        match inner.kind() {
1381            SyntaxKind::RequirementsItemNode => Some(Self(inner)),
1382            _ => None,
1383        }
1384    }
1385
1386    fn inner(&self) -> &N {
1387        &self.0
1388    }
1389}
1390
1391/// Represents a hints section in a task definition.
1392#[derive(Clone, Debug, PartialEq, Eq)]
1393pub struct TaskHintsSection<N: TreeNode = SyntaxNode>(N);
1394
1395impl<N: TreeNode> TaskHintsSection<N> {
1396    /// Gets the items in the hints section.
1397    pub fn items(&self) -> impl Iterator<Item = TaskHintsItem<N>> + use<'_, N> {
1398        self.children()
1399    }
1400
1401    /// Gets the parent of the hints section.
1402    pub fn parent(&self) -> TaskDefinition<N> {
1403        TaskDefinition::cast(self.0.parent().expect("should have a parent"))
1404            .expect("parent should cast")
1405    }
1406}
1407
1408impl<N: TreeNode> AstNode<N> for TaskHintsSection<N> {
1409    fn can_cast(kind: SyntaxKind) -> bool {
1410        kind == SyntaxKind::TaskHintsSectionNode
1411    }
1412
1413    fn cast(inner: N) -> Option<Self> {
1414        match inner.kind() {
1415            SyntaxKind::TaskHintsSectionNode => Some(Self(inner)),
1416            _ => None,
1417        }
1418    }
1419
1420    fn inner(&self) -> &N {
1421        &self.0
1422    }
1423}
1424
1425/// Represents an item in a task hints section.
1426#[derive(Clone, Debug, PartialEq, Eq)]
1427pub struct TaskHintsItem<N: TreeNode = SyntaxNode>(N);
1428
1429impl<N: TreeNode> TaskHintsItem<N> {
1430    /// Gets the name of the hints item.
1431    pub fn name(&self) -> Ident<N::Token> {
1432        self.token().expect("expected an item name")
1433    }
1434
1435    /// Gets the expression of the hints item.
1436    pub fn expr(&self) -> Expr<N> {
1437        Expr::child(&self.0).expect("expected an item expression")
1438    }
1439}
1440
1441impl<N: TreeNode> AstNode<N> for TaskHintsItem<N> {
1442    fn can_cast(kind: SyntaxKind) -> bool {
1443        kind == SyntaxKind::TaskHintsItemNode
1444    }
1445
1446    fn cast(inner: N) -> Option<Self> {
1447        match inner.kind() {
1448            SyntaxKind::TaskHintsItemNode => Some(Self(inner)),
1449            _ => None,
1450        }
1451    }
1452
1453    fn inner(&self) -> &N {
1454        &self.0
1455    }
1456}
1457
1458/// Represents a runtime section in a task definition.
1459#[derive(Clone, Debug, PartialEq, Eq)]
1460pub struct RuntimeSection<N: TreeNode = SyntaxNode>(N);
1461
1462impl<N: TreeNode> RuntimeSection<N> {
1463    /// Gets the items in the runtime section.
1464    pub fn items(&self) -> impl Iterator<Item = RuntimeItem<N>> + use<'_, N> {
1465        self.children()
1466    }
1467
1468    /// Gets the parent of the runtime section.
1469    pub fn parent(&self) -> SectionParent<N> {
1470        SectionParent::cast(self.0.parent().expect("should have a parent"))
1471            .expect("parent should cast")
1472    }
1473
1474    /// Gets the `container` item as a [`Container`](runtime::item::Container)
1475    /// (if it exists).
1476    pub fn container(&self) -> Option<runtime::item::Container<N>> {
1477        // NOTE: validation should ensure that, at most, one `container`/`docker` item
1478        // exists in the `runtime` section.
1479        self.child()
1480    }
1481}
1482
1483impl<N: TreeNode> AstNode<N> for RuntimeSection<N> {
1484    fn can_cast(kind: SyntaxKind) -> bool {
1485        kind == SyntaxKind::RuntimeSectionNode
1486    }
1487
1488    fn cast(inner: N) -> Option<Self> {
1489        match inner.kind() {
1490            SyntaxKind::RuntimeSectionNode => Some(Self(inner)),
1491            _ => None,
1492        }
1493    }
1494
1495    fn inner(&self) -> &N {
1496        &self.0
1497    }
1498}
1499
1500/// Represents an item in a runtime section.
1501#[derive(Clone, Debug, PartialEq, Eq)]
1502pub struct RuntimeItem<N: TreeNode = SyntaxNode>(N);
1503
1504impl<N: TreeNode> RuntimeItem<N> {
1505    /// Gets the name of the runtime item.
1506    pub fn name(&self) -> Ident<N::Token> {
1507        self.token().expect("expected an item name")
1508    }
1509
1510    /// Gets the expression of the runtime item.
1511    pub fn expr(&self) -> Expr<N> {
1512        Expr::child(&self.0).expect("expected an item expression")
1513    }
1514
1515    /// Consumes `self` and attempts to cast the runtime item to a
1516    /// [`Container`](runtime::item::Container).
1517    pub fn into_container(self) -> Option<runtime::item::Container<N>> {
1518        runtime::item::Container::try_from(self).ok()
1519    }
1520}
1521
1522impl<N: TreeNode> AstNode<N> for RuntimeItem<N> {
1523    fn can_cast(kind: SyntaxKind) -> bool {
1524        kind == SyntaxKind::RuntimeItemNode
1525    }
1526
1527    fn cast(inner: N) -> Option<Self> {
1528        match inner.kind() {
1529            SyntaxKind::RuntimeItemNode => Some(Self(inner)),
1530            _ => None,
1531        }
1532    }
1533
1534    fn inner(&self) -> &N {
1535        &self.0
1536    }
1537}
1538
1539/// Represents a metadata section in a task or workflow definition.
1540#[derive(Clone, Debug, PartialEq, Eq)]
1541pub struct MetadataSection<N: TreeNode = SyntaxNode>(N);
1542
1543impl<N: TreeNode> MetadataSection<N> {
1544    /// Gets the items of the metadata section.
1545    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1546        self.children()
1547    }
1548
1549    /// Gets the parent of the metadata section.
1550    pub fn parent(&self) -> SectionParent<N> {
1551        SectionParent::cast(self.0.parent().expect("should have a parent"))
1552            .expect("parent should cast")
1553    }
1554}
1555
1556impl<N: TreeNode> AstNode<N> for MetadataSection<N> {
1557    fn can_cast(kind: SyntaxKind) -> bool {
1558        kind == SyntaxKind::MetadataSectionNode
1559    }
1560
1561    fn cast(inner: N) -> Option<Self> {
1562        match inner.kind() {
1563            SyntaxKind::MetadataSectionNode => Some(Self(inner)),
1564            _ => None,
1565        }
1566    }
1567
1568    fn inner(&self) -> &N {
1569        &self.0
1570    }
1571}
1572
1573/// Represents a metadata object item.
1574#[derive(Clone, Debug, PartialEq, Eq)]
1575pub struct MetadataObjectItem<N: TreeNode = SyntaxNode>(N);
1576
1577impl<N: TreeNode> MetadataObjectItem<N> {
1578    /// Gets the name of the item.
1579    pub fn name(&self) -> Ident<N::Token> {
1580        self.token().expect("expected a name")
1581    }
1582
1583    /// Gets the value of the item.
1584    pub fn value(&self) -> MetadataValue<N> {
1585        self.child().expect("expected a value")
1586    }
1587}
1588
1589impl<N: TreeNode> AstNode<N> for MetadataObjectItem<N> {
1590    fn can_cast(kind: SyntaxKind) -> bool {
1591        kind == SyntaxKind::MetadataObjectItemNode
1592    }
1593
1594    fn cast(inner: N) -> Option<Self> {
1595        match inner.kind() {
1596            SyntaxKind::MetadataObjectItemNode => Some(Self(inner)),
1597            _ => None,
1598        }
1599    }
1600
1601    fn inner(&self) -> &N {
1602        &self.0
1603    }
1604}
1605
1606/// Represents a metadata value.
1607#[derive(Clone, Debug, PartialEq, Eq)]
1608pub enum MetadataValue<N: TreeNode = SyntaxNode> {
1609    /// The value is a literal boolean.
1610    Boolean(LiteralBoolean<N>),
1611    /// The value is a literal integer.
1612    Integer(LiteralInteger<N>),
1613    /// The value is a literal float.
1614    Float(LiteralFloat<N>),
1615    /// The value is a literal string.
1616    String(LiteralString<N>),
1617    /// The value is a literal null.
1618    Null(LiteralNull<N>),
1619    /// The value is a metadata object.
1620    Object(MetadataObject<N>),
1621    /// The value is a metadata array.
1622    Array(MetadataArray<N>),
1623}
1624
1625impl<N: TreeNode> MetadataValue<N> {
1626    /// Unwraps the metadata value into a boolean.
1627    ///
1628    /// # Panics
1629    ///
1630    /// Panics if the metadata value is not a boolean.
1631    pub fn unwrap_boolean(self) -> LiteralBoolean<N> {
1632        match self {
1633            Self::Boolean(b) => b,
1634            _ => panic!("not a boolean"),
1635        }
1636    }
1637
1638    /// Unwraps the metadata value into an integer.
1639    ///
1640    /// # Panics
1641    ///
1642    /// Panics if the metadata value is not an integer.
1643    pub fn unwrap_integer(self) -> LiteralInteger<N> {
1644        match self {
1645            Self::Integer(i) => i,
1646            _ => panic!("not an integer"),
1647        }
1648    }
1649
1650    /// Unwraps the metadata value into a float.
1651    ///
1652    /// # Panics
1653    ///
1654    /// Panics if the metadata value is not a float.
1655    pub fn unwrap_float(self) -> LiteralFloat<N> {
1656        match self {
1657            Self::Float(f) => f,
1658            _ => panic!("not a float"),
1659        }
1660    }
1661
1662    /// Unwraps the metadata value into a string.
1663    ///
1664    /// # Panics
1665    ///
1666    /// Panics if the metadata value is not a string.
1667    pub fn unwrap_string(self) -> LiteralString<N> {
1668        match self {
1669            Self::String(s) => s,
1670            _ => panic!("not a string"),
1671        }
1672    }
1673
1674    /// Unwraps the metadata value into a null.
1675    ///
1676    /// # Panics
1677    ///
1678    /// Panics if the metadata value is not a null.
1679    pub fn unwrap_null(self) -> LiteralNull<N> {
1680        match self {
1681            Self::Null(n) => n,
1682            _ => panic!("not a null"),
1683        }
1684    }
1685
1686    /// Unwraps the metadata value into an object.
1687    ///
1688    /// # Panics
1689    ///
1690    /// Panics if the metadata value is not an object.
1691    pub fn unwrap_object(self) -> MetadataObject<N> {
1692        match self {
1693            Self::Object(o) => o,
1694            _ => panic!("not an object"),
1695        }
1696    }
1697
1698    /// Unwraps the metadata value into an array.
1699    ///
1700    /// # Panics
1701    ///
1702    /// Panics if the metadata value is not an array.
1703    pub fn unwrap_array(self) -> MetadataArray<N> {
1704        match self {
1705            Self::Array(a) => a,
1706            _ => panic!("not an array"),
1707        }
1708    }
1709}
1710
1711impl<N: TreeNode> AstNode<N> for MetadataValue<N> {
1712    fn can_cast(kind: SyntaxKind) -> bool {
1713        matches!(
1714            kind,
1715            SyntaxKind::LiteralBooleanNode
1716                | SyntaxKind::LiteralIntegerNode
1717                | SyntaxKind::LiteralFloatNode
1718                | SyntaxKind::LiteralStringNode
1719                | SyntaxKind::LiteralNullNode
1720                | SyntaxKind::MetadataObjectNode
1721                | SyntaxKind::MetadataArrayNode
1722        )
1723    }
1724
1725    fn cast(inner: N) -> Option<Self> {
1726        match inner.kind() {
1727            SyntaxKind::LiteralBooleanNode => Some(Self::Boolean(LiteralBoolean(inner))),
1728            SyntaxKind::LiteralIntegerNode => Some(Self::Integer(LiteralInteger(inner))),
1729            SyntaxKind::LiteralFloatNode => Some(Self::Float(LiteralFloat(inner))),
1730            SyntaxKind::LiteralStringNode => Some(Self::String(LiteralString(inner))),
1731            SyntaxKind::LiteralNullNode => Some(Self::Null(LiteralNull(inner))),
1732            SyntaxKind::MetadataObjectNode => Some(Self::Object(MetadataObject(inner))),
1733            SyntaxKind::MetadataArrayNode => Some(Self::Array(MetadataArray(inner))),
1734            _ => None,
1735        }
1736    }
1737
1738    fn inner(&self) -> &N {
1739        match self {
1740            Self::Boolean(b) => &b.0,
1741            Self::Integer(i) => &i.0,
1742            Self::Float(f) => &f.0,
1743            Self::String(s) => &s.0,
1744            Self::Null(n) => &n.0,
1745            Self::Object(o) => &o.0,
1746            Self::Array(a) => &a.0,
1747        }
1748    }
1749}
1750
1751/// Represents a literal null.
1752#[derive(Clone, Debug, PartialEq, Eq)]
1753pub struct LiteralNull<N: TreeNode = SyntaxNode>(N);
1754
1755impl<N: TreeNode> AstNode<N> for LiteralNull<N> {
1756    fn can_cast(kind: SyntaxKind) -> bool {
1757        kind == SyntaxKind::LiteralNullNode
1758    }
1759
1760    fn cast(inner: N) -> Option<Self> {
1761        match inner.kind() {
1762            SyntaxKind::LiteralNullNode => Some(Self(inner)),
1763            _ => None,
1764        }
1765    }
1766
1767    fn inner(&self) -> &N {
1768        &self.0
1769    }
1770}
1771
1772/// Represents a metadata object.
1773#[derive(Clone, Debug, PartialEq, Eq)]
1774pub struct MetadataObject<N: TreeNode = SyntaxNode>(N);
1775
1776impl<N: TreeNode> MetadataObject<N> {
1777    /// Gets the items of the metadata object.
1778    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1779        self.children()
1780    }
1781}
1782
1783impl<N: TreeNode> AstNode<N> for MetadataObject<N> {
1784    fn can_cast(kind: SyntaxKind) -> bool {
1785        kind == SyntaxKind::MetadataObjectNode
1786    }
1787
1788    fn cast(inner: N) -> Option<Self> {
1789        match inner.kind() {
1790            SyntaxKind::MetadataObjectNode => Some(Self(inner)),
1791            _ => None,
1792        }
1793    }
1794
1795    fn inner(&self) -> &N {
1796        &self.0
1797    }
1798}
1799
1800/// Represents a metadata array.
1801#[derive(Clone, Debug, PartialEq, Eq)]
1802pub struct MetadataArray<N: TreeNode = SyntaxNode>(N);
1803
1804impl<N: TreeNode> MetadataArray<N> {
1805    /// Gets the elements of the metadata array.
1806    pub fn elements(&self) -> impl Iterator<Item = MetadataValue<N>> + use<'_, N> {
1807        self.children()
1808    }
1809}
1810
1811impl<N: TreeNode> AstNode<N> for MetadataArray<N> {
1812    fn can_cast(kind: SyntaxKind) -> bool {
1813        kind == SyntaxKind::MetadataArrayNode
1814    }
1815
1816    fn cast(inner: N) -> Option<Self> {
1817        match inner.kind() {
1818            SyntaxKind::MetadataArrayNode => Some(Self(inner)),
1819            _ => None,
1820        }
1821    }
1822
1823    fn inner(&self) -> &N {
1824        &self.0
1825    }
1826}
1827
1828/// Represents a parameter metadata section in a task or workflow definition.
1829#[derive(Clone, Debug, PartialEq, Eq)]
1830pub struct ParameterMetadataSection<N: TreeNode = SyntaxNode>(N);
1831
1832impl<N: TreeNode> ParameterMetadataSection<N> {
1833    /// Gets the items of the parameter metadata section.
1834    pub fn items(&self) -> impl Iterator<Item = MetadataObjectItem<N>> + use<'_, N> {
1835        self.children()
1836    }
1837
1838    /// Gets the parent of the parameter metadata section.
1839    pub fn parent(&self) -> SectionParent<N> {
1840        SectionParent::cast(self.0.parent().expect("should have a parent"))
1841            .expect("parent should cast")
1842    }
1843}
1844
1845impl<N: TreeNode> AstNode<N> for ParameterMetadataSection<N> {
1846    fn can_cast(kind: SyntaxKind) -> bool {
1847        kind == SyntaxKind::ParameterMetadataSectionNode
1848    }
1849
1850    fn cast(inner: N) -> Option<Self> {
1851        match inner.kind() {
1852            SyntaxKind::ParameterMetadataSectionNode => Some(Self(inner)),
1853            _ => None,
1854        }
1855    }
1856
1857    fn inner(&self) -> &N {
1858        &self.0
1859    }
1860}
1861
1862#[cfg(test)]
1863mod test {
1864    use pretty_assertions::assert_eq;
1865
1866    use super::*;
1867    use crate::Document;
1868
1869    #[test]
1870    fn tasks() {
1871        let (document, diagnostics) = Document::parse(
1872            r#"
1873version 1.2
1874
1875task test {
1876    input {
1877        String name
1878    }
1879
1880    output {
1881        String output = stdout()
1882    }
1883
1884    command <<<
1885        printf "hello, ~{name}!
1886    >>>
1887
1888    requirements {
1889        container: "baz/qux"
1890    }
1891
1892    hints {
1893        foo: "bar"
1894    }
1895
1896    runtime {
1897        container: "foo/bar"
1898    }
1899
1900    meta {
1901        description: "a test"
1902        foo: null
1903    }
1904
1905    parameter_meta {
1906        name: {
1907            help: "a name to greet"
1908        }
1909    }
1910
1911    String x = "private"
1912}
1913"#,
1914        );
1915
1916        assert!(diagnostics.is_empty());
1917        let ast = document.ast();
1918        let ast = ast.as_v1().expect("should be a V1 AST");
1919        let tasks: Vec<_> = ast.tasks().collect();
1920        assert_eq!(tasks.len(), 1);
1921        assert_eq!(tasks[0].name().text(), "test");
1922
1923        // Task input
1924        let input = tasks[0].input().expect("should have an input section");
1925        assert_eq!(input.parent().unwrap_task().name().text(), "test");
1926        let decls: Vec<_> = input.declarations().collect();
1927        assert_eq!(decls.len(), 1);
1928        assert_eq!(
1929            decls[0].clone().unwrap_unbound_decl().ty().to_string(),
1930            "String"
1931        );
1932        assert_eq!(decls[0].clone().unwrap_unbound_decl().name().text(), "name");
1933
1934        // Task output
1935        let output = tasks[0].output().expect("should have an output section");
1936        assert_eq!(output.parent().unwrap_task().name().text(), "test");
1937        let decls: Vec<_> = output.declarations().collect();
1938        assert_eq!(decls.len(), 1);
1939        assert_eq!(decls[0].ty().to_string(), "String");
1940        assert_eq!(decls[0].name().text(), "output");
1941        assert_eq!(decls[0].expr().unwrap_call().target().text(), "stdout");
1942
1943        // Task command
1944        let command = tasks[0].command().expect("should have a command section");
1945        assert_eq!(command.parent().name().text(), "test");
1946        assert!(command.is_heredoc());
1947        let parts: Vec<_> = command.parts().collect();
1948        assert_eq!(parts.len(), 3);
1949        assert_eq!(
1950            parts[0].clone().unwrap_text().text(),
1951            "\n        printf \"hello, "
1952        );
1953        assert_eq!(
1954            parts[1]
1955                .clone()
1956                .unwrap_placeholder()
1957                .expr()
1958                .unwrap_name_ref()
1959                .name()
1960                .text(),
1961            "name"
1962        );
1963        assert_eq!(parts[2].clone().unwrap_text().text(), "!\n    ");
1964
1965        // Task requirements
1966        let requirements = tasks[0]
1967            .requirements()
1968            .expect("should have a requirements section");
1969        assert_eq!(requirements.parent().name().text(), "test");
1970        let items: Vec<_> = requirements.items().collect();
1971        assert_eq!(items.len(), 1);
1972        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
1973        assert_eq!(
1974            items[0]
1975                .expr()
1976                .unwrap_literal()
1977                .unwrap_string()
1978                .text()
1979                .unwrap()
1980                .text(),
1981            "baz/qux"
1982        );
1983
1984        // Task hints
1985        let hints = tasks[0].hints().expect("should have a hints section");
1986        assert_eq!(hints.parent().name().text(), "test");
1987        let items: Vec<_> = hints.items().collect();
1988        assert_eq!(items.len(), 1);
1989        assert_eq!(items[0].name().text(), "foo");
1990        assert_eq!(
1991            items[0]
1992                .expr()
1993                .unwrap_literal()
1994                .unwrap_string()
1995                .text()
1996                .unwrap()
1997                .text(),
1998            "bar"
1999        );
2000
2001        // Task runtimes
2002        let runtime = tasks[0].runtime().expect("should have a runtime section");
2003        assert_eq!(runtime.parent().name().text(), "test");
2004        let items: Vec<_> = runtime.items().collect();
2005        assert_eq!(items.len(), 1);
2006        assert_eq!(items[0].name().text(), TASK_REQUIREMENT_CONTAINER);
2007        assert_eq!(
2008            items[0]
2009                .expr()
2010                .unwrap_literal()
2011                .unwrap_string()
2012                .text()
2013                .unwrap()
2014                .text(),
2015            "foo/bar"
2016        );
2017
2018        // Task metadata
2019        let metadata = tasks[0].metadata().expect("should have a metadata section");
2020        assert_eq!(metadata.parent().unwrap_task().name().text(), "test");
2021        let items: Vec<_> = metadata.items().collect();
2022        assert_eq!(items.len(), 2);
2023        assert_eq!(items[0].name().text(), "description");
2024        assert_eq!(
2025            items[0].value().unwrap_string().text().unwrap().text(),
2026            "a test"
2027        );
2028
2029        // Second metadata
2030        assert_eq!(items[1].name().text(), "foo");
2031        items[1].value().unwrap_null();
2032
2033        // Task parameter metadata
2034        let param_meta = tasks[0]
2035            .parameter_metadata()
2036            .expect("should have a parameter metadata section");
2037        assert_eq!(param_meta.parent().unwrap_task().name().text(), "test");
2038        let items: Vec<_> = param_meta.items().collect();
2039        assert_eq!(items.len(), 1);
2040        assert_eq!(items[0].name().text(), "name");
2041        let items: Vec<_> = items[0].value().unwrap_object().items().collect();
2042        assert_eq!(items.len(), 1);
2043        assert_eq!(items[0].name().text(), "help");
2044        assert_eq!(
2045            items[0].value().unwrap_string().text().unwrap().text(),
2046            "a name to greet"
2047        );
2048
2049        // Task declarations
2050        let decls: Vec<_> = tasks[0].declarations().collect();
2051        assert_eq!(decls.len(), 1);
2052
2053        // First task declaration
2054        assert_eq!(decls[0].ty().to_string(), "String");
2055        assert_eq!(decls[0].name().text(), "x");
2056        assert_eq!(
2057            decls[0]
2058                .expr()
2059                .unwrap_literal()
2060                .unwrap_string()
2061                .text()
2062                .unwrap()
2063                .text(),
2064            "private"
2065        );
2066    }
2067
2068    #[test]
2069    fn whitespace_stripping_without_interpolation() {
2070        let (document, diagnostics) = Document::parse(
2071            r#"
2072version 1.2
2073
2074task test {
2075    command <<<
2076        echo "hello"
2077        echo "world"
2078        echo \
2079            "goodbye"
2080    >>>
2081}
2082"#,
2083        );
2084
2085        assert!(diagnostics.is_empty());
2086        let ast = document.ast();
2087        let ast = ast.as_v1().expect("should be a V1 AST");
2088        let tasks: Vec<_> = ast.tasks().collect();
2089        assert_eq!(tasks.len(), 1);
2090
2091        let command = tasks[0].command().expect("should have a command section");
2092
2093        let stripped = command.strip_whitespace().unwrap();
2094
2095        assert_eq!(stripped.len(), 1);
2096        let text = match &stripped[0] {
2097            StrippedCommandPart::Text(text) => text,
2098            _ => panic!("expected text"),
2099        };
2100        assert_eq!(
2101            text,
2102            "echo \"hello\"\necho \"world\"\necho \\\n    \"goodbye\""
2103        );
2104    }
2105
2106    #[test]
2107    fn whitespace_stripping_with_interpolation() {
2108        let (document, diagnostics) = Document::parse(
2109            r#"
2110version 1.2
2111
2112task test {
2113    input {
2114        String name
2115        Boolean flag
2116    }
2117
2118    command <<<
2119        echo "hello, ~{
2120if flag
2121then name
2122               else "Jerry"
2123    }!"
2124    >>>
2125}
2126    "#,
2127        );
2128
2129        assert!(diagnostics.is_empty());
2130        let ast = document.ast();
2131        let ast = ast.as_v1().expect("should be a V1 AST");
2132        let tasks: Vec<_> = ast.tasks().collect();
2133        assert_eq!(tasks.len(), 1);
2134
2135        let command = tasks[0].command().expect("should have a command section");
2136
2137        let stripped = command.strip_whitespace().unwrap();
2138        assert_eq!(stripped.len(), 3);
2139        let text = match &stripped[0] {
2140            StrippedCommandPart::Text(text) => text,
2141            _ => panic!("expected text"),
2142        };
2143        assert_eq!(text, "echo \"hello, ");
2144
2145        let _placeholder = match &stripped[1] {
2146            StrippedCommandPart::Placeholder(p) => p,
2147            _ => panic!("expected placeholder"),
2148        };
2149        // not testing anything with the placeholder, just making sure it's there
2150
2151        let text = match &stripped[2] {
2152            StrippedCommandPart::Text(text) => text,
2153            _ => panic!("expected text"),
2154        };
2155        assert_eq!(text, "!\"");
2156    }
2157
2158    #[test]
2159    fn whitespace_stripping_when_interpolation_starts_line() {
2160        let (document, diagnostics) = Document::parse(
2161            r#"
2162version 1.2
2163
2164task test {
2165    input {
2166      Int placeholder
2167    }
2168
2169    command <<<
2170            # other weird whitspace
2171      ~{placeholder} "$trailing_pholder" ~{placeholder}
2172      ~{placeholder} somecommand.py "$leading_pholder"
2173    >>>
2174}
2175"#,
2176        );
2177
2178        assert!(diagnostics.is_empty());
2179        let ast = document.ast();
2180        let ast = ast.as_v1().expect("should be a V1 AST");
2181        let tasks: Vec<_> = ast.tasks().collect();
2182        assert_eq!(tasks.len(), 1);
2183
2184        let command = tasks[0].command().expect("should have a command section");
2185
2186        let stripped = command.strip_whitespace().unwrap();
2187        assert_eq!(stripped.len(), 7);
2188        let text = match &stripped[0] {
2189            StrippedCommandPart::Text(text) => text,
2190            _ => panic!("expected text"),
2191        };
2192        assert_eq!(text, "      # other weird whitspace\n");
2193
2194        let _placeholder = match &stripped[1] {
2195            StrippedCommandPart::Placeholder(p) => p,
2196            _ => panic!("expected placeholder"),
2197        };
2198        // not testing anything with the placeholder, just making sure it's there
2199
2200        let text = match &stripped[2] {
2201            StrippedCommandPart::Text(text) => text,
2202            _ => panic!("expected text"),
2203        };
2204        assert_eq!(text, " \"$trailing_pholder\" ");
2205
2206        let _placeholder = match &stripped[3] {
2207            StrippedCommandPart::Placeholder(p) => p,
2208            _ => panic!("expected placeholder"),
2209        };
2210        // not testing anything with the placeholder, just making sure it's there
2211
2212        let text = match &stripped[4] {
2213            StrippedCommandPart::Text(text) => text,
2214            _ => panic!("expected text"),
2215        };
2216        assert_eq!(text, "\n");
2217
2218        let _placeholder = match &stripped[5] {
2219            StrippedCommandPart::Placeholder(p) => p,
2220            _ => panic!("expected placeholder"),
2221        };
2222        // not testing anything with the placeholder, just making sure it's there
2223
2224        let text = match &stripped[6] {
2225            StrippedCommandPart::Text(text) => text,
2226            _ => panic!("expected text"),
2227        };
2228        assert_eq!(text, " somecommand.py \"$leading_pholder\"");
2229    }
2230
2231    #[test]
2232    fn whitespace_stripping_when_command_is_empty() {
2233        let (document, diagnostics) = Document::parse(
2234            r#"
2235version 1.2
2236
2237task test {
2238    command <<<>>>
2239}
2240    "#,
2241        );
2242
2243        assert!(diagnostics.is_empty());
2244        let ast = document.ast();
2245        let ast = ast.as_v1().expect("should be a V1 AST");
2246        let tasks: Vec<_> = ast.tasks().collect();
2247        assert_eq!(tasks.len(), 1);
2248
2249        let command = tasks[0].command().expect("should have a command section");
2250
2251        let stripped = command.strip_whitespace().unwrap();
2252        assert_eq!(stripped.len(), 0);
2253    }
2254
2255    #[test]
2256    fn whitespace_stripping_when_command_is_one_line_of_whitespace() {
2257        let (document, diagnostics) = Document::parse(
2258            r#"
2259version 1.2
2260
2261task test {
2262    command <<<     >>>
2263}
2264    "#,
2265        );
2266
2267        assert!(diagnostics.is_empty());
2268        let ast = document.ast();
2269        let ast = ast.as_v1().expect("should be a V1 AST");
2270        let tasks: Vec<_> = ast.tasks().collect();
2271        assert_eq!(tasks.len(), 1);
2272
2273        let command = tasks[0].command().expect("should have a command section");
2274
2275        let stripped = command.strip_whitespace().unwrap();
2276        assert_eq!(stripped.len(), 1);
2277        let text = match &stripped[0] {
2278            StrippedCommandPart::Text(text) => text,
2279            _ => panic!("expected text"),
2280        };
2281        assert_eq!(text, "");
2282    }
2283
2284    #[test]
2285    fn whitespace_stripping_when_command_is_one_newline() {
2286        let (document, diagnostics) = Document::parse(
2287            r#"
2288version 1.2
2289
2290task test {
2291    command <<<
2292    >>>
2293}
2294    "#,
2295        );
2296
2297        assert!(diagnostics.is_empty());
2298        let ast = document.ast();
2299        let ast = ast.as_v1().expect("should be a V1 AST");
2300        let tasks: Vec<_> = ast.tasks().collect();
2301        assert_eq!(tasks.len(), 1);
2302
2303        let command = tasks[0].command().expect("should have a command section");
2304
2305        let stripped = command.strip_whitespace().unwrap();
2306        assert_eq!(stripped.len(), 1);
2307        let text = match &stripped[0] {
2308            StrippedCommandPart::Text(text) => text,
2309            _ => panic!("expected text"),
2310        };
2311        assert_eq!(text, "");
2312    }
2313
2314    #[test]
2315    fn whitespace_stripping_when_command_is_a_blank_line() {
2316        let (document, diagnostics) = Document::parse(
2317            r#"
2318version 1.2
2319
2320task test {
2321    command <<<
2322
2323    >>>
2324}
2325    "#,
2326        );
2327
2328        assert!(diagnostics.is_empty());
2329        let ast = document.ast();
2330        let ast = ast.as_v1().expect("should be a V1 AST");
2331        let tasks: Vec<_> = ast.tasks().collect();
2332        assert_eq!(tasks.len(), 1);
2333
2334        let command = tasks[0].command().expect("should have a command section");
2335
2336        let stripped = command.strip_whitespace().unwrap();
2337        assert_eq!(stripped.len(), 1);
2338        let text = match &stripped[0] {
2339            StrippedCommandPart::Text(text) => text,
2340            _ => panic!("expected text"),
2341        };
2342        assert_eq!(text, "");
2343    }
2344
2345    #[test]
2346    fn whitespace_stripping_when_command_is_a_blank_line_with_spaces() {
2347        let (document, diagnostics) = Document::parse(
2348            r#"
2349version 1.2
2350
2351task test {
2352    command <<<
2353    
2354    >>>
2355}
2356    "#,
2357        );
2358
2359        assert!(diagnostics.is_empty());
2360        let ast = document.ast();
2361        let ast = ast.as_v1().expect("should be a V1 AST");
2362        let tasks: Vec<_> = ast.tasks().collect();
2363        assert_eq!(tasks.len(), 1);
2364
2365        let command = tasks[0].command().expect("should have a command section");
2366
2367        let stripped = command.strip_whitespace().unwrap();
2368        assert_eq!(stripped.len(), 1);
2369        let text = match &stripped[0] {
2370            StrippedCommandPart::Text(text) => text,
2371            _ => panic!("expected text"),
2372        };
2373        assert_eq!(text, "    ");
2374    }
2375
2376    #[test]
2377    fn whitespace_stripping_with_mixed_indentation() {
2378        let (document, diagnostics) = Document::parse(
2379            r#"
2380version 1.2
2381
2382task test {
2383    command <<<
2384        echo "hello"
2385			echo "world"
2386        echo \
2387            "goodbye"
2388    >>>
2389        }"#,
2390        );
2391
2392        assert!(diagnostics.is_empty());
2393        let ast = document.ast();
2394        let ast = ast.as_v1().expect("should be a V1 AST");
2395        let tasks: Vec<_> = ast.tasks().collect();
2396        assert_eq!(tasks.len(), 1);
2397
2398        let command = tasks[0].command().expect("should have a command section");
2399
2400        let stripped = command.strip_whitespace();
2401        assert!(stripped.is_none());
2402    }
2403
2404    #[test]
2405    fn whitespace_stripping_with_funky_indentation() {
2406        let (document, diagnostics) = Document::parse(
2407            r#"
2408version 1.2
2409
2410task test {
2411    command <<<
2412    echo "hello"
2413        echo "world"
2414    echo \
2415            "goodbye"
2416                >>>
2417        }"#,
2418        );
2419
2420        assert!(diagnostics.is_empty());
2421        let ast = document.ast();
2422        let ast = ast.as_v1().expect("should be a V1 AST");
2423        let tasks: Vec<_> = ast.tasks().collect();
2424        assert_eq!(tasks.len(), 1);
2425
2426        let command = tasks[0].command().expect("should have a command section");
2427
2428        let stripped = command.strip_whitespace().unwrap();
2429        assert_eq!(stripped.len(), 1);
2430        let text = match &stripped[0] {
2431            StrippedCommandPart::Text(text) => text,
2432            _ => panic!("expected text"),
2433        };
2434        assert_eq!(
2435            text,
2436            "echo \"hello\"\n    echo \"world\"\necho \\\n        \"goodbye\""
2437        );
2438    }
2439
2440    /// Regression test for issue [#268](https://github.com/stjude-rust-labs/wdl/issues/268).
2441    #[test]
2442    fn whitespace_stripping_with_content_on_first_line() {
2443        let (document, diagnostics) = Document::parse(
2444            r#"
2445version 1.2
2446
2447task test {
2448    command <<<      weird stuff $firstlinelint
2449            # other weird whitespace
2450      somecommand.py $line120 ~{placeholder}
2451    >>>
2452        }"#,
2453        );
2454
2455        assert!(diagnostics.is_empty());
2456        let ast = document.ast();
2457        let ast = ast.as_v1().expect("should be a V1 AST");
2458        let tasks: Vec<_> = ast.tasks().collect();
2459        assert_eq!(tasks.len(), 1);
2460
2461        let command = tasks[0].command().expect("should have a command section");
2462
2463        let stripped = command.strip_whitespace().unwrap();
2464        assert_eq!(stripped.len(), 3);
2465        let text = match &stripped[0] {
2466            StrippedCommandPart::Text(text) => text,
2467            _ => panic!("expected text"),
2468        };
2469        assert_eq!(
2470            text,
2471            "weird stuff $firstlinelint\n      # other weird whitespace\nsomecommand.py $line120 "
2472        );
2473
2474        let _placeholder = match &stripped[1] {
2475            StrippedCommandPart::Placeholder(p) => p,
2476            _ => panic!("expected placeholder"),
2477        };
2478        // not testing anything with the placeholder, just making sure it's there
2479
2480        let text = match &stripped[2] {
2481            StrippedCommandPart::Text(text) => text,
2482            _ => panic!("expected text"),
2483        };
2484        assert_eq!(text, "");
2485    }
2486
2487    #[test]
2488    fn whitespace_stripping_on_windows() {
2489        let (document, diagnostics) = Document::parse(
2490            "version 1.2\r\ntask test {\r\n    command <<<\r\n        echo \"hello\"\r\n    \
2491             >>>\r\n}\r\n",
2492        );
2493
2494        assert!(diagnostics.is_empty());
2495        let ast = document.ast();
2496        let ast = ast.as_v1().expect("should be a V1 AST");
2497        let tasks: Vec<_> = ast.tasks().collect();
2498        assert_eq!(tasks.len(), 1);
2499
2500        let command = tasks[0].command().expect("should have a command section");
2501        let stripped = command.strip_whitespace().unwrap();
2502        assert_eq!(stripped.len(), 1);
2503        let text = match &stripped[0] {
2504            StrippedCommandPart::Text(text) => text,
2505            _ => panic!("expected text"),
2506        };
2507        assert_eq!(text, "echo \"hello\"");
2508    }
2509}