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/// The rule identifier for unused import warnings.
8pub const UNUSED_IMPORT_RULE_ID: &str = "UnusedImport";
9
10/// The rule identifier for unused input warnings.
11pub const UNUSED_INPUT_RULE_ID: &str = "UnusedInput";
12
13/// The rule identifier for unused declaration warnings.
14pub const UNUSED_DECL_RULE_ID: &str = "UnusedDeclaration";
15
16/// The rule identifier for unused call warnings.
17pub const UNUSED_CALL_RULE_ID: &str = "UnusedCall";
18
19/// The rule identifier for unnecessary function call warnings.
20pub const UNNECESSARY_FUNCTION_CALL: &str = "UnnecessaryFunctionCall";
21
22/// The rule identifier for unsupported version fallback warnings.
23pub const USING_FALLBACK_VERSION: &str = "UsingFallbackVersion";
24
25/// All rule IDs sorted alphabetically.
26pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
27    let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
28    ids.sort();
29    ids
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    /// Denies the rule.
47    ///
48    /// Denying the rule treats any diagnostics it emits as an error.
49    fn deny(&mut self);
50
51    /// Gets the severity of the rule.
52    fn severity(&self) -> Severity;
53}
54
55/// Gets the list of all analysis rules.
56pub fn rules() -> Vec<Box<dyn Rule>> {
57    let rules: Vec<Box<dyn Rule>> = vec![
58        Box::<UnusedImportRule>::default(),
59        Box::<UnusedInputRule>::default(),
60        Box::<UnusedDeclarationRule>::default(),
61        Box::<UnusedCallRule>::default(),
62        Box::<UnnecessaryFunctionCall>::default(),
63        Box::<UsingFallbackVersion>::default(),
64    ];
65
66    // Ensure all the rule ids are unique and pascal case
67    #[cfg(debug_assertions)]
68    {
69        use convert_case::Case;
70        use convert_case::Casing;
71        let mut set = std::collections::HashSet::new();
72        for r in rules.iter() {
73            if r.id().to_case(Case::Pascal) != r.id() {
74                panic!("analysis rule id `{id}` is not pascal case", id = r.id());
75            }
76
77            if !set.insert(r.id()) {
78                panic!("duplicate rule id `{id}`", id = r.id());
79            }
80        }
81    }
82
83    rules
84}
85
86/// Represents the unused import rule.
87#[derive(Debug, Clone, Copy)]
88pub struct UnusedImportRule(Severity);
89
90impl UnusedImportRule {
91    /// Creates a new unused import rule.
92    pub fn new() -> Self {
93        Self(Severity::Warning)
94    }
95}
96
97impl Default for UnusedImportRule {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl Rule for UnusedImportRule {
104    fn id(&self) -> &'static str {
105        UNUSED_IMPORT_RULE_ID
106    }
107
108    fn description(&self) -> &'static str {
109        "Ensures that import namespaces are used in the importing document."
110    }
111
112    fn explanation(&self) -> &'static str {
113        "Imported WDL documents should be used in the document that imports them. Unused imports \
114         impact parsing and evaluation performance."
115    }
116
117    fn deny(&mut self) {
118        self.0 = Severity::Error;
119    }
120
121    fn severity(&self) -> Severity {
122        self.0
123    }
124}
125
126/// Represents the unused input rule.
127#[derive(Debug, Clone, Copy)]
128pub struct UnusedInputRule(Severity);
129
130impl UnusedInputRule {
131    /// Creates a new unused input rule.
132    pub fn new() -> Self {
133        Self(Severity::Warning)
134    }
135}
136
137impl Default for UnusedInputRule {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl Rule for UnusedInputRule {
144    fn id(&self) -> &'static str {
145        UNUSED_INPUT_RULE_ID
146    }
147
148    fn description(&self) -> &'static str {
149        "Ensures that task or workspace inputs are used within the declaring task or workspace."
150    }
151
152    fn explanation(&self) -> &'static str {
153        "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
154         file inputs in tasks can also cause unnecessary file localizations."
155    }
156
157    fn deny(&mut self) {
158        self.0 = Severity::Error;
159    }
160
161    fn severity(&self) -> Severity {
162        self.0
163    }
164}
165
166/// Represents the unused declaration rule.
167#[derive(Debug, Clone, Copy)]
168pub struct UnusedDeclarationRule(Severity);
169
170impl UnusedDeclarationRule {
171    /// Creates a new unused declaration rule.
172    pub fn new() -> Self {
173        Self(Severity::Warning)
174    }
175}
176
177impl Default for UnusedDeclarationRule {
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl Rule for UnusedDeclarationRule {
184    fn id(&self) -> &'static str {
185        UNUSED_DECL_RULE_ID
186    }
187
188    fn description(&self) -> &'static str {
189        "Ensures that private declarations in tasks or workspaces are used within the declaring \
190         task or workspace."
191    }
192
193    fn explanation(&self) -> &'static str {
194        "Unused private declarations degrade evaluation performance and reduce the clarity of the \
195         code."
196    }
197
198    fn deny(&mut self) {
199        self.0 = Severity::Error;
200    }
201
202    fn severity(&self) -> Severity {
203        self.0
204    }
205}
206
207/// Represents the unused call rule.
208#[derive(Debug, Clone, Copy)]
209pub struct UnusedCallRule(Severity);
210
211impl UnusedCallRule {
212    /// Creates a new unused call rule.
213    pub fn new() -> Self {
214        Self(Severity::Warning)
215    }
216}
217
218impl Default for UnusedCallRule {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl Rule for UnusedCallRule {
225    fn id(&self) -> &'static str {
226        UNUSED_CALL_RULE_ID
227    }
228
229    fn description(&self) -> &'static str {
230        "Ensures that outputs of a call statement are used in the declaring workflow."
231    }
232
233    fn explanation(&self) -> &'static str {
234        "Unused calls may cause unnecessary consumption of compute resources."
235    }
236
237    fn deny(&mut self) {
238        self.0 = Severity::Error;
239    }
240
241    fn severity(&self) -> Severity {
242        self.0
243    }
244}
245
246/// Represents the unnecessary call rule.
247#[derive(Debug, Clone, Copy)]
248pub struct UnnecessaryFunctionCall(Severity);
249
250impl UnnecessaryFunctionCall {
251    /// Creates a new unnecessary function call rule.
252    pub fn new() -> Self {
253        Self(Severity::Warning)
254    }
255}
256
257impl Default for UnnecessaryFunctionCall {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263impl Rule for UnnecessaryFunctionCall {
264    fn id(&self) -> &'static str {
265        UNNECESSARY_FUNCTION_CALL
266    }
267
268    fn description(&self) -> &'static str {
269        "Ensures that function calls are necessary."
270    }
271
272    fn explanation(&self) -> &'static str {
273        "Unnecessary function calls may impact evaluation performance."
274    }
275
276    fn deny(&mut self) {
277        self.0 = Severity::Error;
278    }
279
280    fn severity(&self) -> Severity {
281        self.0
282    }
283}
284
285/// Represents the using fallback version rule.
286#[derive(Debug, Clone, Copy)]
287pub struct UsingFallbackVersion(Severity);
288
289impl UsingFallbackVersion {
290    /// Creates a new using fallback version rule.
291    pub fn new() -> Self {
292        Self(Severity::Warning)
293    }
294}
295
296impl Default for UsingFallbackVersion {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302impl Rule for UsingFallbackVersion {
303    fn id(&self) -> &'static str {
304        USING_FALLBACK_VERSION
305    }
306
307    fn description(&self) -> &'static str {
308        "Warns if interpretation of a document with an unsupported version falls back to a default."
309    }
310
311    fn explanation(&self) -> &'static str {
312        "A document with an unsupported version may have unpredictable behavior if interpreted as \
313         a different version."
314    }
315
316    fn deny(&mut self) {
317        self.0 = Severity::Error;
318    }
319
320    fn severity(&self) -> Severity {
321        self.0
322    }
323}