Skip to main content

wdl_analysis/
rules.rs

1//! Implementation of analysis rules.
2
3pub mod util;
4
5use std::collections::HashMap;
6use std::sync::LazyLock;
7
8use wdl_ast::Severity;
9use wdl_grammar::SyntaxKind;
10
11use crate::RuleMap;
12
13/// All rule IDs sorted alphabetically.
14pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
15    let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
16    ids.sort();
17    ids
18});
19
20/// All rules and their exceptable nodes.
21pub(crate) static RULE_MAP: LazyLock<RuleMap> = LazyLock::new(|| {
22    let rules = rules();
23    let mut map = HashMap::with_capacity(rules.len());
24    for rule in rules {
25        map.insert(String::from(rule.id()), rule.exceptable_nodes());
26    }
27    map
28});
29
30/// A labeled WDL code snippet.
31#[derive(Copy, Clone, Debug)]
32pub struct LabeledSnippet {
33    /// A label for the snippet.
34    pub label: Option<&'static str>,
35    /// A WDL code snippet.
36    pub snippet: &'static str,
37}
38
39/// A lint rule example.
40#[derive(Copy, Clone, Debug)]
41pub struct Example {
42    /// A snippet that will trigger the target lint rule.
43    pub negative: LabeledSnippet,
44    /// A revision of the negative snippet that will no longer trigger the rule.
45    pub revised: Option<LabeledSnippet>,
46}
47
48/// A trait implemented by analysis rules.
49pub trait Rule: Send + Sync {
50    /// The unique identifier for the rule.
51    ///
52    /// The identifier is required to be pascal case and it is the identifier by
53    /// which a rule is excepted or denied.
54    fn id(&self) -> &'static str;
55
56    /// A short, single sentence description of the rule.
57    fn description(&self) -> &'static str;
58
59    /// Get the long-form explanation of the rule.
60    fn explanation(&self) -> &'static str;
61
62    /// Get a list of examples that would trigger this rule.
63    fn examples(&self) -> &'static [Example];
64
65    /// Gets the nodes that are exceptable for this rule.
66    ///
67    /// If `None` is returned, all nodes are exceptable.
68    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
69
70    /// Denies the rule.
71    ///
72    /// Denying the rule treats any diagnostics it emits as an error.
73    fn deny(&mut self);
74
75    /// Gets the severity of the rule.
76    fn severity(&self) -> Severity;
77}
78
79/// Gets the list of all analysis rules.
80pub fn rules() -> Vec<Box<dyn Rule>> {
81    let rules: Vec<Box<dyn Rule>> = vec![
82        Box::<UnusedImportRule>::default(),
83        Box::<UnusedInputRule>::default(),
84        Box::<UnusedDeclarationRule>::default(),
85        Box::<UnusedCallRule>::default(),
86        Box::<UnnecessaryFunctionCall>::default(),
87        Box::<UsingFallbackVersion>::default(),
88        Box::<MisleadingDeclarationOrderRule>::default(),
89        Box::<MeaninglessLintDirective>::default(),
90        Box::<KnownRulesRule>::default(),
91        Box::<ExceptDirectiveValidRule>::default(),
92        Box::<CommandSectionIndentationRule>::default(),
93        Box::<DeprecatedObjectRule>::default(),
94        Box::<DeprecatedPlaceholderRule>::default(),
95        Box::<DeprecatedRuntimeSectionRule>::default(),
96    ];
97
98    // Ensure all the rule ids are unique and pascal case
99    #[cfg(debug_assertions)]
100    {
101        use convert_case::Case;
102        use convert_case::Casing;
103        let mut set = std::collections::HashSet::new();
104        for r in rules.iter() {
105            if r.id().to_case(Case::Pascal) != r.id() {
106                panic!("analysis rule id `{id}` is not pascal case", id = r.id());
107            }
108
109            if !set.insert(r.id()) {
110                panic!("duplicate rule id `{id}`", id = r.id());
111            }
112        }
113    }
114
115    rules
116}
117
118/// Represents the unused import rule.
119#[derive(Debug, Clone, Copy)]
120pub struct UnusedImportRule(Severity);
121
122impl UnusedImportRule {
123    /// See [`Self::exceptable_nodes()`].
124    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
125        SyntaxKind::VersionStatementNode,
126        SyntaxKind::ImportStatementNode,
127    ]);
128    /// The rule identifier for unused import warnings.
129    pub const ID: &'static str = "UnusedImport";
130
131    /// Creates a new unused import rule.
132    pub fn new() -> Self {
133        Self(Severity::Warning)
134    }
135}
136
137impl Default for UnusedImportRule {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl Rule for UnusedImportRule {
144    fn id(&self) -> &'static str {
145        Self::ID
146    }
147
148    fn description(&self) -> &'static str {
149        "Ensures that import namespaces are used in the importing document."
150    }
151
152    fn explanation(&self) -> &'static str {
153        "Imported WDL documents should be used in the document that imports them. Unused imports \
154         impact parsing and evaluation performance."
155    }
156
157    fn examples(&self) -> &'static [Example] {
158        &[Example {
159            negative: LabeledSnippet {
160                label: None,
161                snippet: r#"version 1.3
162
163import "bar.wdl"
164import "foo.wdl" as used
165
166workflow example {
167    call used.test
168}
169"#,
170            },
171            revised: Some(LabeledSnippet {
172                label: Some("Consider removing the import entirely"),
173                snippet: r#"version 1.3
174
175import "foo.wdl" as used
176
177workflow example {
178    call used.test
179}
180"#,
181            }),
182        }]
183    }
184
185    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
186        Self::EXCEPTABLE_NODES
187    }
188
189    fn deny(&mut self) {
190        self.0 = Severity::Error;
191    }
192
193    fn severity(&self) -> Severity {
194        self.0
195    }
196}
197
198/// Represents the unused input rule.
199#[derive(Debug, Clone, Copy)]
200pub struct UnusedInputRule(Severity);
201
202impl UnusedInputRule {
203    /// See [`Self::exceptable_nodes()`].
204    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
205        SyntaxKind::VersionStatementNode,
206        SyntaxKind::WorkflowDefinitionNode,
207        SyntaxKind::TaskDefinitionNode,
208        SyntaxKind::BoundDeclNode,
209        SyntaxKind::UnboundDeclNode,
210    ]);
211    /// The rule identifier for unused input warnings.
212    pub const ID: &str = "UnusedInput";
213
214    /// Creates a new unused input rule.
215    pub fn new() -> Self {
216        Self(Severity::Warning)
217    }
218}
219
220impl Default for UnusedInputRule {
221    fn default() -> Self {
222        Self::new()
223    }
224}
225
226impl Rule for UnusedInputRule {
227    fn id(&self) -> &'static str {
228        Self::ID
229    }
230
231    fn description(&self) -> &'static str {
232        "Ensures that task or workspace inputs are used within the declaring task or workspace."
233    }
234
235    fn explanation(&self) -> &'static str {
236        "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
237         file inputs in tasks can also cause unnecessary file localizations."
238    }
239
240    fn examples(&self) -> &'static [Example] {
241        &[Example {
242            negative: LabeledSnippet {
243                label: None,
244                snippet: r#"version 1.2
245
246workflow example {
247    input {
248        String unused
249    }
250}
251"#,
252            },
253            revised: Some(LabeledSnippet {
254                label: Some("Consider removing the input entirely"),
255                snippet: r#"version 1.2
256
257workflow example {
258    input {
259    }
260}
261"#,
262            }),
263        }]
264    }
265
266    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
267        Self::EXCEPTABLE_NODES
268    }
269
270    fn deny(&mut self) {
271        self.0 = Severity::Error;
272    }
273
274    fn severity(&self) -> Severity {
275        self.0
276    }
277}
278
279/// Represents the unused declaration rule.
280#[derive(Debug, Clone, Copy)]
281pub struct UnusedDeclarationRule(Severity);
282
283impl UnusedDeclarationRule {
284    /// See [`Self::exceptable_nodes()`].
285    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
286        SyntaxKind::VersionStatementNode,
287        SyntaxKind::WorkflowDefinitionNode,
288        SyntaxKind::TaskDefinitionNode,
289        SyntaxKind::BoundDeclNode,
290        SyntaxKind::UnboundDeclNode,
291    ]);
292    /// The rule identifier for unused declaration warnings.
293    pub const ID: &str = "UnusedDeclaration";
294
295    /// Creates a new unused declaration rule.
296    pub fn new() -> Self {
297        Self(Severity::Warning)
298    }
299}
300
301impl Default for UnusedDeclarationRule {
302    fn default() -> Self {
303        Self::new()
304    }
305}
306
307impl Rule for UnusedDeclarationRule {
308    fn id(&self) -> &'static str {
309        Self::ID
310    }
311
312    fn description(&self) -> &'static str {
313        "Ensures that private declarations in tasks or workspaces are used within the declaring \
314         task or workspace."
315    }
316
317    fn explanation(&self) -> &'static str {
318        "Unused private declarations degrade evaluation performance and reduce the clarity of the \
319         code."
320    }
321
322    fn examples(&self) -> &'static [Example] {
323        &[Example {
324            negative: LabeledSnippet {
325                label: None,
326                snippet: r#"version 1.2
327
328workflow example {
329    String unused = "this will produce a warning"
330}
331"#,
332            },
333            revised: Some(LabeledSnippet {
334                label: Some("Consider removing the declaration entirely"),
335                snippet: r#"version 1.2
336
337workflow example {
338}
339"#,
340            }),
341        }]
342    }
343
344    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
345        Self::EXCEPTABLE_NODES
346    }
347
348    fn deny(&mut self) {
349        self.0 = Severity::Error;
350    }
351
352    fn severity(&self) -> Severity {
353        self.0
354    }
355}
356
357/// Represents the unused call rule.
358#[derive(Debug, Clone, Copy)]
359pub struct UnusedCallRule(Severity);
360
361impl UnusedCallRule {
362    /// See [`Self::exceptable_nodes()`].
363    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
364        SyntaxKind::VersionStatementNode,
365        SyntaxKind::WorkflowDefinitionNode,
366        SyntaxKind::CallStatementNode,
367    ]);
368    /// The rule identifier for unused call warnings.
369    pub const ID: &str = "UnusedCall";
370
371    /// Creates a new unused call rule.
372    pub fn new() -> Self {
373        Self(Severity::Warning)
374    }
375}
376
377impl Default for UnusedCallRule {
378    fn default() -> Self {
379        Self::new()
380    }
381}
382
383impl Rule for UnusedCallRule {
384    fn id(&self) -> &'static str {
385        Self::ID
386    }
387
388    fn description(&self) -> &'static str {
389        "Ensures that outputs of a call statement are used in the declaring workflow."
390    }
391
392    fn explanation(&self) -> &'static str {
393        "Unused calls may cause unnecessary consumption of compute resources."
394    }
395
396    fn examples(&self) -> &'static [Example] {
397        &[Example {
398            negative: LabeledSnippet {
399                label: None,
400                snippet: r#"version 1.2
401
402workflow example {
403    # The output of `do_work` is never used
404    call do_work
405}
406
407task do_work {
408    command <<<
409    >>>
410
411    output {
412        Int x = 0
413    }
414}
415"#,
416            },
417            revised: Some(LabeledSnippet {
418                label: Some("Consider removing the call entirely"),
419                snippet: r#"version 1.2
420
421workflow example {
422}
423
424task do_work {
425    command <<<
426    >>>
427
428    output {
429        Int x = 0
430    }
431}
432"#,
433            }),
434        }]
435    }
436
437    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
438        Self::EXCEPTABLE_NODES
439    }
440
441    fn deny(&mut self) {
442        self.0 = Severity::Error;
443    }
444
445    fn severity(&self) -> Severity {
446        self.0
447    }
448}
449
450/// Represents the unnecessary call rule.
451#[derive(Debug, Clone, Copy)]
452pub struct UnnecessaryFunctionCall(Severity);
453
454impl UnnecessaryFunctionCall {
455    /// See [`Self::exceptable_nodes()`].
456    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
457        SyntaxKind::VersionStatementNode,
458        SyntaxKind::WorkflowDefinitionNode,
459        SyntaxKind::TaskDefinitionNode,
460        SyntaxKind::BoundDeclNode,
461    ]);
462    /// The rule identifier for unnecessary function call warnings.
463    pub const ID: &str = "UnnecessaryFunctionCall";
464
465    /// Creates a new unnecessary function call rule.
466    pub fn new() -> Self {
467        Self(Severity::Warning)
468    }
469}
470
471impl Default for UnnecessaryFunctionCall {
472    fn default() -> Self {
473        Self::new()
474    }
475}
476
477impl Rule for UnnecessaryFunctionCall {
478    fn id(&self) -> &'static str {
479        Self::ID
480    }
481
482    fn description(&self) -> &'static str {
483        "Ensures that function calls are necessary."
484    }
485
486    fn explanation(&self) -> &'static str {
487        "Unnecessary function calls may impact evaluation performance."
488    }
489
490    fn examples(&self) -> &'static [Example] {
491        &[Example {
492            negative: LabeledSnippet {
493                label: None,
494                snippet: r#"version 1.2
495
496workflow example {
497    # Calls to `defined` on values that are statically
498    # known to be non-None are unnecessary.
499    Boolean exists = defined("hello")
500}
501"#,
502            },
503            revised: None,
504        }]
505    }
506
507    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
508        Self::EXCEPTABLE_NODES
509    }
510
511    fn deny(&mut self) {
512        self.0 = Severity::Error;
513    }
514
515    fn severity(&self) -> Severity {
516        self.0
517    }
518}
519
520/// Represents the using fallback version rule.
521#[derive(Debug, Clone, Copy)]
522pub struct UsingFallbackVersion(Severity);
523
524impl UsingFallbackVersion {
525    /// See [`Self::exceptable_nodes()`].
526    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
527        Some(&[SyntaxKind::VersionStatementNode]);
528    /// The rule identifier for unsupported version fallback warnings.
529    pub const ID: &str = "UsingFallbackVersion";
530
531    /// Creates a new using fallback version rule.
532    pub fn new() -> Self {
533        Self(Severity::Warning)
534    }
535}
536
537impl Default for UsingFallbackVersion {
538    fn default() -> Self {
539        Self::new()
540    }
541}
542
543impl Rule for UsingFallbackVersion {
544    fn id(&self) -> &'static str {
545        Self::ID
546    }
547
548    fn description(&self) -> &'static str {
549        "Warns if interpretation of a document with an unsupported version falls back to a default."
550    }
551
552    fn explanation(&self) -> &'static str {
553        "A document with an unsupported version may have unpredictable behavior if interpreted as \
554         a different version."
555    }
556
557    fn examples(&self) -> &'static [Example] {
558        &[Example {
559            negative: LabeledSnippet {
560                label: None,
561                snippet: r#"# Not a valid version. If a fallback version is configured,
562# the document will be interpreted as that version.
563version development
564
565workflow example {
566}
567"#,
568            },
569            revised: None,
570        }]
571    }
572
573    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
574        Self::EXCEPTABLE_NODES
575    }
576
577    fn deny(&mut self) {
578        self.0 = Severity::Error;
579    }
580
581    fn severity(&self) -> Severity {
582        self.0
583    }
584}
585
586/// Represents the meaningless lint directive rule.
587#[derive(Debug, Clone, Copy)]
588pub struct MeaninglessLintDirective(Severity);
589
590impl MeaninglessLintDirective {
591    /// See [`Self::exceptable_nodes()`].
592    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = None;
593    /// The rule identifier for meaningless lint directive warnings.
594    pub const ID: &str = "MeaninglessLintDirective";
595
596    /// Creates a new meaningless lint directive rule.
597    pub fn new() -> Self {
598        Self(Severity::Note)
599    }
600}
601
602impl Default for MeaninglessLintDirective {
603    fn default() -> Self {
604        Self::new()
605    }
606}
607
608impl Rule for MeaninglessLintDirective {
609    fn id(&self) -> &'static str {
610        Self::ID
611    }
612
613    fn description(&self) -> &'static str {
614        "Warns if an `#@ except:` comment doesn't actually suppress a lint."
615    }
616
617    fn explanation(&self) -> &'static str {
618        "Unused `#@ except:` comments are likely leftovers of refactoring or debugging, and can \
619         reduce the clarity of the code. It is best to remove them."
620    }
621
622    fn examples(&self) -> &'static [Example] {
623        &[Example {
624            negative: LabeledSnippet {
625                label: None,
626                snippet: r#"version 1.3
627
628task do_work {
629    command <<<
630        echo "Lots of hard work!"
631    >>>
632
633    output {
634        String result = read_string(stdout())
635    }
636}
637
638# We except `UnusedCall` unnecessarily.
639workflow calculate {
640    #@ except: UnusedCall
641    call do_work
642
643    output {
644        # We're using the result here!
645        String result = do_work.result
646    }
647}
648"#,
649            },
650            revised: Some(LabeledSnippet {
651                label: Some("Consider removing the unused exception"),
652                snippet: r#"version 1.3
653
654task do_work {
655    command <<<
656        echo "Lots of hard work!"
657    >>>
658
659    output {
660        String result = read_string(stdout())
661    }
662}
663
664workflow calculate {
665    call do_work
666
667    output {
668        String result = do_work.result
669    }
670}
671"#,
672            }),
673        }]
674    }
675
676    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
677        Self::EXCEPTABLE_NODES
678    }
679
680    fn deny(&mut self) {
681        self.0 = Severity::Error;
682    }
683
684    fn severity(&self) -> Severity {
685        self.0
686    }
687}
688
689/// Represents the using misleading declaration order rule.
690#[derive(Debug, Clone, Copy)]
691pub struct MisleadingDeclarationOrderRule(Severity);
692
693impl MisleadingDeclarationOrderRule {
694    /// See [`Self::exceptable_nodes()`].
695    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
696        Some(&[SyntaxKind::TaskDefinitionNode, SyntaxKind::BoundDeclNode]);
697    /// The rule identifier for misleading declaration order warnings.
698    pub const ID: &str = "MisleadingDeclarationOrder";
699
700    /// Creates a new misleading declaration order rule.
701    pub fn new() -> Self {
702        Self(Severity::Warning)
703    }
704}
705
706impl Default for MisleadingDeclarationOrderRule {
707    fn default() -> Self {
708        Self::new()
709    }
710}
711
712impl Rule for MisleadingDeclarationOrderRule {
713    fn id(&self) -> &'static str {
714        Self::ID
715    }
716
717    fn description(&self) -> &'static str {
718        "Warns when a variable declaration is placed after a `command` block."
719    }
720
721    fn explanation(&self) -> &'static str {
722        "WDL tasks are evaluated based on their dependency graph, not top-to-bottom. Variable \
723         declarations that appear after `command` sections are visually misleading, as they will \
724         still be evaluated _before_ the command is executed."
725    }
726
727    fn examples(&self) -> &'static [Example] {
728        &[Example {
729            negative: LabeledSnippet {
730                label: None,
731                snippet: r#"version 1.2
732
733task greet {
734    String greeting = "Hello"
735
736    command <<<
737        echo "~{greeting}, ~{name}!"
738    >>>
739
740    String name = "World"
741}
742"#,
743            },
744            revised: Some(LabeledSnippet {
745                label: None,
746                snippet: r#"version 1.2
747
748task greet {
749    String greeting = "Hello"
750    String name = "World"
751
752    command <<<
753        echo "~{greeting}, ~{name}!"
754    >>>
755}
756"#,
757            }),
758        }]
759    }
760
761    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
762        Self::EXCEPTABLE_NODES
763    }
764
765    fn deny(&mut self) {
766        self.0 = Severity::Error;
767    }
768
769    fn severity(&self) -> Severity {
770        self.0
771    }
772}
773
774/// Detects unknown rules within lint directives.
775#[derive(Debug, Clone, Copy)]
776pub struct KnownRulesRule(Severity);
777
778impl KnownRulesRule {
779    /// See [`Self::exceptable_nodes()`].
780    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
781        Some(&[SyntaxKind::VersionStatementNode]);
782    /// The rule identifier for known rules warnings.
783    pub const ID: &str = "KnownRules";
784
785    /// Creates a new "known rules" rule.
786    pub fn new() -> Self {
787        Self(Severity::Note)
788    }
789}
790
791impl Default for KnownRulesRule {
792    fn default() -> Self {
793        Self::new()
794    }
795}
796
797impl Rule for KnownRulesRule {
798    fn id(&self) -> &'static str {
799        Self::ID
800    }
801
802    fn description(&self) -> &'static str {
803        "Ensures only known rules are used in `except` directives."
804    }
805
806    fn explanation(&self) -> &'static str {
807        "When writing WDL, `except` directives are used to suppress certain rules. If a rule is \
808         unknown, nothing will be suppressed. This rule flags unknown rules as they are often \
809         mistakes."
810    }
811
812    fn examples(&self) -> &'static [Example] {
813        &[Example {
814            negative: LabeledSnippet {
815                label: None,
816                snippet: r#"#@ except: LintThatDoesNotExist
817version 1.2
818
819workflow example {
820}
821"#,
822            },
823            revised: Some(LabeledSnippet {
824                label: None,
825                snippet: r#"version 1.2
826
827workflow example {
828}
829"#,
830            }),
831        }]
832    }
833
834    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
835        Self::EXCEPTABLE_NODES
836    }
837
838    fn deny(&mut self) {
839        self.0 = Severity::Error;
840    }
841
842    fn severity(&self) -> Severity {
843        self.0
844    }
845}
846
847/// Detects improperly placed `except` directives.
848#[derive(Debug, Clone, Copy)]
849pub struct ExceptDirectiveValidRule(Severity);
850
851impl ExceptDirectiveValidRule {
852    /// See [`Self::exceptable_nodes()`].
853    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
854        Some(&[SyntaxKind::VersionStatementNode]);
855    /// The rule identifier for except directive warnings.
856    pub const ID: &str = "ExceptDirectiveValid";
857
858    /// Creates a new "except directive valid" rule.
859    pub fn new() -> Self {
860        Self(Severity::Note)
861    }
862}
863
864impl Default for ExceptDirectiveValidRule {
865    fn default() -> Self {
866        Self::new()
867    }
868}
869
870impl Rule for ExceptDirectiveValidRule {
871    fn id(&self) -> &'static str {
872        Self::ID
873    }
874
875    fn description(&self) -> &'static str {
876        "Ensures `except` directives are placed correctly to have the intended effect."
877    }
878
879    fn explanation(&self) -> &'static str {
880        "When writing WDL, `except` directives are used to suppress certain rules. If an `except` \
881         directive is misplaced, it will have no effect. This rule flags misplaced `except` \
882         directives to ensure they are in the correct location."
883    }
884
885    fn examples(&self) -> &'static [Example] {
886        &[Example {
887            negative: LabeledSnippet {
888                label: None,
889                snippet: r#"version 1.3
890
891# UsingFallbackVersion exceptions aren't valid
892# in this context
893#@ except: UsingFallbackVersion
894workflow example {
895}
896"#,
897            },
898            revised: Some(LabeledSnippet {
899                label: None,
900                snippet: r#"#@ except: UsingFallbackVersion
901version 1.3
902
903workflow example {
904}
905"#,
906            }),
907        }]
908    }
909
910    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
911        Self::EXCEPTABLE_NODES
912    }
913
914    fn deny(&mut self) {
915        self.0 = Severity::Error;
916    }
917
918    fn severity(&self) -> Severity {
919        self.0
920    }
921}
922
923/// Detects mixed indentation within command sections.
924#[derive(Debug, Clone, Copy)]
925pub struct CommandSectionIndentationRule(Severity);
926
927impl CommandSectionIndentationRule {
928    /// See [`Self::exceptable_nodes()`].
929    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
930        SyntaxKind::VersionStatementNode,
931        SyntaxKind::CommandSectionNode,
932    ]);
933    /// The rule identifier for mixed command section indentation warnings.
934    pub const ID: &str = "CommandSectionIndentation";
935
936    /// Creates a new "mixed command section indentation" rule.
937    pub fn new() -> Self {
938        Self(Severity::Warning)
939    }
940}
941
942impl Default for CommandSectionIndentationRule {
943    fn default() -> Self {
944        Self::new()
945    }
946}
947
948impl Rule for CommandSectionIndentationRule {
949    fn id(&self) -> &'static str {
950        Self::ID
951    }
952
953    fn description(&self) -> &'static str {
954        "Ensures consistent indentation (no mixed spaces/tabs) within command sections."
955    }
956
957    fn explanation(&self) -> &'static str {
958        "Mixing indentation (tab and space) characters within the command line causes leading \
959         whitespace stripping to be skipped. Commands may be whitespace sensitive, and skipping \
960         the whitespace stripping step may cause unexpected behavior."
961    }
962
963    fn examples(&self) -> &'static [Example] {
964        &[Example {
965            negative: LabeledSnippet {
966                label: None,
967                snippet: r#"version 1.3
968
969task say_greetings {
970    input {
971        String name
972    }
973
974    command <<<
975        # this line is prefixed with tabs
976		echo "Hello, ~{name}!"
977        # this line is prefixed with spaces
978        echo "Goodbye, ~{name}!"
979    >>>
980}
981"#,
982            },
983            revised: Some(LabeledSnippet {
984                label: None,
985                snippet: r#"version 1.3
986
987task say_greetings {
988    input {
989        String name
990    }
991
992    command <<<
993        # this line is prefixed with spaces
994        echo "Hello, ~{name}!"
995        # this line is prefixed with spaces
996        echo "Goodbye, ~{name}!"
997    >>>
998}
999"#,
1000            }),
1001        }]
1002    }
1003
1004    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
1005        Self::EXCEPTABLE_NODES
1006    }
1007
1008    fn deny(&mut self) {
1009        self.0 = Severity::Error;
1010    }
1011
1012    fn severity(&self) -> Severity {
1013        self.0
1014    }
1015}
1016
1017/// Detects the use of the deprecated `Object` types.
1018#[derive(Debug, Clone, Copy)]
1019pub struct DeprecatedObjectRule(Severity);
1020
1021impl DeprecatedObjectRule {
1022    /// See [`Self::exceptable_nodes()`].
1023    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
1024        SyntaxKind::VersionStatementNode,
1025        SyntaxKind::TaskDefinitionNode,
1026        SyntaxKind::WorkflowDefinitionNode,
1027        SyntaxKind::BoundDeclNode,
1028        SyntaxKind::UnboundDeclNode,
1029    ]);
1030    /// The rule identifier for deprecated object warnings.
1031    pub const ID: &str = "DeprecatedObject";
1032
1033    /// Creates a new "deprecated object" rule.
1034    pub fn new() -> Self {
1035        Self(Severity::Warning)
1036    }
1037}
1038
1039impl Default for DeprecatedObjectRule {
1040    fn default() -> Self {
1041        Self::new()
1042    }
1043}
1044
1045impl Rule for DeprecatedObjectRule {
1046    fn id(&self) -> &'static str {
1047        Self::ID
1048    }
1049
1050    fn description(&self) -> &'static str {
1051        "Ensures that the deprecated `Object` types are not used."
1052    }
1053
1054    fn explanation(&self) -> &'static str {
1055        "WDL `Object` types are officially deprecated and will be removed in the next major WDL release.
1056
1057`Object`s existed prior to better containers, such as `Map`s and `Struct`s, being \
1058introduced into the language. Unfortunately, though these better alternatives did exist at \
1059the time of the v1.0 release, the type was not removed. It was later decided \
1060that `Object`s overlapped with `Map`s and `Struct`s in functionality, and the type was marked for removal.
1061
1062See this issue for more details: <https://github.com/openwdl/wdl/pull/228>."
1063    }
1064
1065    fn examples(&self) -> &'static [Example] {
1066        &[Example {
1067            negative: LabeledSnippet {
1068                label: None,
1069                snippet: r#"version 1.2
1070
1071workflow example {
1072    Object person = object {
1073        name: "Jimmy",
1074        age: 55,
1075    }
1076}
1077"#,
1078            },
1079            revised: Some(LabeledSnippet {
1080                label: Some("Consider switching to a `Struct` or `Map`"),
1081                snippet: r#"version 1.2
1082
1083struct Person {
1084    String name
1085    Int age
1086}
1087
1088workflow example {
1089    Person person = Person {
1090        name: "Jimmy",
1091        age: 55,
1092    }
1093}
1094"#,
1095            }),
1096        }]
1097    }
1098
1099    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
1100        Self::EXCEPTABLE_NODES
1101    }
1102
1103    fn deny(&mut self) {
1104        self.0 = Severity::Error;
1105    }
1106
1107    fn severity(&self) -> Severity {
1108        self.0
1109    }
1110}
1111
1112/// Detects the use of a deprecated placeholder option.
1113#[derive(Debug, Clone, Copy)]
1114pub struct DeprecatedPlaceholderRule(Severity);
1115
1116impl DeprecatedPlaceholderRule {
1117    /// See [`Self::exceptable_nodes()`].
1118    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
1119        SyntaxKind::VersionStatementNode,
1120        SyntaxKind::TaskDefinitionNode,
1121        SyntaxKind::WorkflowDefinitionNode,
1122        SyntaxKind::PlaceholderNode,
1123    ]);
1124    /// The rule identifier for deprecated placeholder option warnings.
1125    pub const ID: &str = "DeprecatedPlaceholder";
1126
1127    /// Creates a new "deprecated placeholder option" rule.
1128    pub fn new() -> Self {
1129        Self(Severity::Warning)
1130    }
1131}
1132
1133impl Default for DeprecatedPlaceholderRule {
1134    fn default() -> Self {
1135        Self::new()
1136    }
1137}
1138
1139impl Rule for DeprecatedPlaceholderRule {
1140    fn id(&self) -> &'static str {
1141        Self::ID
1142    }
1143
1144    fn description(&self) -> &'static str {
1145        "Ensures that deprecated expression placeholder options are not used."
1146    }
1147
1148    fn explanation(&self) -> &'static str {
1149        "Expression placeholder options were deprecated in WDL v1.1 and will be removed in the \
1150         next major WDL version.
1151
1152         - `sep` placeholder options should be replaced by the `sep()` standard library function.
1153         - `true/false` placeholder options should be replaced with `if`/`else` statements.
1154         - `default` placeholder options should be replaced by the `select_first()` standard \
1155         library function.
1156         - `${}` interpolation placeholders should be replaced by `~{}` interpolation placeholders.
1157
1158
1159This rule only evaluates for WDL V1 documents with a version of v1.1 or later, as this was the \
1160         version where the deprecation was introduced."
1161    }
1162
1163    fn examples(&self) -> &'static [Example] {
1164        &[Example {
1165            negative: LabeledSnippet {
1166                label: None,
1167                snippet: r#"version 1.2
1168
1169workflow example {
1170    Array[String] names = [
1171        "James",
1172        "Jimmy",
1173        "John",
1174    ]
1175    String names_separated = "~{sep="," names}"
1176    String names_interpolated = "${names_separated}"
1177}
1178"#,
1179            },
1180            revised: Some(LabeledSnippet {
1181                label: None,
1182                snippet: r#"version 1.2
1183
1184workflow example {
1185    Array[String] names = [
1186        "James",
1187        "Jimmy",
1188        "John",
1189    ]
1190    String names_separated = "~{sep(",", names)}"
1191    String names_interpolated = "~{names_separated}"
1192}
1193"#,
1194            }),
1195        }]
1196    }
1197
1198    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
1199        Self::EXCEPTABLE_NODES
1200    }
1201
1202    fn deny(&mut self) {
1203        self.0 = Severity::Error;
1204    }
1205
1206    fn severity(&self) -> Severity {
1207        self.0
1208    }
1209}
1210
1211/// Detects deprecated `runtime` sections.
1212#[derive(Debug, Clone, Copy)]
1213pub struct DeprecatedRuntimeSectionRule(Severity);
1214
1215impl DeprecatedRuntimeSectionRule {
1216    /// See [`Self::exceptable_nodes()`].
1217    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
1218        SyntaxKind::VersionStatementNode,
1219        SyntaxKind::TaskDefinitionNode,
1220        SyntaxKind::RuntimeSectionNode,
1221    ]);
1222    /// The rule identifier for deprecated runtime section warnings.
1223    pub const ID: &str = "DeprecatedRuntimeSection";
1224
1225    /// Creates a new "deprecated runtime section" rule.
1226    pub fn new() -> Self {
1227        Self(Severity::Warning)
1228    }
1229}
1230
1231impl Default for DeprecatedRuntimeSectionRule {
1232    fn default() -> Self {
1233        Self::new()
1234    }
1235}
1236
1237impl Rule for DeprecatedRuntimeSectionRule {
1238    fn id(&self) -> &'static str {
1239        Self::ID
1240    }
1241
1242    fn description(&self) -> &'static str {
1243        "Detects deprecated `runtime` sections."
1244    }
1245
1246    fn explanation(&self) -> &'static str {
1247        "The `runtime` section is deprecated in WDL v1.2 and later. Replace it with a \
1248         `requirements` section."
1249    }
1250
1251    fn examples(&self) -> &'static [Example] {
1252        &[Example {
1253            negative: LabeledSnippet {
1254                label: None,
1255                snippet: r#"version 1.2
1256
1257task say_hello {
1258    input {
1259        String name
1260    }
1261
1262    command <<<
1263        echo "Hello, ~{name}!"
1264    >>>
1265
1266    runtime {
1267        container: "ubuntu:latest"
1268    }
1269}
1270"#,
1271            },
1272            revised: Some(LabeledSnippet {
1273                label: None,
1274                snippet: r#"version 1.2
1275
1276task say_hello {
1277    input {
1278        String name
1279    }
1280
1281    command <<<
1282        echo "Hello, ~{name}!"
1283    >>>
1284
1285    requirements {
1286        container: "ubuntu:latest"
1287    }
1288}
1289"#,
1290            }),
1291        }]
1292    }
1293
1294    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
1295        Self::EXCEPTABLE_NODES
1296    }
1297
1298    fn deny(&mut self) {
1299        self.0 = Severity::Error;
1300    }
1301
1302    fn severity(&self) -> Severity {
1303        self.0
1304    }
1305}