1use std::sync::LazyLock;
4
5use wdl_ast::Severity;
6use wdl_grammar::SyntaxKind;
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)]
17pub struct LabeledSnippet {
18 pub label: Option<&'static str>,
20 pub snippet: &'static str,
22}
23
24#[derive(Copy, Clone, Debug)]
26pub struct Example {
27 pub negative: LabeledSnippet,
29 pub revised: Option<LabeledSnippet>,
31}
32
33pub trait Rule: Send + Sync {
35 fn id(&self) -> &'static str;
40
41 fn description(&self) -> &'static str;
43
44 fn explanation(&self) -> &'static str;
46
47 fn examples(&self) -> &'static [Example];
49
50 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
54
55 fn deny(&mut self);
59
60 fn severity(&self) -> Severity;
62}
63
64pub fn rules() -> Vec<Box<dyn Rule>> {
66 let rules: Vec<Box<dyn Rule>> = vec![
67 Box::<UnusedImportRule>::default(),
68 Box::<UnusedInputRule>::default(),
69 Box::<UnusedDeclarationRule>::default(),
70 Box::<UnusedCallRule>::default(),
71 Box::<UnnecessaryFunctionCall>::default(),
72 Box::<UsingFallbackVersion>::default(),
73 Box::<MisleadingDeclarationOrderRule>::default(),
74 Box::<MeaninglessLintDirective>::default(),
75 Box::<KnownRulesRule>::default(),
76 ];
77
78 #[cfg(debug_assertions)]
80 {
81 use convert_case::Case;
82 use convert_case::Casing;
83 let mut set = std::collections::HashSet::new();
84 for r in rules.iter() {
85 if r.id().to_case(Case::Pascal) != r.id() {
86 panic!("analysis rule id `{id}` is not pascal case", id = r.id());
87 }
88
89 if !set.insert(r.id()) {
90 panic!("duplicate rule id `{id}`", id = r.id());
91 }
92 }
93 }
94
95 rules
96}
97
98#[derive(Debug, Clone, Copy)]
100pub struct UnusedImportRule(Severity);
101
102impl UnusedImportRule {
103 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
105 SyntaxKind::VersionStatementNode,
106 SyntaxKind::ImportStatementNode,
107 ]);
108 pub const ID: &'static str = "UnusedImport";
110
111 pub fn new() -> Self {
113 Self(Severity::Warning)
114 }
115}
116
117impl Default for UnusedImportRule {
118 fn default() -> Self {
119 Self::new()
120 }
121}
122
123impl Rule for UnusedImportRule {
124 fn id(&self) -> &'static str {
125 Self::ID
126 }
127
128 fn description(&self) -> &'static str {
129 "Ensures that import namespaces are used in the importing document."
130 }
131
132 fn explanation(&self) -> &'static str {
133 "Imported WDL documents should be used in the document that imports them. Unused imports \
134 impact parsing and evaluation performance."
135 }
136
137 fn examples(&self) -> &'static [Example] {
138 &[Example {
139 negative: LabeledSnippet {
140 label: None,
141 snippet: r#"version 1.2
142
143import "foo.wdl"
144
145workflow example {
146}
147"#,
148 },
149 revised: Some(LabeledSnippet {
150 label: Some("Consider removing the import entirely"),
151 snippet: r#"version 1.2
152
153workflow example {
154}
155"#,
156 }),
157 }]
158 }
159
160 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
161 Self::EXCEPTABLE_NODES
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 const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
180 SyntaxKind::VersionStatementNode,
181 SyntaxKind::WorkflowDefinitionNode,
182 SyntaxKind::TaskDefinitionNode,
183 SyntaxKind::BoundDeclNode,
184 SyntaxKind::UnboundDeclNode,
185 ]);
186 pub const ID: &str = "UnusedInput";
188
189 pub fn new() -> Self {
191 Self(Severity::Warning)
192 }
193}
194
195impl Default for UnusedInputRule {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl Rule for UnusedInputRule {
202 fn id(&self) -> &'static str {
203 Self::ID
204 }
205
206 fn description(&self) -> &'static str {
207 "Ensures that task or workspace inputs are used within the declaring task or workspace."
208 }
209
210 fn explanation(&self) -> &'static str {
211 "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
212 file inputs in tasks can also cause unnecessary file localizations."
213 }
214
215 fn examples(&self) -> &'static [Example] {
216 &[Example {
217 negative: LabeledSnippet {
218 label: None,
219 snippet: r#"version 1.2
220
221workflow example {
222 input {
223 String unused
224 }
225}
226"#,
227 },
228 revised: Some(LabeledSnippet {
229 label: Some("Consider removing the input entirely"),
230 snippet: r#"version 1.2
231
232workflow example {
233 input {
234 }
235}
236"#,
237 }),
238 }]
239 }
240
241 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
242 Self::EXCEPTABLE_NODES
243 }
244
245 fn deny(&mut self) {
246 self.0 = Severity::Error;
247 }
248
249 fn severity(&self) -> Severity {
250 self.0
251 }
252}
253
254#[derive(Debug, Clone, Copy)]
256pub struct UnusedDeclarationRule(Severity);
257
258impl UnusedDeclarationRule {
259 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
261 SyntaxKind::VersionStatementNode,
262 SyntaxKind::WorkflowDefinitionNode,
263 SyntaxKind::TaskDefinitionNode,
264 SyntaxKind::BoundDeclNode,
265 SyntaxKind::UnboundDeclNode,
266 ]);
267 pub const ID: &str = "UnusedDeclaration";
269
270 pub fn new() -> Self {
272 Self(Severity::Warning)
273 }
274}
275
276impl Default for UnusedDeclarationRule {
277 fn default() -> Self {
278 Self::new()
279 }
280}
281
282impl Rule for UnusedDeclarationRule {
283 fn id(&self) -> &'static str {
284 Self::ID
285 }
286
287 fn description(&self) -> &'static str {
288 "Ensures that private declarations in tasks or workspaces are used within the declaring \
289 task or workspace."
290 }
291
292 fn explanation(&self) -> &'static str {
293 "Unused private declarations degrade evaluation performance and reduce the clarity of the \
294 code."
295 }
296
297 fn examples(&self) -> &'static [Example] {
298 &[Example {
299 negative: LabeledSnippet {
300 label: None,
301 snippet: r#"version 1.2
302
303workflow example {
304 String unused = "this will produce a warning"
305}
306"#,
307 },
308 revised: Some(LabeledSnippet {
309 label: Some("Consider removing the declaration entirely"),
310 snippet: r#"version 1.2
311
312workflow example {
313}
314"#,
315 }),
316 }]
317 }
318
319 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
320 Self::EXCEPTABLE_NODES
321 }
322
323 fn deny(&mut self) {
324 self.0 = Severity::Error;
325 }
326
327 fn severity(&self) -> Severity {
328 self.0
329 }
330}
331
332#[derive(Debug, Clone, Copy)]
334pub struct UnusedCallRule(Severity);
335
336impl UnusedCallRule {
337 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
339 SyntaxKind::VersionStatementNode,
340 SyntaxKind::WorkflowDefinitionNode,
341 SyntaxKind::CallStatementNode,
342 ]);
343 pub const ID: &str = "UnusedCall";
345
346 pub fn new() -> Self {
348 Self(Severity::Warning)
349 }
350}
351
352impl Default for UnusedCallRule {
353 fn default() -> Self {
354 Self::new()
355 }
356}
357
358impl Rule for UnusedCallRule {
359 fn id(&self) -> &'static str {
360 Self::ID
361 }
362
363 fn description(&self) -> &'static str {
364 "Ensures that outputs of a call statement are used in the declaring workflow."
365 }
366
367 fn explanation(&self) -> &'static str {
368 "Unused calls may cause unnecessary consumption of compute resources."
369 }
370
371 fn examples(&self) -> &'static [Example] {
372 &[Example {
373 negative: LabeledSnippet {
374 label: None,
375 snippet: r#"version 1.2
376
377workflow example {
378 # The output of `do_work` is never used
379 call do_work
380}
381
382task do_work {
383 command <<<
384 >>>
385
386 output {
387 Int x = 0
388 }
389}
390"#,
391 },
392 revised: Some(LabeledSnippet {
393 label: Some("Consider removing the call entirely"),
394 snippet: r#"version 1.2
395
396workflow example {
397}
398
399task do_work {
400 command <<<
401 >>>
402
403 output {
404 Int x = 0
405 }
406}
407"#,
408 }),
409 }]
410 }
411
412 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
413 Self::EXCEPTABLE_NODES
414 }
415
416 fn deny(&mut self) {
417 self.0 = Severity::Error;
418 }
419
420 fn severity(&self) -> Severity {
421 self.0
422 }
423}
424
425#[derive(Debug, Clone, Copy)]
427pub struct UnnecessaryFunctionCall(Severity);
428
429impl UnnecessaryFunctionCall {
430 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
432 SyntaxKind::VersionStatementNode,
433 SyntaxKind::WorkflowDefinitionNode,
434 SyntaxKind::TaskDefinitionNode,
435 SyntaxKind::BoundDeclNode,
436 ]);
437 pub const ID: &str = "UnnecessaryFunctionCall";
439
440 pub fn new() -> Self {
442 Self(Severity::Warning)
443 }
444}
445
446impl Default for UnnecessaryFunctionCall {
447 fn default() -> Self {
448 Self::new()
449 }
450}
451
452impl Rule for UnnecessaryFunctionCall {
453 fn id(&self) -> &'static str {
454 Self::ID
455 }
456
457 fn description(&self) -> &'static str {
458 "Ensures that function calls are necessary."
459 }
460
461 fn explanation(&self) -> &'static str {
462 "Unnecessary function calls may impact evaluation performance."
463 }
464
465 fn examples(&self) -> &'static [Example] {
466 &[Example {
467 negative: LabeledSnippet {
468 label: None,
469 snippet: r#"version 1.2
470
471workflow example {
472 # Calls to `defined` on values that are statically
473 # known to be non-None are unnecessary.
474 Boolean exists = defined("hello")
475}
476"#,
477 },
478 revised: None,
479 }]
480 }
481
482 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
483 Self::EXCEPTABLE_NODES
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 UsingFallbackVersion(Severity);
498
499impl UsingFallbackVersion {
500 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
502 Some(&[SyntaxKind::VersionStatementNode]);
503 pub const ID: &str = "UsingFallbackVersion";
505
506 pub fn new() -> Self {
508 Self(Severity::Warning)
509 }
510}
511
512impl Default for UsingFallbackVersion {
513 fn default() -> Self {
514 Self::new()
515 }
516}
517
518impl Rule for UsingFallbackVersion {
519 fn id(&self) -> &'static str {
520 Self::ID
521 }
522
523 fn description(&self) -> &'static str {
524 "Warns if interpretation of a document with an unsupported version falls back to a default."
525 }
526
527 fn explanation(&self) -> &'static str {
528 "A document with an unsupported version may have unpredictable behavior if interpreted as \
529 a different version."
530 }
531
532 fn examples(&self) -> &'static [Example] {
533 &[Example {
534 negative: LabeledSnippet {
535 label: None,
536 snippet: r#"# Not a valid version. If a fallback version is configured,
537# the document will be interpreted as that version.
538version development
539
540workflow example {
541}
542"#,
543 },
544 revised: None,
545 }]
546 }
547
548 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
549 Self::EXCEPTABLE_NODES
550 }
551
552 fn deny(&mut self) {
553 self.0 = Severity::Error;
554 }
555
556 fn severity(&self) -> Severity {
557 self.0
558 }
559}
560
561#[derive(Debug, Clone, Copy)]
563pub struct MeaninglessLintDirective(Severity);
564
565impl MeaninglessLintDirective {
566 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = None;
568 pub const ID: &str = "MeaninglessLintDirective";
570
571 pub fn new() -> Self {
573 Self(Severity::Note)
574 }
575}
576
577impl Default for MeaninglessLintDirective {
578 fn default() -> Self {
579 Self::new()
580 }
581}
582
583impl Rule for MeaninglessLintDirective {
584 fn id(&self) -> &'static str {
585 Self::ID
586 }
587
588 fn description(&self) -> &'static str {
589 "Warns if an `#@ except:` comment doesn't actually suppress a lint."
590 }
591
592 fn explanation(&self) -> &'static str {
593 "Unused `#@ except:` comments are likely leftovers of refactoring or debugging, and can \
594 reduce the clarity of the code. It is best to remove them."
595 }
596
597 fn examples(&self) -> &'static [Example] {
598 &[Example {
599 negative: LabeledSnippet {
600 label: None,
601 snippet: r#"version 1.3
602
603task do_work {
604 command <<<
605 echo "Lots of hard work!"
606 >>>
607
608 output {
609 String result = read_string(stdout())
610 }
611}
612
613# We except `UnusedCall` unnecessarily.
614workflow calculate {
615 #@ except: UnusedCall
616 call do_work
617
618 output {
619 # We're using the result here!
620 String result = do_work.result
621 }
622}
623"#,
624 },
625 revised: Some(LabeledSnippet {
626 label: Some("Consider removing the unused exception"),
627 snippet: r#"version 1.3
628
629task do_work {
630 command <<<
631 echo "Lots of hard work!"
632 >>>
633
634 output {
635 String result = read_string(stdout())
636 }
637}
638
639workflow calculate {
640 call do_work
641
642 output {
643 String result = do_work.result
644 }
645}
646"#,
647 }),
648 }]
649 }
650
651 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
652 Self::EXCEPTABLE_NODES
653 }
654
655 fn deny(&mut self) {
656 self.0 = Severity::Error;
657 }
658
659 fn severity(&self) -> Severity {
660 self.0
661 }
662}
663
664#[derive(Debug, Clone, Copy)]
666pub struct MisleadingDeclarationOrderRule(Severity);
667
668impl MisleadingDeclarationOrderRule {
669 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
671 Some(&[SyntaxKind::TaskDefinitionNode, SyntaxKind::BoundDeclNode]);
672 pub const ID: &str = "MisleadingDeclarationOrder";
674
675 pub fn new() -> Self {
677 Self(Severity::Warning)
678 }
679}
680
681impl Default for MisleadingDeclarationOrderRule {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687impl Rule for MisleadingDeclarationOrderRule {
688 fn id(&self) -> &'static str {
689 Self::ID
690 }
691
692 fn description(&self) -> &'static str {
693 "Warns when a variable declaration is placed after a `command` block."
694 }
695
696 fn explanation(&self) -> &'static str {
697 "WDL tasks are evaluated based on their dependency graph, not top-to-bottom. Variable \
698 declarations that appear after `command` sections are visually misleading, as they will \
699 still be evaluated _before_ the command is executed."
700 }
701
702 fn examples(&self) -> &'static [Example] {
703 &[Example {
704 negative: LabeledSnippet {
705 label: None,
706 snippet: r#"version 1.2
707
708task greet {
709 String greeting = "Hello"
710
711 command <<<
712 echo "~{greeting}, ~{name}!"
713 >>>
714
715 String name = "World"
716}
717"#,
718 },
719 revised: Some(LabeledSnippet {
720 label: None,
721 snippet: r#"version 1.2
722
723task greet {
724 String greeting = "Hello"
725 String name = "World"
726
727 command <<<
728 echo "~{greeting}, ~{name}!"
729 >>>
730}
731"#,
732 }),
733 }]
734 }
735
736 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
737 Self::EXCEPTABLE_NODES
738 }
739
740 fn deny(&mut self) {
741 self.0 = Severity::Error;
742 }
743
744 fn severity(&self) -> Severity {
745 self.0
746 }
747}
748
749#[derive(Debug, Clone, Copy)]
751pub struct KnownRulesRule(Severity);
752
753impl KnownRulesRule {
754 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
756 Some(&[SyntaxKind::VersionStatementNode]);
757 pub const ID: &str = "KnownRules";
759
760 pub fn new() -> Self {
762 Self(Severity::Note)
763 }
764}
765
766impl Default for KnownRulesRule {
767 fn default() -> Self {
768 Self::new()
769 }
770}
771
772impl Rule for KnownRulesRule {
773 fn id(&self) -> &'static str {
774 Self::ID
775 }
776
777 fn description(&self) -> &'static str {
778 "Ensures only known rules are used in `except` directives."
779 }
780
781 fn explanation(&self) -> &'static str {
782 "When writing WDL, `except` directives are used to suppress certain rules. If a rule is \
783 unknown, nothing will be suppressed. This rule flags unknown rules as they are often \
784 mistakes."
785 }
786
787 fn examples(&self) -> &'static [Example] {
788 &[Example {
789 negative: LabeledSnippet {
790 label: None,
791 snippet: r#"#@ except: LintThatDoesNotExist
792version 1.2
793
794workflow example {
795}
796"#,
797 },
798 revised: Some(LabeledSnippet {
799 label: None,
800 snippet: r#"version 1.2
801
802workflow example {
803}
804"#,
805 }),
806 }]
807 }
808
809 fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
810 Self::EXCEPTABLE_NODES
811 }
812
813 fn deny(&mut self) {
814 self.0 = Severity::Error;
815 }
816
817 fn severity(&self) -> Severity {
818 self.0
819 }
820}