wdl_analysis/
visitor.rs

1//! Implementation for AST visitation.
2//!
3//! An AST visitor is called when a WDL document is being visited (see
4//! [Document::visit]); callbacks correspond to specific nodes and tokens in the
5//! AST based on [SyntaxKind]. As `SyntaxKind` is the union of nodes and tokens
6//! from _every_ version of WDL, the `Visitor` trait is also the union of
7//! visitation callbacks.
8//!
9//! The [Visitor] trait is not WDL version-specific, meaning that the trait's
10//! methods currently receive V1 representation of AST nodes.
11//!
12//! In the future, a major version change to the WDL specification will
13//! introduce V2 representations for AST nodes that are either brand new or have
14//! changed since V1.
15//!
16//! When this occurs, the `Visitor` trait will be extended to support the new
17//! syntax; however, syntax that has not changed since V1 will continue to use
18//! the V1 AST types.
19//!
20//! That means it is possible to receive callbacks for V1 nodes and tokens when
21//! visiting a V2 document; the hope is that enables some visitors to be
22//! "shared" across different WDL versions.
23
24use rowan::WalkEvent;
25use wdl_ast::AstNode;
26use wdl_ast::AstToken;
27use wdl_ast::Comment;
28use wdl_ast::Document as AstDocument;
29use wdl_ast::SupportedVersion;
30use wdl_ast::SyntaxKind;
31use wdl_ast::SyntaxNode;
32use wdl_ast::VersionStatement;
33use wdl_ast::Whitespace;
34use wdl_ast::v1::BoundDecl;
35use wdl_ast::v1::CallStatement;
36use wdl_ast::v1::CommandSection;
37use wdl_ast::v1::CommandText;
38use wdl_ast::v1::ConditionalStatement;
39use wdl_ast::v1::Expr;
40use wdl_ast::v1::ImportStatement;
41use wdl_ast::v1::InputSection;
42use wdl_ast::v1::MetadataArray;
43use wdl_ast::v1::MetadataObject;
44use wdl_ast::v1::MetadataObjectItem;
45use wdl_ast::v1::MetadataSection;
46use wdl_ast::v1::OutputSection;
47use wdl_ast::v1::ParameterMetadataSection;
48use wdl_ast::v1::Placeholder;
49use wdl_ast::v1::RequirementsSection;
50use wdl_ast::v1::RuntimeItem;
51use wdl_ast::v1::RuntimeSection;
52use wdl_ast::v1::ScatterStatement;
53use wdl_ast::v1::StringText;
54use wdl_ast::v1::StructDefinition;
55use wdl_ast::v1::TaskDefinition;
56use wdl_ast::v1::TaskHintsSection;
57use wdl_ast::v1::UnboundDecl;
58use wdl_ast::v1::WorkflowDefinition;
59use wdl_ast::v1::WorkflowHintsSection;
60
61use crate::Diagnostics;
62use crate::document::Document as AnalysisDocument;
63
64/// Represents the reason an AST node has been visited.
65///
66/// Each node is visited exactly once, but the visitor will receive
67/// a call for entering the node and a call for exiting the node.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
69pub enum VisitReason {
70    /// The visit has entered the node.
71    Enter,
72    /// The visit has exited the node.
73    Exit,
74}
75
76/// A trait used to implement an AST visitor.
77///
78/// Each encountered node will receive a corresponding method call
79/// that receives both a [VisitReason::Enter] call and a
80/// matching [VisitReason::Exit] call.
81#[allow(unused_variables)]
82pub trait Visitor {
83    /// Resets the visitor to its initial state.
84    ///
85    /// A visitor must implement this with resetting any internal state so that
86    /// a visitor may be reused between documents.
87    fn reset(&mut self);
88
89    /// Visits the root document node.
90    fn document(
91        &mut self,
92        diagnostics: &mut Diagnostics,
93        reason: VisitReason,
94        doc: &AnalysisDocument,
95        version: SupportedVersion,
96    ) {
97    }
98
99    /// Visits a whitespace token.
100    fn whitespace(&mut self, diagnostics: &mut Diagnostics, whitespace: &Whitespace) {}
101
102    /// Visit a comment token.
103    fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {}
104
105    /// Visits a top-level version statement node.
106    fn version_statement(
107        &mut self,
108        diagnostics: &mut Diagnostics,
109        reason: VisitReason,
110        stmt: &VersionStatement,
111    ) {
112    }
113
114    /// Visits a top-level import statement node.
115    fn import_statement(
116        &mut self,
117        diagnostics: &mut Diagnostics,
118        reason: VisitReason,
119        stmt: &ImportStatement,
120    ) {
121    }
122
123    /// Visits a struct definition node.
124    fn struct_definition(
125        &mut self,
126        diagnostics: &mut Diagnostics,
127        reason: VisitReason,
128        def: &StructDefinition,
129    ) {
130    }
131
132    /// Visits a task definition node.
133    fn task_definition(
134        &mut self,
135        diagnostics: &mut Diagnostics,
136        reason: VisitReason,
137        task: &TaskDefinition,
138    ) {
139    }
140
141    /// Visits a workflow definition node.
142    fn workflow_definition(
143        &mut self,
144        diagnostics: &mut Diagnostics,
145        reason: VisitReason,
146        workflow: &WorkflowDefinition,
147    ) {
148    }
149
150    /// Visits an input section node.
151    fn input_section(
152        &mut self,
153        diagnostics: &mut Diagnostics,
154        reason: VisitReason,
155        section: &InputSection,
156    ) {
157    }
158
159    /// Visits an output section node.
160    fn output_section(
161        &mut self,
162        diagnostics: &mut Diagnostics,
163        reason: VisitReason,
164        section: &OutputSection,
165    ) {
166    }
167
168    /// Visits a command section node.
169    fn command_section(
170        &mut self,
171        diagnostics: &mut Diagnostics,
172        reason: VisitReason,
173        section: &CommandSection,
174    ) {
175    }
176
177    /// Visits a command text token in a command section node.
178    fn command_text(&mut self, diagnostics: &mut Diagnostics, text: &CommandText) {}
179
180    /// Visits a requirements section node.
181    fn requirements_section(
182        &mut self,
183        diagnostics: &mut Diagnostics,
184        reason: VisitReason,
185        section: &RequirementsSection,
186    ) {
187    }
188
189    /// Visits a task hints section node.
190    fn task_hints_section(
191        &mut self,
192        diagnostics: &mut Diagnostics,
193        reason: VisitReason,
194        section: &TaskHintsSection,
195    ) {
196    }
197
198    /// Visits a workflow hints section node.
199    fn workflow_hints_section(
200        &mut self,
201        diagnostics: &mut Diagnostics,
202        reason: VisitReason,
203        section: &WorkflowHintsSection,
204    ) {
205    }
206
207    /// Visits a runtime section node.
208    fn runtime_section(
209        &mut self,
210        diagnostics: &mut Diagnostics,
211        reason: VisitReason,
212        section: &RuntimeSection,
213    ) {
214    }
215
216    /// Visits a runtime item node.
217    fn runtime_item(
218        &mut self,
219        diagnostics: &mut Diagnostics,
220        reason: VisitReason,
221        item: &RuntimeItem,
222    ) {
223    }
224
225    /// Visits a metadata section node.
226    fn metadata_section(
227        &mut self,
228        diagnostics: &mut Diagnostics,
229        reason: VisitReason,
230        section: &MetadataSection,
231    ) {
232    }
233
234    /// Visits a parameter metadata section node.
235    fn parameter_metadata_section(
236        &mut self,
237        diagnostics: &mut Diagnostics,
238        reason: VisitReason,
239        section: &ParameterMetadataSection,
240    ) {
241    }
242
243    /// Visits a metadata object in a metadata or parameter metadata section.
244    fn metadata_object(
245        &mut self,
246        diagnostics: &mut Diagnostics,
247        reason: VisitReason,
248        object: &MetadataObject,
249    ) {
250    }
251
252    /// Visits a metadata object item in a metadata object.
253    fn metadata_object_item(
254        &mut self,
255        diagnostics: &mut Diagnostics,
256        reason: VisitReason,
257        item: &MetadataObjectItem,
258    ) {
259    }
260
261    /// Visits a metadata array node in a metadata or parameter metadata
262    /// section.
263    fn metadata_array(
264        &mut self,
265        diagnostics: &mut Diagnostics,
266        reason: VisitReason,
267        item: &MetadataArray,
268    ) {
269    }
270
271    /// Visits an unbound declaration node.
272    fn unbound_decl(
273        &mut self,
274        diagnostics: &mut Diagnostics,
275        reason: VisitReason,
276        decl: &UnboundDecl,
277    ) {
278    }
279
280    /// Visits a bound declaration node.
281    fn bound_decl(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, decl: &BoundDecl) {
282    }
283
284    /// Visits an expression node.
285    fn expr(&mut self, diagnostics: &mut Diagnostics, reason: VisitReason, expr: &Expr) {}
286
287    /// Visits a string text token in a literal string node.
288    fn string_text(&mut self, diagnostics: &mut Diagnostics, text: &StringText) {}
289
290    /// Visits a placeholder node.
291    fn placeholder(
292        &mut self,
293        diagnostics: &mut Diagnostics,
294        reason: VisitReason,
295        placeholder: &Placeholder,
296    ) {
297    }
298
299    /// Visits a conditional statement node in a workflow.
300    fn conditional_statement(
301        &mut self,
302        diagnostics: &mut Diagnostics,
303        reason: VisitReason,
304        stmt: &ConditionalStatement,
305    ) {
306    }
307
308    /// Visits a scatter statement node in a workflow.
309    fn scatter_statement(
310        &mut self,
311        diagnostics: &mut Diagnostics,
312        reason: VisitReason,
313        stmt: &ScatterStatement,
314    ) {
315    }
316
317    /// Visits a call statement node in a workflow.
318    fn call_statement(
319        &mut self,
320        diagnostics: &mut Diagnostics,
321        reason: VisitReason,
322        stmt: &CallStatement,
323    ) {
324    }
325}
326
327/// Used to visit each descendant node of the given root in a preorder
328/// traversal.
329pub(crate) fn visit<V: Visitor>(
330    document: &AnalysisDocument,
331    diagnostics: &mut Diagnostics,
332    visitor: &mut V,
333) {
334    for event in document.root().inner().preorder_with_tokens() {
335        let (reason, element) = match event {
336            WalkEvent::Enter(node) => (VisitReason::Enter, node),
337            WalkEvent::Leave(node) => (VisitReason::Exit, node),
338        };
339
340        match element.kind() {
341            SyntaxKind::RootNode => {
342                let ast_document = AstDocument::cast(element.into_node().unwrap())
343                    .expect("root node should be a document");
344
345                let version = ast_document
346                    .version_statement()
347                    .and_then(|s| s.version().text().parse::<SupportedVersion>().ok())
348                    .expect("only WDL documents with supported versions can be visited");
349
350                visitor.document(diagnostics, reason, document, version)
351            }
352            SyntaxKind::VersionStatementNode => visitor.version_statement(
353                diagnostics,
354                reason,
355                &VersionStatement::cast(element.into_node().unwrap()).expect("should cast"),
356            ),
357            SyntaxKind::ImportStatementNode => visitor.import_statement(
358                diagnostics,
359                reason,
360                &ImportStatement::cast(element.into_node().unwrap()).expect("should cast"),
361            ),
362            SyntaxKind::ImportAliasNode => {
363                // Skip these nodes as they're part of an import statement
364            }
365            SyntaxKind::StructDefinitionNode => visitor.struct_definition(
366                diagnostics,
367                reason,
368                &StructDefinition::cast(element.into_node().unwrap()).expect("should cast"),
369            ),
370            SyntaxKind::TaskDefinitionNode => visitor.task_definition(
371                diagnostics,
372                reason,
373                &TaskDefinition::cast(element.into_node().unwrap()).expect("should cast"),
374            ),
375            SyntaxKind::WorkflowDefinitionNode => visitor.workflow_definition(
376                diagnostics,
377                reason,
378                &WorkflowDefinition::cast(element.into_node().unwrap()).expect("should cast"),
379            ),
380            SyntaxKind::UnboundDeclNode => visitor.unbound_decl(
381                diagnostics,
382                reason,
383                &UnboundDecl::cast(element.into_node().unwrap()).expect("should cast"),
384            ),
385            SyntaxKind::BoundDeclNode => visitor.bound_decl(
386                diagnostics,
387                reason,
388                &BoundDecl::cast(element.into_node().unwrap()).expect("should cast"),
389            ),
390            SyntaxKind::PrimitiveTypeNode
391            | SyntaxKind::MapTypeNode
392            | SyntaxKind::ArrayTypeNode
393            | SyntaxKind::PairTypeNode
394            | SyntaxKind::ObjectTypeNode
395            | SyntaxKind::TypeRefNode => {
396                // Skip these nodes as they're part of declarations
397            }
398            SyntaxKind::InputSectionNode => visitor.input_section(
399                diagnostics,
400                reason,
401                &InputSection::cast(element.into_node().unwrap()).expect("should cast"),
402            ),
403            SyntaxKind::OutputSectionNode => visitor.output_section(
404                diagnostics,
405                reason,
406                &OutputSection::cast(element.into_node().unwrap()).expect("should cast"),
407            ),
408            SyntaxKind::CommandSectionNode => visitor.command_section(
409                diagnostics,
410                reason,
411                &CommandSection::cast(element.into_node().unwrap()).expect("should cast"),
412            ),
413            SyntaxKind::RequirementsSectionNode => visitor.requirements_section(
414                diagnostics,
415                reason,
416                &RequirementsSection::cast(element.into_node().unwrap()).expect("should cast"),
417            ),
418            SyntaxKind::TaskHintsSectionNode => visitor.task_hints_section(
419                diagnostics,
420                reason,
421                &TaskHintsSection::cast(element.into_node().unwrap()).expect("should cast"),
422            ),
423            SyntaxKind::WorkflowHintsSectionNode => visitor.workflow_hints_section(
424                diagnostics,
425                reason,
426                &WorkflowHintsSection::cast(element.into_node().unwrap()).expect("should cast"),
427            ),
428            SyntaxKind::TaskHintsItemNode | SyntaxKind::WorkflowHintsItemNode => {
429                // Skip this node as it's part of a hints section
430            }
431            SyntaxKind::RequirementsItemNode => {
432                // Skip this node as it's part of a requirements section
433            }
434            SyntaxKind::RuntimeSectionNode => visitor.runtime_section(
435                diagnostics,
436                reason,
437                &RuntimeSection::cast(element.into_node().unwrap()).expect("should cast"),
438            ),
439            SyntaxKind::RuntimeItemNode => visitor.runtime_item(
440                diagnostics,
441                reason,
442                &RuntimeItem::cast(element.into_node().unwrap()).expect("should cast"),
443            ),
444            SyntaxKind::MetadataSectionNode => visitor.metadata_section(
445                diagnostics,
446                reason,
447                &MetadataSection::cast(element.into_node().unwrap()).expect("should cast"),
448            ),
449            SyntaxKind::ParameterMetadataSectionNode => visitor.parameter_metadata_section(
450                diagnostics,
451                reason,
452                &ParameterMetadataSection::cast(element.into_node().unwrap()).expect("should cast"),
453            ),
454            SyntaxKind::MetadataObjectNode => visitor.metadata_object(
455                diagnostics,
456                reason,
457                &MetadataObject::cast(element.into_node().unwrap()).expect("should cast"),
458            ),
459            SyntaxKind::MetadataObjectItemNode => visitor.metadata_object_item(
460                diagnostics,
461                reason,
462                &MetadataObjectItem::cast(element.into_node().unwrap()).expect("should cast"),
463            ),
464            SyntaxKind::MetadataArrayNode => visitor.metadata_array(
465                diagnostics,
466                reason,
467                &MetadataArray::cast(element.into_node().unwrap()).expect("should cast"),
468            ),
469            SyntaxKind::LiteralNullNode => {
470                // Skip these nodes as they're part of a metadata section
471            }
472            k if Expr::<SyntaxNode>::can_cast(k) => {
473                visitor.expr(
474                    diagnostics,
475                    reason,
476                    &Expr::cast(element.into_node().expect(
477                        "any element that is able to be turned into an expr should be a node",
478                    ))
479                    .expect("expr should be built"),
480                )
481            }
482            SyntaxKind::LiteralMapItemNode
483            | SyntaxKind::LiteralObjectItemNode
484            | SyntaxKind::LiteralStructItemNode
485            | SyntaxKind::LiteralHintsItemNode
486            | SyntaxKind::LiteralInputItemNode
487            | SyntaxKind::LiteralOutputItemNode => {
488                // Skip these nodes as they're part of literal expressions
489            }
490            k @ (SyntaxKind::LiteralIntegerNode
491            | SyntaxKind::LiteralFloatNode
492            | SyntaxKind::LiteralBooleanNode
493            | SyntaxKind::LiteralNoneNode
494            | SyntaxKind::LiteralStringNode
495            | SyntaxKind::LiteralPairNode
496            | SyntaxKind::LiteralArrayNode
497            | SyntaxKind::LiteralMapNode
498            | SyntaxKind::LiteralObjectNode
499            | SyntaxKind::LiteralStructNode
500            | SyntaxKind::LiteralHintsNode
501            | SyntaxKind::LiteralInputNode
502            | SyntaxKind::LiteralOutputNode
503            | SyntaxKind::ParenthesizedExprNode
504            | SyntaxKind::NameRefExprNode
505            | SyntaxKind::IfExprNode
506            | SyntaxKind::LogicalNotExprNode
507            | SyntaxKind::NegationExprNode
508            | SyntaxKind::LogicalOrExprNode
509            | SyntaxKind::LogicalAndExprNode
510            | SyntaxKind::EqualityExprNode
511            | SyntaxKind::InequalityExprNode
512            | SyntaxKind::LessExprNode
513            | SyntaxKind::LessEqualExprNode
514            | SyntaxKind::GreaterExprNode
515            | SyntaxKind::GreaterEqualExprNode
516            | SyntaxKind::AdditionExprNode
517            | SyntaxKind::SubtractionExprNode
518            | SyntaxKind::MultiplicationExprNode
519            | SyntaxKind::DivisionExprNode
520            | SyntaxKind::ModuloExprNode
521            | SyntaxKind::CallExprNode
522            | SyntaxKind::IndexExprNode
523            | SyntaxKind::AccessExprNode) => {
524                unreachable!("`{k:?}` should be handled by `Expr::can_cast`")
525            }
526            SyntaxKind::PlaceholderNode => visitor.placeholder(
527                diagnostics,
528                reason,
529                &Placeholder::cast(element.into_node().unwrap()).expect("should cast"),
530            ),
531            SyntaxKind::PlaceholderSepOptionNode
532            | SyntaxKind::PlaceholderDefaultOptionNode
533            | SyntaxKind::PlaceholderTrueFalseOptionNode => {
534                // Skip these nodes as they're part of a placeholder
535            }
536            SyntaxKind::ConditionalStatementNode => visitor.conditional_statement(
537                diagnostics,
538                reason,
539                &ConditionalStatement::cast(element.into_node().unwrap()).expect("should cast"),
540            ),
541            SyntaxKind::ScatterStatementNode => visitor.scatter_statement(
542                diagnostics,
543                reason,
544                &ScatterStatement::cast(element.into_node().unwrap()).expect("should cast"),
545            ),
546            SyntaxKind::CallStatementNode => visitor.call_statement(
547                diagnostics,
548                reason,
549                &CallStatement::cast(element.into_node().unwrap()).expect("should cast"),
550            ),
551            SyntaxKind::CallTargetNode
552            | SyntaxKind::CallAliasNode
553            | SyntaxKind::CallAfterNode
554            | SyntaxKind::CallInputItemNode => {
555                // Skip these nodes as they're part of a call statement
556            }
557            SyntaxKind::Abandoned | SyntaxKind::MAX => {
558                unreachable!("node should not exist in the tree")
559            }
560            SyntaxKind::Whitespace if reason == VisitReason::Enter => visitor.whitespace(
561                diagnostics,
562                &Whitespace::cast(element.into_token().unwrap()).expect("should cast"),
563            ),
564            SyntaxKind::Comment if reason == VisitReason::Enter => visitor.comment(
565                diagnostics,
566                &Comment::cast(element.into_token().unwrap()).expect("should cast"),
567            ),
568            SyntaxKind::LiteralStringText if reason == VisitReason::Enter => visitor.string_text(
569                diagnostics,
570                &StringText::cast(element.into_token().unwrap()).expect("should cast"),
571            ),
572            SyntaxKind::LiteralCommandText if reason == VisitReason::Enter => visitor.command_text(
573                diagnostics,
574                &CommandText::cast(element.into_token().unwrap()).expect("should cast"),
575            ),
576            _ => {
577                // Skip remaining tokens
578            }
579        }
580    }
581}