1use std::sync::LazyLock;
4
5use serde::Serialize;
6use wdl_ast::Severity;
7
8pub const UNUSED_IMPORT_RULE_ID: &str = "UnusedImport";
10
11pub const UNUSED_INPUT_RULE_ID: &str = "UnusedInput";
13
14pub const UNUSED_DECL_RULE_ID: &str = "UnusedDeclaration";
16
17pub const UNUSED_CALL_RULE_ID: &str = "UnusedCall";
19
20pub const UNNECESSARY_FUNCTION_CALL: &str = "UnnecessaryFunctionCall";
22
23pub const USING_FALLBACK_VERSION: &str = "UsingFallbackVersion";
25
26pub 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#[derive(Copy, Clone, Debug, Serialize)]
35pub struct LabeledSnippet {
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub label: Option<&'static str>,
39 pub snippet: &'static str,
41}
42
43#[derive(Copy, Clone, Debug, Serialize)]
45pub struct Example {
46 pub negative: LabeledSnippet,
48 #[serde(skip_serializing_if = "Option::is_none")]
50 pub revised: Option<LabeledSnippet>,
51}
52
53pub trait Rule: Send + Sync {
55 fn id(&self) -> &'static str;
60
61 fn description(&self) -> &'static str;
63
64 fn explanation(&self) -> &'static str;
66
67 fn examples(&self) -> &'static [Example];
69
70 fn deny(&mut self);
74
75 fn severity(&self) -> Severity;
77}
78
79pub 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 #[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#[derive(Debug, Clone, Copy)]
112pub struct UnusedImportRule(Severity);
113
114impl UnusedImportRule {
115 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#[derive(Debug, Clone, Copy)]
175pub struct UnusedInputRule(Severity);
176
177impl UnusedInputRule {
178 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#[derive(Debug, Clone, Copy)]
241pub struct UnusedDeclarationRule(Severity);
242
243impl UnusedDeclarationRule {
244 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#[derive(Debug, Clone, Copy)]
304pub struct UnusedCallRule(Severity);
305
306impl UnusedCallRule {
307 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#[derive(Debug, Clone, Copy)]
384pub struct UnnecessaryFunctionCall(Severity);
385
386impl UnnecessaryFunctionCall {
387 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#[derive(Debug, Clone, Copy)]
440pub struct UsingFallbackVersion(Severity);
441
442impl UsingFallbackVersion {
443 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}