Skip to main content

wdl_analysis/
rules.rs

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