wdl_ast/v1/
task.rs

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