Skip to main content

wdl_analysis/
rules.rs

1//! Implementation of analysis rules.
2
3use std::sync::LazyLock;
4
5use serde::Serialize;
6use wdl_ast::Severity;
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, Serialize)]
17pub struct LabeledSnippet {
18    /// A label for the snippet.
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub label: Option<&'static str>,
21    /// A WDL code snippet.
22    pub snippet: &'static str,
23}
24
25/// A lint rule example.
26#[derive(Copy, Clone, Debug, Serialize)]
27pub struct Example {
28    /// A snippet that will trigger the target lint rule.
29    pub negative: LabeledSnippet,
30    /// A revision of the negative snippet that will no longer trigger the rule.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub revised: Option<LabeledSnippet>,
33}
34
35/// A trait implemented by analysis rules.
36pub trait Rule: Send + Sync {
37    /// The unique identifier for the rule.
38    ///
39    /// The identifier is required to be pascal case and it is the identifier by
40    /// which a rule is excepted or denied.
41    fn id(&self) -> &'static str;
42
43    /// A short, single sentence description of the rule.
44    fn description(&self) -> &'static str;
45
46    /// Get the long-form explanation of the rule.
47    fn explanation(&self) -> &'static str;
48
49    /// Get a list of examples that would trigger this rule.
50    fn examples(&self) -> &'static [Example];
51
52    /// Denies the rule.
53    ///
54    /// Denying the rule treats any diagnostics it emits as an error.
55    fn deny(&mut self);
56
57    /// Gets the severity of the rule.
58    fn severity(&self) -> Severity;
59}
60
61/// Gets the list of all analysis rules.
62pub fn rules() -> Vec<Box<dyn Rule>> {
63    let rules: Vec<Box<dyn Rule>> = vec![
64        Box::<UnusedImportRule>::default(),
65        Box::<UnusedInputRule>::default(),
66        Box::<UnusedDeclarationRule>::default(),
67        Box::<UnusedCallRule>::default(),
68        Box::<UnnecessaryFunctionCall>::default(),
69        Box::<UsingFallbackVersion>::default(),
70        Box::<MisleadingDeclarationOrderRule>::default(),
71    ];
72
73    // Ensure all the rule ids are unique and pascal case
74    #[cfg(debug_assertions)]
75    {
76        use convert_case::Case;
77        use convert_case::Casing;
78        let mut set = std::collections::HashSet::new();
79        for r in rules.iter() {
80            if r.id().to_case(Case::Pascal) != r.id() {
81                panic!("analysis rule id `{id}` is not pascal case", id = r.id());
82            }
83
84            if !set.insert(r.id()) {
85                panic!("duplicate rule id `{id}`", id = r.id());
86            }
87        }
88    }
89
90    rules
91}
92
93/// Represents the unused import rule.
94#[derive(Debug, Clone, Copy)]
95pub struct UnusedImportRule(Severity);
96
97impl UnusedImportRule {
98    /// The rule identifier for unused import warnings.
99    pub const ID: &'static str = "UnusedImport";
100
101    /// Creates a new unused import rule.
102    pub fn new() -> Self {
103        Self(Severity::Warning)
104    }
105}
106
107impl Default for UnusedImportRule {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl Rule for UnusedImportRule {
114    fn id(&self) -> &'static str {
115        Self::ID
116    }
117
118    fn description(&self) -> &'static str {
119        "Ensures that import namespaces are used in the importing document."
120    }
121
122    fn explanation(&self) -> &'static str {
123        "Imported WDL documents should be used in the document that imports them. Unused imports \
124         impact parsing and evaluation performance."
125    }
126
127    fn examples(&self) -> &'static [Example] {
128        &[Example {
129            negative: LabeledSnippet {
130                label: None,
131                snippet: r#"version 1.2
132
133import "foo.wdl"
134
135workflow example {
136}
137"#,
138            },
139            revised: Some(LabeledSnippet {
140                label: Some("Consider removing the import entirely"),
141                snippet: r#"version 1.2
142
143workflow example {
144}
145"#,
146            }),
147        }]
148    }
149
150    fn deny(&mut self) {
151        self.0 = Severity::Error;
152    }
153
154    fn severity(&self) -> Severity {
155        self.0
156    }
157}
158
159/// Represents the unused input rule.
160#[derive(Debug, Clone, Copy)]
161pub struct UnusedInputRule(Severity);
162
163impl UnusedInputRule {
164    /// The rule identifier for unused input warnings.
165    pub const ID: &str = "UnusedInput";
166
167    /// Creates a new unused input rule.
168    pub fn new() -> Self {
169        Self(Severity::Warning)
170    }
171}
172
173impl Default for UnusedInputRule {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179impl Rule for UnusedInputRule {
180    fn id(&self) -> &'static str {
181        Self::ID
182    }
183
184    fn description(&self) -> &'static str {
185        "Ensures that task or workspace inputs are used within the declaring task or workspace."
186    }
187
188    fn explanation(&self) -> &'static str {
189        "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
190         file inputs in tasks can also cause unnecessary file localizations."
191    }
192
193    fn examples(&self) -> &'static [Example] {
194        &[Example {
195            negative: LabeledSnippet {
196                label: None,
197                snippet: r#"version 1.2
198
199workflow example {
200    input {
201        String unused
202    }
203}
204"#,
205            },
206            revised: Some(LabeledSnippet {
207                label: Some("Consider removing the input entirely"),
208                snippet: r#"version 1.2
209
210workflow example {
211    input {
212    }
213}
214"#,
215            }),
216        }]
217    }
218
219    fn deny(&mut self) {
220        self.0 = Severity::Error;
221    }
222
223    fn severity(&self) -> Severity {
224        self.0
225    }
226}
227
228/// Represents the unused declaration rule.
229#[derive(Debug, Clone, Copy)]
230pub struct UnusedDeclarationRule(Severity);
231
232impl UnusedDeclarationRule {
233    /// The rule identifier for unused declaration warnings.
234    pub const ID: &str = "UnusedDeclaration";
235
236    /// Creates a new unused declaration rule.
237    pub fn new() -> Self {
238        Self(Severity::Warning)
239    }
240}
241
242impl Default for UnusedDeclarationRule {
243    fn default() -> Self {
244        Self::new()
245    }
246}
247
248impl Rule for UnusedDeclarationRule {
249    fn id(&self) -> &'static str {
250        Self::ID
251    }
252
253    fn description(&self) -> &'static str {
254        "Ensures that private declarations in tasks or workspaces are used within the declaring \
255         task or workspace."
256    }
257
258    fn explanation(&self) -> &'static str {
259        "Unused private declarations degrade evaluation performance and reduce the clarity of the \
260         code."
261    }
262
263    fn examples(&self) -> &'static [Example] {
264        &[Example {
265            negative: LabeledSnippet {
266                label: None,
267                snippet: r#"version 1.2
268
269workflow example {
270    String unused = "this will produce a warning"
271}
272"#,
273            },
274            revised: Some(LabeledSnippet {
275                label: Some("Consider removing the declaration entirely"),
276                snippet: r#"version 1.2
277
278workflow example {
279}
280"#,
281            }),
282        }]
283    }
284
285    fn deny(&mut self) {
286        self.0 = Severity::Error;
287    }
288
289    fn severity(&self) -> Severity {
290        self.0
291    }
292}
293
294/// Represents the unused call rule.
295#[derive(Debug, Clone, Copy)]
296pub struct UnusedCallRule(Severity);
297
298impl UnusedCallRule {
299    /// The rule identifier for unused call warnings.
300    pub const ID: &str = "UnusedCall";
301
302    /// Creates a new unused call rule.
303    pub fn new() -> Self {
304        Self(Severity::Warning)
305    }
306}
307
308impl Default for UnusedCallRule {
309    fn default() -> Self {
310        Self::new()
311    }
312}
313
314impl Rule for UnusedCallRule {
315    fn id(&self) -> &'static str {
316        Self::ID
317    }
318
319    fn description(&self) -> &'static str {
320        "Ensures that outputs of a call statement are used in the declaring workflow."
321    }
322
323    fn explanation(&self) -> &'static str {
324        "Unused calls may cause unnecessary consumption of compute resources."
325    }
326
327    fn examples(&self) -> &'static [Example] {
328        &[Example {
329            negative: LabeledSnippet {
330                label: None,
331                snippet: r#"version 1.2
332
333workflow example {
334    # The output of `do_work` is never used
335    call do_work
336}
337
338task do_work {
339    command <<<
340    >>>
341
342    output {
343        Int x = 0
344    }
345}
346"#,
347            },
348            revised: Some(LabeledSnippet {
349                label: Some("Consider removing the call entirely"),
350                snippet: r#"version 1.2
351
352workflow example {
353}
354
355task do_work {
356    command <<<
357    >>>
358
359    output {
360        Int x = 0
361    }
362}
363"#,
364            }),
365        }]
366    }
367
368    fn deny(&mut self) {
369        self.0 = Severity::Error;
370    }
371
372    fn severity(&self) -> Severity {
373        self.0
374    }
375}
376
377/// Represents the unnecessary call rule.
378#[derive(Debug, Clone, Copy)]
379pub struct UnnecessaryFunctionCall(Severity);
380
381impl UnnecessaryFunctionCall {
382    /// The rule identifier for unnecessary function call warnings.
383    pub const ID: &str = "UnnecessaryFunctionCall";
384
385    /// Creates a new unnecessary function call rule.
386    pub fn new() -> Self {
387        Self(Severity::Warning)
388    }
389}
390
391impl Default for UnnecessaryFunctionCall {
392    fn default() -> Self {
393        Self::new()
394    }
395}
396
397impl Rule for UnnecessaryFunctionCall {
398    fn id(&self) -> &'static str {
399        Self::ID
400    }
401
402    fn description(&self) -> &'static str {
403        "Ensures that function calls are necessary."
404    }
405
406    fn explanation(&self) -> &'static str {
407        "Unnecessary function calls may impact evaluation performance."
408    }
409
410    fn examples(&self) -> &'static [Example] {
411        &[Example {
412            negative: LabeledSnippet {
413                label: None,
414                snippet: r#"version 1.2
415
416workflow example {
417    # Calls to `defined` on values that are statically
418    # known to be non-None are unnecessary.
419    Boolean exists = defined("hello")
420}
421"#,
422            },
423            revised: None,
424        }]
425    }
426
427    fn deny(&mut self) {
428        self.0 = Severity::Error;
429    }
430
431    fn severity(&self) -> Severity {
432        self.0
433    }
434}
435
436/// Represents the using fallback version rule.
437#[derive(Debug, Clone, Copy)]
438pub struct UsingFallbackVersion(Severity);
439
440impl UsingFallbackVersion {
441    /// The rule identifier for unsupported version fallback warnings.
442    pub const ID: &str = "UsingFallbackVersion";
443
444    /// Creates a new using fallback version rule.
445    pub fn new() -> Self {
446        Self(Severity::Warning)
447    }
448}
449
450impl Default for UsingFallbackVersion {
451    fn default() -> Self {
452        Self::new()
453    }
454}
455
456impl Rule for UsingFallbackVersion {
457    fn id(&self) -> &'static str {
458        Self::ID
459    }
460
461    fn description(&self) -> &'static str {
462        "Warns if interpretation of a document with an unsupported version falls back to a default."
463    }
464
465    fn explanation(&self) -> &'static str {
466        "A document with an unsupported version may have unpredictable behavior if interpreted as \
467         a different version."
468    }
469
470    fn examples(&self) -> &'static [Example] {
471        &[Example {
472            negative: LabeledSnippet {
473                label: None,
474                snippet: r#"# Not a valid version. If a fallback version is configured,
475# the document will be interpreted as that version.
476version development
477
478workflow example {
479}
480"#,
481            },
482            revised: None,
483        }]
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 misleading declaration order rule.
496#[derive(Debug, Clone, Copy)]
497pub struct MisleadingDeclarationOrderRule(Severity);
498
499impl MisleadingDeclarationOrderRule {
500    /// The rule identifier for misleading declaration order warnings.
501    pub const ID: &str = "MisleadingDeclarationOrder";
502
503    /// Creates a new misleading declaration order rule.
504    pub fn new() -> Self {
505        Self(Severity::Warning)
506    }
507}
508
509impl Default for MisleadingDeclarationOrderRule {
510    fn default() -> Self {
511        Self::new()
512    }
513}
514
515impl Rule for MisleadingDeclarationOrderRule {
516    fn id(&self) -> &'static str {
517        Self::ID
518    }
519
520    fn description(&self) -> &'static str {
521        "Warns when a variable declaration is placed after a `command` block."
522    }
523
524    fn explanation(&self) -> &'static str {
525        "WDL tasks are evaluated based on their dependency graph, not top-to-bottom. Variable \
526         declarations that appear after `command` sections are visually misleading, as they will \
527         still be evaluated _before_ the command is executed."
528    }
529
530    fn examples(&self) -> &'static [Example] {
531        &[Example {
532            negative: LabeledSnippet {
533                label: None,
534                snippet: r#"version 1.2
535
536task greet {
537    String greeting = "Hello"
538
539    command <<<
540        echo "~{greeting}, ~{name}!"
541    >>>
542
543    String name = "World"
544}
545"#,
546            },
547            revised: Some(LabeledSnippet {
548                label: None,
549                snippet: r#"version 1.2
550
551task greet {
552    String greeting = "Hello"
553    String name = "World"
554
555    command <<<
556        echo "~{greeting}, ~{name}!"
557    >>>
558}
559"#,
560            }),
561        }]
562    }
563
564    fn deny(&mut self) {
565        self.0 = Severity::Error;
566    }
567
568    fn severity(&self) -> Severity {
569        self.0
570    }
571}