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/// The rule identifier for unused import warnings.
9pub const UNUSED_IMPORT_RULE_ID: &str = "UnusedImport";
10
11/// The rule identifier for unused input warnings.
12pub const UNUSED_INPUT_RULE_ID: &str = "UnusedInput";
13
14/// The rule identifier for unused declaration warnings.
15pub const UNUSED_DECL_RULE_ID: &str = "UnusedDeclaration";
16
17/// The rule identifier for unused call warnings.
18pub const UNUSED_CALL_RULE_ID: &str = "UnusedCall";
19
20/// The rule identifier for unnecessary function call warnings.
21pub const UNNECESSARY_FUNCTION_CALL: &str = "UnnecessaryFunctionCall";
22
23/// The rule identifier for unsupported version fallback warnings.
24pub const USING_FALLBACK_VERSION: &str = "UsingFallbackVersion";
25
26/// All rule IDs sorted alphabetically.
27pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
28    let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
29    ids.sort();
30    ids
31});
32
33/// A labeled WDL code snippet.
34#[derive(Copy, Clone, Debug, Serialize)]
35pub struct LabeledSnippet {
36    /// A label for the snippet.
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub label: Option<&'static str>,
39    /// A WDL code snippet.
40    pub snippet: &'static str,
41}
42
43/// A lint rule example.
44#[derive(Copy, Clone, Debug, Serialize)]
45pub struct Example {
46    /// A snippet that will trigger the target lint rule.
47    pub negative: LabeledSnippet,
48    /// A revision of the negative snippet that will no longer trigger the rule.
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub revised: Option<LabeledSnippet>,
51}
52
53/// A trait implemented by analysis rules.
54pub trait Rule: Send + Sync {
55    /// The unique identifier for the rule.
56    ///
57    /// The identifier is required to be pascal case and it is the identifier by
58    /// which a rule is excepted or denied.
59    fn id(&self) -> &'static str;
60
61    /// A short, single sentence description of the rule.
62    fn description(&self) -> &'static str;
63
64    /// Get the long-form explanation of the rule.
65    fn explanation(&self) -> &'static str;
66
67    /// Get a list of examples that would trigger this rule.
68    fn examples(&self) -> &'static [Example];
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    ];
89
90    // Ensure all the rule ids are unique and pascal case
91    #[cfg(debug_assertions)]
92    {
93        use convert_case::Case;
94        use convert_case::Casing;
95        let mut set = std::collections::HashSet::new();
96        for r in rules.iter() {
97            if r.id().to_case(Case::Pascal) != r.id() {
98                panic!("analysis rule id `{id}` is not pascal case", id = r.id());
99            }
100
101            if !set.insert(r.id()) {
102                panic!("duplicate rule id `{id}`", id = r.id());
103            }
104        }
105    }
106
107    rules
108}
109
110/// Represents the unused import rule.
111#[derive(Debug, Clone, Copy)]
112pub struct UnusedImportRule(Severity);
113
114impl UnusedImportRule {
115    /// Creates a new unused import rule.
116    pub fn new() -> Self {
117        Self(Severity::Warning)
118    }
119}
120
121impl Default for UnusedImportRule {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl Rule for UnusedImportRule {
128    fn id(&self) -> &'static str {
129        UNUSED_IMPORT_RULE_ID
130    }
131
132    fn description(&self) -> &'static str {
133        "Ensures that import namespaces are used in the importing document."
134    }
135
136    fn explanation(&self) -> &'static str {
137        "Imported WDL documents should be used in the document that imports them. Unused imports \
138         impact parsing and evaluation performance."
139    }
140
141    fn examples(&self) -> &'static [Example] {
142        &[Example {
143            negative: LabeledSnippet {
144                label: None,
145                snippet: r#"version 1.2
146
147import "foo.wdl"
148
149workflow example {
150}
151"#,
152            },
153            revised: Some(LabeledSnippet {
154                label: Some("Consider removing the import entirely"),
155                snippet: r#"version 1.2
156
157workflow example {
158}
159"#,
160            }),
161        }]
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    /// Creates a new unused input rule.
179    pub fn new() -> Self {
180        Self(Severity::Warning)
181    }
182}
183
184impl Default for UnusedInputRule {
185    fn default() -> Self {
186        Self::new()
187    }
188}
189
190impl Rule for UnusedInputRule {
191    fn id(&self) -> &'static str {
192        UNUSED_INPUT_RULE_ID
193    }
194
195    fn description(&self) -> &'static str {
196        "Ensures that task or workspace inputs are used within the declaring task or workspace."
197    }
198
199    fn explanation(&self) -> &'static str {
200        "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
201         file inputs in tasks can also cause unnecessary file localizations."
202    }
203
204    fn examples(&self) -> &'static [Example] {
205        &[Example {
206            negative: LabeledSnippet {
207                label: None,
208                snippet: r#"version 1.2
209
210workflow example {
211    input {
212        String unused
213    }
214}
215"#,
216            },
217            revised: Some(LabeledSnippet {
218                label: Some("Consider removing the input entirely"),
219                snippet: r#"version 1.2
220
221workflow example {
222    input {
223    }
224}
225"#,
226            }),
227        }]
228    }
229
230    fn deny(&mut self) {
231        self.0 = Severity::Error;
232    }
233
234    fn severity(&self) -> Severity {
235        self.0
236    }
237}
238
239/// Represents the unused declaration rule.
240#[derive(Debug, Clone, Copy)]
241pub struct UnusedDeclarationRule(Severity);
242
243impl UnusedDeclarationRule {
244    /// Creates a new unused declaration rule.
245    pub fn new() -> Self {
246        Self(Severity::Warning)
247    }
248}
249
250impl Default for UnusedDeclarationRule {
251    fn default() -> Self {
252        Self::new()
253    }
254}
255
256impl Rule for UnusedDeclarationRule {
257    fn id(&self) -> &'static str {
258        UNUSED_DECL_RULE_ID
259    }
260
261    fn description(&self) -> &'static str {
262        "Ensures that private declarations in tasks or workspaces are used within the declaring \
263         task or workspace."
264    }
265
266    fn explanation(&self) -> &'static str {
267        "Unused private declarations degrade evaluation performance and reduce the clarity of the \
268         code."
269    }
270
271    fn examples(&self) -> &'static [Example] {
272        &[Example {
273            negative: LabeledSnippet {
274                label: None,
275                snippet: r#"version 1.2
276
277workflow example {
278    String unused = "this will produce a warning"
279}
280"#,
281            },
282            revised: Some(LabeledSnippet {
283                label: Some("Consider removing the declaration entirely"),
284                snippet: r#"version 1.2
285
286workflow example {
287}
288"#,
289            }),
290        }]
291    }
292
293    fn deny(&mut self) {
294        self.0 = Severity::Error;
295    }
296
297    fn severity(&self) -> Severity {
298        self.0
299    }
300}
301
302/// Represents the unused call rule.
303#[derive(Debug, Clone, Copy)]
304pub struct UnusedCallRule(Severity);
305
306impl UnusedCallRule {
307    /// Creates a new unused call rule.
308    pub fn new() -> Self {
309        Self(Severity::Warning)
310    }
311}
312
313impl Default for UnusedCallRule {
314    fn default() -> Self {
315        Self::new()
316    }
317}
318
319impl Rule for UnusedCallRule {
320    fn id(&self) -> &'static str {
321        UNUSED_CALL_RULE_ID
322    }
323
324    fn description(&self) -> &'static str {
325        "Ensures that outputs of a call statement are used in the declaring workflow."
326    }
327
328    fn explanation(&self) -> &'static str {
329        "Unused calls may cause unnecessary consumption of compute resources."
330    }
331
332    fn examples(&self) -> &'static [Example] {
333        &[Example {
334            negative: LabeledSnippet {
335                label: None,
336                snippet: r#"version 1.2
337
338workflow example {
339    # The output of `do_work` is never used
340    call do_work
341}
342
343task do_work {
344    command <<<
345    >>>
346
347    output {
348        Int x = 0
349    }
350}
351"#,
352            },
353            revised: Some(LabeledSnippet {
354                label: Some("Consider removing the call entirely"),
355                snippet: r#"version 1.2
356
357workflow example {
358}
359
360task do_work {
361    command <<<
362    >>>
363
364    output {
365        Int x = 0
366    }
367}
368"#,
369            }),
370        }]
371    }
372
373    fn deny(&mut self) {
374        self.0 = Severity::Error;
375    }
376
377    fn severity(&self) -> Severity {
378        self.0
379    }
380}
381
382/// Represents the unnecessary call rule.
383#[derive(Debug, Clone, Copy)]
384pub struct UnnecessaryFunctionCall(Severity);
385
386impl UnnecessaryFunctionCall {
387    /// Creates a new unnecessary function call rule.
388    pub fn new() -> Self {
389        Self(Severity::Warning)
390    }
391}
392
393impl Default for UnnecessaryFunctionCall {
394    fn default() -> Self {
395        Self::new()
396    }
397}
398
399impl Rule for UnnecessaryFunctionCall {
400    fn id(&self) -> &'static str {
401        UNNECESSARY_FUNCTION_CALL
402    }
403
404    fn description(&self) -> &'static str {
405        "Ensures that function calls are necessary."
406    }
407
408    fn explanation(&self) -> &'static str {
409        "Unnecessary function calls may impact evaluation performance."
410    }
411
412    fn examples(&self) -> &'static [Example] {
413        &[Example {
414            negative: LabeledSnippet {
415                label: None,
416                snippet: r#"version 1.2
417
418workflow example {
419    # Calls to `defined` on values that are statically
420    # known to be non-None are unnecessary.
421    Boolean exists = defined("hello")
422}
423"#,
424            },
425            revised: None,
426        }]
427    }
428
429    fn deny(&mut self) {
430        self.0 = Severity::Error;
431    }
432
433    fn severity(&self) -> Severity {
434        self.0
435    }
436}
437
438/// Represents the using fallback version rule.
439#[derive(Debug, Clone, Copy)]
440pub struct UsingFallbackVersion(Severity);
441
442impl UsingFallbackVersion {
443    /// Creates a new using fallback version rule.
444    pub fn new() -> Self {
445        Self(Severity::Warning)
446    }
447}
448
449impl Default for UsingFallbackVersion {
450    fn default() -> Self {
451        Self::new()
452    }
453}
454
455impl Rule for UsingFallbackVersion {
456    fn id(&self) -> &'static str {
457        USING_FALLBACK_VERSION
458    }
459
460    fn description(&self) -> &'static str {
461        "Warns if interpretation of a document with an unsupported version falls back to a default."
462    }
463
464    fn explanation(&self) -> &'static str {
465        "A document with an unsupported version may have unpredictable behavior if interpreted as \
466         a different version."
467    }
468
469    fn examples(&self) -> &'static [Example] {
470        &[Example {
471            negative: LabeledSnippet {
472                label: None,
473                snippet: r#"# Not a valid version. If a fallback version is configured,
474# the document will be interpreted as that version.
475version development
476
477workflow example {
478}
479"#,
480            },
481            revised: None,
482        }]
483    }
484
485    fn deny(&mut self) {
486        self.0 = Severity::Error;
487    }
488
489    fn severity(&self) -> Severity {
490        self.0
491    }
492}