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 tracing::trace;
26use wdl_ast::AstNode;
27use wdl_ast::AstToken;
28use wdl_ast::Comment;
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 a call for
67/// 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    trace!(
335        uri = %document.uri(),
336        "beginning document traversal",
337    );
338    for event in document.root().inner().preorder_with_tokens() {
339        let (reason, element) = match event {
340            WalkEvent::Enter(node) => (VisitReason::Enter, node),
341            WalkEvent::Leave(node) => (VisitReason::Exit, node),
342        };
343        trace!(uri = %document.uri(), ?reason, element = ?element.kind());
344        match element.kind() {
345            SyntaxKind::RootNode => visitor.document(
346                diagnostics,
347                reason,
348                document,
349                document
350                    .version()
351                    .expect("visited document must have a version"),
352            ),
353            SyntaxKind::VersionStatementNode => visitor.version_statement(
354                diagnostics,
355                reason,
356                &VersionStatement::cast(element.into_node().unwrap()).expect("should cast"),
357            ),
358            SyntaxKind::ImportStatementNode => visitor.import_statement(
359                diagnostics,
360                reason,
361                &ImportStatement::cast(element.into_node().unwrap()).expect("should cast"),
362            ),
363            SyntaxKind::ImportAliasNode => {
364                // Skip these nodes as they're part of an import statement
365            }
366            SyntaxKind::StructDefinitionNode => visitor.struct_definition(
367                diagnostics,
368                reason,
369                &StructDefinition::cast(element.into_node().unwrap()).expect("should cast"),
370            ),
371            SyntaxKind::TaskDefinitionNode => visitor.task_definition(
372                diagnostics,
373                reason,
374                &TaskDefinition::cast(element.into_node().unwrap()).expect("should cast"),
375            ),
376            SyntaxKind::WorkflowDefinitionNode => visitor.workflow_definition(
377                diagnostics,
378                reason,
379                &WorkflowDefinition::cast(element.into_node().unwrap()).expect("should cast"),
380            ),
381            SyntaxKind::UnboundDeclNode => visitor.unbound_decl(
382                diagnostics,
383                reason,
384                &UnboundDecl::cast(element.into_node().unwrap()).expect("should cast"),
385            ),
386            SyntaxKind::BoundDeclNode => visitor.bound_decl(
387                diagnostics,
388                reason,
389                &BoundDecl::cast(element.into_node().unwrap()).expect("should cast"),
390            ),
391            SyntaxKind::PrimitiveTypeNode
392            | SyntaxKind::MapTypeNode
393            | SyntaxKind::ArrayTypeNode
394            | SyntaxKind::PairTypeNode
395            | SyntaxKind::ObjectTypeNode
396            | SyntaxKind::TypeRefNode => {
397                // Skip these nodes as they're part of declarations
398            }
399            SyntaxKind::InputSectionNode => visitor.input_section(
400                diagnostics,
401                reason,
402                &InputSection::cast(element.into_node().unwrap()).expect("should cast"),
403            ),
404            SyntaxKind::OutputSectionNode => visitor.output_section(
405                diagnostics,
406                reason,
407                &OutputSection::cast(element.into_node().unwrap()).expect("should cast"),
408            ),
409            SyntaxKind::CommandSectionNode => visitor.command_section(
410                diagnostics,
411                reason,
412                &CommandSection::cast(element.into_node().unwrap()).expect("should cast"),
413            ),
414            SyntaxKind::RequirementsSectionNode => visitor.requirements_section(
415                diagnostics,
416                reason,
417                &RequirementsSection::cast(element.into_node().unwrap()).expect("should cast"),
418            ),
419            SyntaxKind::TaskHintsSectionNode => visitor.task_hints_section(
420                diagnostics,
421                reason,
422                &TaskHintsSection::cast(element.into_node().unwrap()).expect("should cast"),
423            ),
424            SyntaxKind::WorkflowHintsSectionNode => visitor.workflow_hints_section(
425                diagnostics,
426                reason,
427                &WorkflowHintsSection::cast(element.into_node().unwrap()).expect("should cast"),
428            ),
429            SyntaxKind::TaskHintsItemNode | SyntaxKind::WorkflowHintsItemNode => {
430                // Skip this node as it's part of a hints section
431            }
432            SyntaxKind::RequirementsItemNode => {
433                // Skip this node as it's part of a requirements section
434            }
435            SyntaxKind::RuntimeSectionNode => visitor.runtime_section(
436                diagnostics,
437                reason,
438                &RuntimeSection::cast(element.into_node().unwrap()).expect("should cast"),
439            ),
440            SyntaxKind::RuntimeItemNode => visitor.runtime_item(
441                diagnostics,
442                reason,
443                &RuntimeItem::cast(element.into_node().unwrap()).expect("should cast"),
444            ),
445            SyntaxKind::MetadataSectionNode => visitor.metadata_section(
446                diagnostics,
447                reason,
448                &MetadataSection::cast(element.into_node().unwrap()).expect("should cast"),
449            ),
450            SyntaxKind::ParameterMetadataSectionNode => visitor.parameter_metadata_section(
451                diagnostics,
452                reason,
453                &ParameterMetadataSection::cast(element.into_node().unwrap()).expect("should cast"),
454            ),
455            SyntaxKind::MetadataObjectNode => visitor.metadata_object(
456                diagnostics,
457                reason,
458                &MetadataObject::cast(element.into_node().unwrap()).expect("should cast"),
459            ),
460            SyntaxKind::MetadataObjectItemNode => visitor.metadata_object_item(
461                diagnostics,
462                reason,
463                &MetadataObjectItem::cast(element.into_node().unwrap()).expect("should cast"),
464            ),
465            SyntaxKind::MetadataArrayNode => visitor.metadata_array(
466                diagnostics,
467                reason,
468                &MetadataArray::cast(element.into_node().unwrap()).expect("should cast"),
469            ),
470            SyntaxKind::LiteralNullNode => {
471                // Skip these nodes as they're part of a metadata section
472            }
473            k if Expr::<SyntaxNode>::can_cast(k) => {
474                visitor.expr(
475                    diagnostics,
476                    reason,
477                    &Expr::cast(element.into_node().expect(
478                        "any element that is able to be turned into an expr should be a node",
479                    ))
480                    .expect("expr should be built"),
481                )
482            }
483            SyntaxKind::LiteralMapItemNode
484            | SyntaxKind::LiteralObjectItemNode
485            | SyntaxKind::LiteralStructItemNode
486            | SyntaxKind::LiteralHintsItemNode
487            | SyntaxKind::LiteralInputItemNode
488            | SyntaxKind::LiteralOutputItemNode => {
489                // Skip these nodes as they're part of literal expressions
490            }
491            k @ (SyntaxKind::LiteralIntegerNode
492            | SyntaxKind::LiteralFloatNode
493            | SyntaxKind::LiteralBooleanNode
494            | SyntaxKind::LiteralNoneNode
495            | SyntaxKind::LiteralStringNode
496            | SyntaxKind::LiteralPairNode
497            | SyntaxKind::LiteralArrayNode
498            | SyntaxKind::LiteralMapNode
499            | SyntaxKind::LiteralObjectNode
500            | SyntaxKind::LiteralStructNode
501            | SyntaxKind::LiteralHintsNode
502            | SyntaxKind::LiteralInputNode
503            | SyntaxKind::LiteralOutputNode
504            | SyntaxKind::ParenthesizedExprNode
505            | SyntaxKind::NameRefExprNode
506            | SyntaxKind::IfExprNode
507            | SyntaxKind::LogicalNotExprNode
508            | SyntaxKind::NegationExprNode
509            | SyntaxKind::LogicalOrExprNode
510            | SyntaxKind::LogicalAndExprNode
511            | SyntaxKind::EqualityExprNode
512            | SyntaxKind::InequalityExprNode
513            | SyntaxKind::LessExprNode
514            | SyntaxKind::LessEqualExprNode
515            | SyntaxKind::GreaterExprNode
516            | SyntaxKind::GreaterEqualExprNode
517            | SyntaxKind::AdditionExprNode
518            | SyntaxKind::SubtractionExprNode
519            | SyntaxKind::MultiplicationExprNode
520            | SyntaxKind::DivisionExprNode
521            | SyntaxKind::ModuloExprNode
522            | SyntaxKind::CallExprNode
523            | SyntaxKind::IndexExprNode
524            | SyntaxKind::AccessExprNode) => {
525                unreachable!("`{k:?}` should be handled by `Expr::can_cast`")
526            }
527            SyntaxKind::PlaceholderNode => visitor.placeholder(
528                diagnostics,
529                reason,
530                &Placeholder::cast(element.into_node().unwrap()).expect("should cast"),
531            ),
532            SyntaxKind::PlaceholderSepOptionNode
533            | SyntaxKind::PlaceholderDefaultOptionNode
534            | SyntaxKind::PlaceholderTrueFalseOptionNode => {
535                // Skip these nodes as they're part of a placeholder
536            }
537            SyntaxKind::ConditionalStatementNode => visitor.conditional_statement(
538                diagnostics,
539                reason,
540                &ConditionalStatement::cast(element.into_node().unwrap()).expect("should cast"),
541            ),
542            SyntaxKind::ScatterStatementNode => visitor.scatter_statement(
543                diagnostics,
544                reason,
545                &ScatterStatement::cast(element.into_node().unwrap()).expect("should cast"),
546            ),
547            SyntaxKind::CallStatementNode => visitor.call_statement(
548                diagnostics,
549                reason,
550                &CallStatement::cast(element.into_node().unwrap()).expect("should cast"),
551            ),
552            SyntaxKind::CallTargetNode
553            | SyntaxKind::CallAliasNode
554            | SyntaxKind::CallAfterNode
555            | SyntaxKind::CallInputItemNode => {
556                // Skip these nodes as they're part of a call statement
557            }
558            SyntaxKind::Abandoned | SyntaxKind::MAX => {
559                unreachable!("node should not exist in the tree")
560            }
561            SyntaxKind::Whitespace if reason == VisitReason::Enter => visitor.whitespace(
562                diagnostics,
563                &Whitespace::cast(element.into_token().unwrap()).expect("should cast"),
564            ),
565            SyntaxKind::Comment if reason == VisitReason::Enter => visitor.comment(
566                diagnostics,
567                &Comment::cast(element.into_token().unwrap()).expect("should cast"),
568            ),
569            SyntaxKind::LiteralStringText if reason == VisitReason::Enter => visitor.string_text(
570                diagnostics,
571                &StringText::cast(element.into_token().unwrap()).expect("should cast"),
572            ),
573            SyntaxKind::LiteralCommandText if reason == VisitReason::Enter => visitor.command_text(
574                diagnostics,
575                &CommandText::cast(element.into_token().unwrap()).expect("should cast"),
576            ),
577            _ => {
578                // Skip remaining tokens
579            }
580        }
581    }
582}