wdl_lint/rules/
no_curly_commands.rs1use 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
22const ID: &str = "NoCurlyCommands";
24
25fn 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#[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 *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}