Skip to main content

wdl_analysis/
rules.rs

1//! Implementation of analysis rules.
2
3use std::sync::LazyLock;
4
5use wdl_ast::Severity;
6use wdl_grammar::SyntaxKind;
7
8/// All rule IDs sorted alphabetically.
9pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
10    let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
11    ids.sort();
12    ids
13});
14
15/// A labeled WDL code snippet.
16#[derive(Copy, Clone, Debug)]
17pub struct LabeledSnippet {
18    /// A label for the snippet.
19    pub label: Option<&'static str>,
20    /// A WDL code snippet.
21    pub snippet: &'static str,
22}
23
24/// A lint rule example.
25#[derive(Copy, Clone, Debug)]
26pub struct Example {
27    /// A snippet that will trigger the target lint rule.
28    pub negative: LabeledSnippet,
29    /// A revision of the negative snippet that will no longer trigger the rule.
30    pub revised: Option<LabeledSnippet>,
31}
32
33/// A trait implemented by analysis rules.
34pub trait Rule: Send + Sync {
35    /// The unique identifier for the rule.
36    ///
37    /// The identifier is required to be pascal case and it is the identifier by
38    /// which a rule is excepted or denied.
39    fn id(&self) -> &'static str;
40
41    /// A short, single sentence description of the rule.
42    fn description(&self) -> &'static str;
43
44    /// Get the long-form explanation of the rule.
45    fn explanation(&self) -> &'static str;
46
47    /// Get a list of examples that would trigger this rule.
48    fn examples(&self) -> &'static [Example];
49
50    /// Gets the nodes that are exceptable for this rule.
51    ///
52    /// If `None` is returned, all nodes are exceptable.
53    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
54
55    /// Denies the rule.
56    ///
57    /// Denying the rule treats any diagnostics it emits as an error.
58    fn deny(&mut self);
59
60    /// Gets the severity of the rule.
61    fn severity(&self) -> Severity;
62}
63
64/// Gets the list of all analysis rules.
65pub fn rules() -> Vec<Box<dyn Rule>> {
66    let rules: Vec<Box<dyn Rule>> = vec![
67        Box::<UnusedImportRule>::default(),
68        Box::<UnusedInputRule>::default(),
69        Box::<UnusedDeclarationRule>::default(),
70        Box::<UnusedCallRule>::default(),
71        Box::<UnnecessaryFunctionCall>::default(),
72        Box::<UsingFallbackVersion>::default(),
73        Box::<MisleadingDeclarationOrderRule>::default(),
74        Box::<MeaninglessLintDirective>::default(),
75        Box::<KnownRulesRule>::default(),
76    ];
77
78    // Ensure all the rule ids are unique and pascal case
79    #[cfg(debug_assertions)]
80    {
81        use convert_case::Case;
82        use convert_case::Casing;
83        let mut set = std::collections::HashSet::new();
84        for r in rules.iter() {
85            if r.id().to_case(Case::Pascal) != r.id() {
86                panic!("analysis rule id `{id}` is not pascal case", id = r.id());
87            }
88
89            if !set.insert(r.id()) {
90                panic!("duplicate rule id `{id}`", id = r.id());
91            }
92        }
93    }
94
95    rules
96}
97
98/// Represents the unused import rule.
99#[derive(Debug, Clone, Copy)]
100pub struct UnusedImportRule(Severity);
101
102impl UnusedImportRule {
103    /// See [`Self::exceptable_nodes()`].
104    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
105        SyntaxKind::VersionStatementNode,
106        SyntaxKind::ImportStatementNode,
107    ]);
108    /// The rule identifier for unused import warnings.
109    pub const ID: &'static str = "UnusedImport";
110
111    /// Creates a new unused import rule.
112    pub fn new() -> Self {
113        Self(Severity::Warning)
114    }
115}
116
117impl Default for UnusedImportRule {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123impl Rule for UnusedImportRule {
124    fn id(&self) -> &'static str {
125        Self::ID
126    }
127
128    fn description(&self) -> &'static str {
129        "Ensures that import namespaces are used in the importing document."
130    }
131
132    fn explanation(&self) -> &'static str {
133        "Imported WDL documents should be used in the document that imports them. Unused imports \
134         impact parsing and evaluation performance."
135    }
136
137    fn examples(&self) -> &'static [Example] {
138        &[Example {
139            negative: LabeledSnippet {
140                label: None,
141                snippet: r#"version 1.2
142
143import "foo.wdl"
144
145workflow example {
146}
147"#,
148            },
149            revised: Some(LabeledSnippet {
150                label: Some("Consider removing the import entirely"),
151                snippet: r#"version 1.2
152
153workflow example {
154}
155"#,
156            }),
157        }]
158    }
159
160    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
161        Self::EXCEPTABLE_NODES
162    }
163
164    fn deny(&mut self) {
165        self.0 = Severity::Error;
166    }
167
168    fn severity(&self) -> Severity {
169        self.0
170    }
171}
172
173/// Represents the unused input rule.
174#[derive(Debug, Clone, Copy)]
175pub struct UnusedInputRule(Severity);
176
177impl UnusedInputRule {
178    /// See [`Self::exceptable_nodes()`].
179    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
180        SyntaxKind::VersionStatementNode,
181        SyntaxKind::WorkflowDefinitionNode,
182        SyntaxKind::TaskDefinitionNode,
183        SyntaxKind::BoundDeclNode,
184        SyntaxKind::UnboundDeclNode,
185    ]);
186    /// The rule identifier for unused input warnings.
187    pub const ID: &str = "UnusedInput";
188
189    /// Creates a new unused input rule.
190    pub fn new() -> Self {
191        Self(Severity::Warning)
192    }
193}
194
195impl Default for UnusedInputRule {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl Rule for UnusedInputRule {
202    fn id(&self) -> &'static str {
203        Self::ID
204    }
205
206    fn description(&self) -> &'static str {
207        "Ensures that task or workspace inputs are used within the declaring task or workspace."
208    }
209
210    fn explanation(&self) -> &'static str {
211        "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
212         file inputs in tasks can also cause unnecessary file localizations."
213    }
214
215    fn examples(&self) -> &'static [Example] {
216        &[Example {
217            negative: LabeledSnippet {
218                label: None,
219                snippet: r#"version 1.2
220
221workflow example {
222    input {
223        String unused
224    }
225}
226"#,
227            },
228            revised: Some(LabeledSnippet {
229                label: Some("Consider removing the input entirely"),
230                snippet: r#"version 1.2
231
232workflow example {
233    input {
234    }
235}
236"#,
237            }),
238        }]
239    }
240
241    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
242        Self::EXCEPTABLE_NODES
243    }
244
245    fn deny(&mut self) {
246        self.0 = Severity::Error;
247    }
248
249    fn severity(&self) -> Severity {
250        self.0
251    }
252}
253
254/// Represents the unused declaration rule.
255#[derive(Debug, Clone, Copy)]
256pub struct UnusedDeclarationRule(Severity);
257
258impl UnusedDeclarationRule {
259    /// See [`Self::exceptable_nodes()`].
260    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
261        SyntaxKind::VersionStatementNode,
262        SyntaxKind::WorkflowDefinitionNode,
263        SyntaxKind::TaskDefinitionNode,
264        SyntaxKind::BoundDeclNode,
265        SyntaxKind::UnboundDeclNode,
266    ]);
267    /// The rule identifier for unused declaration warnings.
268    pub const ID: &str = "UnusedDeclaration";
269
270    /// Creates a new unused declaration rule.
271    pub fn new() -> Self {
272        Self(Severity::Warning)
273    }
274}
275
276impl Default for UnusedDeclarationRule {
277    fn default() -> Self {
278        Self::new()
279    }
280}
281
282impl Rule for UnusedDeclarationRule {
283    fn id(&self) -> &'static str {
284        Self::ID
285    }
286
287    fn description(&self) -> &'static str {
288        "Ensures that private declarations in tasks or workspaces are used within the declaring \
289         task or workspace."
290    }
291
292    fn explanation(&self) -> &'static str {
293        "Unused private declarations degrade evaluation performance and reduce the clarity of the \
294         code."
295    }
296
297    fn examples(&self) -> &'static [Example] {
298        &[Example {
299            negative: LabeledSnippet {
300                label: None,
301                snippet: r#"version 1.2
302
303workflow example {
304    String unused = "this will produce a warning"
305}
306"#,
307            },
308            revised: Some(LabeledSnippet {
309                label: Some("Consider removing the declaration entirely"),
310                snippet: r#"version 1.2
311
312workflow example {
313}
314"#,
315            }),
316        }]
317    }
318
319    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
320        Self::EXCEPTABLE_NODES
321    }
322
323    fn deny(&mut self) {
324        self.0 = Severity::Error;
325    }
326
327    fn severity(&self) -> Severity {
328        self.0
329    }
330}
331
332/// Represents the unused call rule.
333#[derive(Debug, Clone, Copy)]
334pub struct UnusedCallRule(Severity);
335
336impl UnusedCallRule {
337    /// See [`Self::exceptable_nodes()`].
338    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
339        SyntaxKind::VersionStatementNode,
340        SyntaxKind::WorkflowDefinitionNode,
341        SyntaxKind::CallStatementNode,
342    ]);
343    /// The rule identifier for unused call warnings.
344    pub const ID: &str = "UnusedCall";
345
346    /// Creates a new unused call rule.
347    pub fn new() -> Self {
348        Self(Severity::Warning)
349    }
350}
351
352impl Default for UnusedCallRule {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358impl Rule for UnusedCallRule {
359    fn id(&self) -> &'static str {
360        Self::ID
361    }
362
363    fn description(&self) -> &'static str {
364        "Ensures that outputs of a call statement are used in the declaring workflow."
365    }
366
367    fn explanation(&self) -> &'static str {
368        "Unused calls may cause unnecessary consumption of compute resources."
369    }
370
371    fn examples(&self) -> &'static [Example] {
372        &[Example {
373            negative: LabeledSnippet {
374                label: None,
375                snippet: r#"version 1.2
376
377workflow example {
378    # The output of `do_work` is never used
379    call do_work
380}
381
382task do_work {
383    command <<<
384    >>>
385
386    output {
387        Int x = 0
388    }
389}
390"#,
391            },
392            revised: Some(LabeledSnippet {
393                label: Some("Consider removing the call entirely"),
394                snippet: r#"version 1.2
395
396workflow example {
397}
398
399task do_work {
400    command <<<
401    >>>
402
403    output {
404        Int x = 0
405    }
406}
407"#,
408            }),
409        }]
410    }
411
412    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
413        Self::EXCEPTABLE_NODES
414    }
415
416    fn deny(&mut self) {
417        self.0 = Severity::Error;
418    }
419
420    fn severity(&self) -> Severity {
421        self.0
422    }
423}
424
425/// Represents the unnecessary call rule.
426#[derive(Debug, Clone, Copy)]
427pub struct UnnecessaryFunctionCall(Severity);
428
429impl UnnecessaryFunctionCall {
430    /// See [`Self::exceptable_nodes()`].
431    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
432        SyntaxKind::VersionStatementNode,
433        SyntaxKind::WorkflowDefinitionNode,
434        SyntaxKind::TaskDefinitionNode,
435        SyntaxKind::BoundDeclNode,
436    ]);
437    /// The rule identifier for unnecessary function call warnings.
438    pub const ID: &str = "UnnecessaryFunctionCall";
439
440    /// Creates a new unnecessary function call rule.
441    pub fn new() -> Self {
442        Self(Severity::Warning)
443    }
444}
445
446impl Default for UnnecessaryFunctionCall {
447    fn default() -> Self {
448        Self::new()
449    }
450}
451
452impl Rule for UnnecessaryFunctionCall {
453    fn id(&self) -> &'static str {
454        Self::ID
455    }
456
457    fn description(&self) -> &'static str {
458        "Ensures that function calls are necessary."
459    }
460
461    fn explanation(&self) -> &'static str {
462        "Unnecessary function calls may impact evaluation performance."
463    }
464
465    fn examples(&self) -> &'static [Example] {
466        &[Example {
467            negative: LabeledSnippet {
468                label: None,
469                snippet: r#"version 1.2
470
471workflow example {
472    # Calls to `defined` on values that are statically
473    # known to be non-None are unnecessary.
474    Boolean exists = defined("hello")
475}
476"#,
477            },
478            revised: None,
479        }]
480    }
481
482    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
483        Self::EXCEPTABLE_NODES
484    }
485
486    fn deny(&mut self) {
487        self.0 = Severity::Error;
488    }
489
490    fn severity(&self) -> Severity {
491        self.0
492    }
493}
494
495/// Represents the using fallback version rule.
496#[derive(Debug, Clone, Copy)]
497pub struct UsingFallbackVersion(Severity);
498
499impl UsingFallbackVersion {
500    /// See [`Self::exceptable_nodes()`].
501    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
502        Some(&[SyntaxKind::VersionStatementNode]);
503    /// The rule identifier for unsupported version fallback warnings.
504    pub const ID: &str = "UsingFallbackVersion";
505
506    /// Creates a new using fallback version rule.
507    pub fn new() -> Self {
508        Self(Severity::Warning)
509    }
510}
511
512impl Default for UsingFallbackVersion {
513    fn default() -> Self {
514        Self::new()
515    }
516}
517
518impl Rule for UsingFallbackVersion {
519    fn id(&self) -> &'static str {
520        Self::ID
521    }
522
523    fn description(&self) -> &'static str {
524        "Warns if interpretation of a document with an unsupported version falls back to a default."
525    }
526
527    fn explanation(&self) -> &'static str {
528        "A document with an unsupported version may have unpredictable behavior if interpreted as \
529         a different version."
530    }
531
532    fn examples(&self) -> &'static [Example] {
533        &[Example {
534            negative: LabeledSnippet {
535                label: None,
536                snippet: r#"# Not a valid version. If a fallback version is configured,
537# the document will be interpreted as that version.
538version development
539
540workflow example {
541}
542"#,
543            },
544            revised: None,
545        }]
546    }
547
548    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
549        Self::EXCEPTABLE_NODES
550    }
551
552    fn deny(&mut self) {
553        self.0 = Severity::Error;
554    }
555
556    fn severity(&self) -> Severity {
557        self.0
558    }
559}
560
561/// Represents the meaningless lint directive rule.
562#[derive(Debug, Clone, Copy)]
563pub struct MeaninglessLintDirective(Severity);
564
565impl MeaninglessLintDirective {
566    /// See [`Self::exceptable_nodes()`].
567    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = None;
568    /// The rule identifier for meaningless lint directive warnings.
569    pub const ID: &str = "MeaninglessLintDirective";
570
571    /// Creates a new meaningless lint directive rule.
572    pub fn new() -> Self {
573        Self(Severity::Note)
574    }
575}
576
577impl Default for MeaninglessLintDirective {
578    fn default() -> Self {
579        Self::new()
580    }
581}
582
583impl Rule for MeaninglessLintDirective {
584    fn id(&self) -> &'static str {
585        Self::ID
586    }
587
588    fn description(&self) -> &'static str {
589        "Warns if an `#@ except:` comment doesn't actually suppress a lint."
590    }
591
592    fn explanation(&self) -> &'static str {
593        "Unused `#@ except:` comments are likely leftovers of refactoring or debugging, and can \
594         reduce the clarity of the code. It is best to remove them."
595    }
596
597    fn examples(&self) -> &'static [Example] {
598        &[Example {
599            negative: LabeledSnippet {
600                label: None,
601                snippet: r#"version 1.3
602
603task do_work {
604    command <<<
605        echo "Lots of hard work!"
606    >>>
607
608    output {
609        String result = read_string(stdout())
610    }
611}
612
613# We except `UnusedCall` unnecessarily.
614workflow calculate {
615    #@ except: UnusedCall
616    call do_work
617
618    output {
619        # We're using the result here!
620        String result = do_work.result
621    }
622}
623"#,
624            },
625            revised: Some(LabeledSnippet {
626                label: Some("Consider removing the unused exception"),
627                snippet: r#"version 1.3
628
629task do_work {
630    command <<<
631        echo "Lots of hard work!"
632    >>>
633
634    output {
635        String result = read_string(stdout())
636    }
637}
638
639workflow calculate {
640    call do_work
641
642    output {
643        String result = do_work.result
644    }
645}
646"#,
647            }),
648        }]
649    }
650
651    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
652        Self::EXCEPTABLE_NODES
653    }
654
655    fn deny(&mut self) {
656        self.0 = Severity::Error;
657    }
658
659    fn severity(&self) -> Severity {
660        self.0
661    }
662}
663
664/// Represents the using misleading declaration order rule.
665#[derive(Debug, Clone, Copy)]
666pub struct MisleadingDeclarationOrderRule(Severity);
667
668impl MisleadingDeclarationOrderRule {
669    /// See [`Self::exceptable_nodes()`].
670    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
671        Some(&[SyntaxKind::TaskDefinitionNode, SyntaxKind::BoundDeclNode]);
672    /// The rule identifier for misleading declaration order warnings.
673    pub const ID: &str = "MisleadingDeclarationOrder";
674
675    /// Creates a new misleading declaration order rule.
676    pub fn new() -> Self {
677        Self(Severity::Warning)
678    }
679}
680
681impl Default for MisleadingDeclarationOrderRule {
682    fn default() -> Self {
683        Self::new()
684    }
685}
686
687impl Rule for MisleadingDeclarationOrderRule {
688    fn id(&self) -> &'static str {
689        Self::ID
690    }
691
692    fn description(&self) -> &'static str {
693        "Warns when a variable declaration is placed after a `command` block."
694    }
695
696    fn explanation(&self) -> &'static str {
697        "WDL tasks are evaluated based on their dependency graph, not top-to-bottom. Variable \
698         declarations that appear after `command` sections are visually misleading, as they will \
699         still be evaluated _before_ the command is executed."
700    }
701
702    fn examples(&self) -> &'static [Example] {
703        &[Example {
704            negative: LabeledSnippet {
705                label: None,
706                snippet: r#"version 1.2
707
708task greet {
709    String greeting = "Hello"
710
711    command <<<
712        echo "~{greeting}, ~{name}!"
713    >>>
714
715    String name = "World"
716}
717"#,
718            },
719            revised: Some(LabeledSnippet {
720                label: None,
721                snippet: r#"version 1.2
722
723task greet {
724    String greeting = "Hello"
725    String name = "World"
726
727    command <<<
728        echo "~{greeting}, ~{name}!"
729    >>>
730}
731"#,
732            }),
733        }]
734    }
735
736    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
737        Self::EXCEPTABLE_NODES
738    }
739
740    fn deny(&mut self) {
741        self.0 = Severity::Error;
742    }
743
744    fn severity(&self) -> Severity {
745        self.0
746    }
747}
748
749/// Detects unknown rules within lint directives.
750#[derive(Debug, Clone, Copy)]
751pub struct KnownRulesRule(Severity);
752
753impl KnownRulesRule {
754    /// See [`Self::exceptable_nodes()`].
755    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
756        Some(&[SyntaxKind::VersionStatementNode]);
757    /// The rule identifier for known rules warnings.
758    pub const ID: &str = "KnownRules";
759
760    /// Creates a new "known rules" rule.
761    pub fn new() -> Self {
762        Self(Severity::Note)
763    }
764}
765
766impl Default for KnownRulesRule {
767    fn default() -> Self {
768        Self::new()
769    }
770}
771
772impl Rule for KnownRulesRule {
773    fn id(&self) -> &'static str {
774        Self::ID
775    }
776
777    fn description(&self) -> &'static str {
778        "Ensures only known rules are used in `except` directives."
779    }
780
781    fn explanation(&self) -> &'static str {
782        "When writing WDL, `except` directives are used to suppress certain rules. If a rule is \
783         unknown, nothing will be suppressed. This rule flags unknown rules as they are often \
784         mistakes."
785    }
786
787    fn examples(&self) -> &'static [Example] {
788        &[Example {
789            negative: LabeledSnippet {
790                label: None,
791                snippet: r#"#@ except: LintThatDoesNotExist
792version 1.2
793
794workflow example {
795}
796"#,
797            },
798            revised: Some(LabeledSnippet {
799                label: None,
800                snippet: r#"version 1.2
801
802workflow example {
803}
804"#,
805            }),
806        }]
807    }
808
809    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
810        Self::EXCEPTABLE_NODES
811    }
812
813    fn deny(&mut self) {
814        self.0 = Severity::Error;
815    }
816
817    fn severity(&self) -> Severity {
818        self.0
819    }
820}