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