Skip to main content

wdl_ast/v1/
task.rs

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