wdl_lint/rules/
no_curly_commands.rs

1//! A lint rule for ensuring no curly commands are used.
2
3use wdl_ast::AstNode;
4use wdl_ast::AstToken;
5use wdl_ast::Diagnostic;
6use wdl_ast::Diagnostics;
7use wdl_ast::Document;
8use wdl_ast::Span;
9use wdl_ast::SupportedVersion;
10use wdl_ast::SyntaxElement;
11use wdl_ast::SyntaxKind;
12use wdl_ast::ToSpan;
13use wdl_ast::VisitReason;
14use wdl_ast::Visitor;
15use wdl_ast::support;
16use wdl_ast::v1::CommandSection;
17
18use crate::Rule;
19use crate::Tag;
20use crate::TagSet;
21
22/// The identifier for the no curly commands rule.
23const ID: &str = "NoCurlyCommands";
24
25/// Creates a "curly commands" diagnostic.
26fn curly_commands(task: &str, span: Span) -> Diagnostic {
27    Diagnostic::warning(format!(
28        "task `{task}` uses curly braces in command section"
29    ))
30    .with_rule(ID)
31    .with_label("this command section uses curly braces", span)
32    .with_fix("instead of curly braces, use heredoc syntax (<<<>>>>) for command sections")
33}
34
35/// Detects curly command section for tasks.
36#[derive(Default, Debug, Clone, Copy)]
37pub struct NoCurlyCommandsRule;
38
39impl Rule for NoCurlyCommandsRule {
40    fn id(&self) -> &'static str {
41        ID
42    }
43
44    fn description(&self) -> &'static str {
45        "Ensures that tasks use heredoc syntax in command sections."
46    }
47
48    fn explanation(&self) -> &'static str {
49        "Curly command blocks are no longer considered idiomatic WDL. Idiomatic WDL code uses \
50         heredoc command blocks instead. This is because curly command blocks create ambiguity \
51         with Bash syntax."
52    }
53
54    fn tags(&self) -> TagSet {
55        TagSet::new(&[Tag::Clarity])
56    }
57
58    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
59        Some(&[
60            SyntaxKind::VersionStatementNode,
61            SyntaxKind::CommandSectionNode,
62        ])
63    }
64}
65
66impl Visitor for NoCurlyCommandsRule {
67    type State = Diagnostics;
68
69    fn document(
70        &mut self,
71        _: &mut Self::State,
72        reason: VisitReason,
73        _: &Document,
74        _: SupportedVersion,
75    ) {
76        if reason == VisitReason::Exit {
77            return;
78        }
79
80        // Reset the visitor upon document entry
81        *self = Default::default();
82    }
83
84    fn command_section(
85        &mut self,
86        state: &mut Self::State,
87        reason: VisitReason,
88        section: &CommandSection,
89    ) {
90        if reason == VisitReason::Exit {
91            return;
92        }
93
94        if !section.is_heredoc() {
95            let name = section.parent().name();
96            let command_keyword = support::token(section.syntax(), SyntaxKind::CommandKeyword)
97                .expect("should have a command keyword token");
98
99            state.exceptable_add(
100                curly_commands(name.as_str(), command_keyword.text_range().to_span()),
101                SyntaxElement::from(section.syntax().clone()),
102                &self.exceptable_nodes(),
103            );
104        }
105    }
106}