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