Skip to main content

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