1use std::sync::LazyLock;
4
5use wdl_ast::Severity;
6
7pub 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#[derive(Copy, Clone, Debug)]
16pub struct LabeledSnippet {
17 pub label: Option<&'static str>,
19 pub snippet: &'static str,
21}
22
23#[derive(Copy, Clone, Debug)]
25pub struct Example {
26 pub negative: LabeledSnippet,
28 pub revised: Option<LabeledSnippet>,
30}
31
32pub trait Rule: Send + Sync {
34 fn id(&self) -> &'static str;
39
40 fn description(&self) -> &'static str;
42
43 fn explanation(&self) -> &'static str;
45
46 fn examples(&self) -> &'static [Example];
48
49 fn deny(&mut self);
53
54 fn severity(&self) -> Severity;
56}
57
58pub 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 #[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#[derive(Debug, Clone, Copy)]
92pub struct UnusedImportRule(Severity);
93
94impl UnusedImportRule {
95 pub const ID: &'static str = "UnusedImport";
97
98 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#[derive(Debug, Clone, Copy)]
158pub struct UnusedInputRule(Severity);
159
160impl UnusedInputRule {
161 pub const ID: &str = "UnusedInput";
163
164 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#[derive(Debug, Clone, Copy)]
227pub struct UnusedDeclarationRule(Severity);
228
229impl UnusedDeclarationRule {
230 pub const ID: &str = "UnusedDeclaration";
232
233 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#[derive(Debug, Clone, Copy)]
293pub struct UnusedCallRule(Severity);
294
295impl UnusedCallRule {
296 pub const ID: &str = "UnusedCall";
298
299 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#[derive(Debug, Clone, Copy)]
376pub struct UnnecessaryFunctionCall(Severity);
377
378impl UnnecessaryFunctionCall {
379 pub const ID: &str = "UnnecessaryFunctionCall";
381
382 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#[derive(Debug, Clone, Copy)]
435pub struct UsingFallbackVersion(Severity);
436
437impl UsingFallbackVersion {
438 pub const ID: &str = "UsingFallbackVersion";
440
441 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#[derive(Debug, Clone, Copy)]
494pub struct MisleadingDeclarationOrderRule(Severity);
495
496impl MisleadingDeclarationOrderRule {
497 pub const ID: &str = "MisleadingDeclarationOrder";
499
500 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}