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