1use std::collections::HashMap;
4use std::sync::LazyLock;
5
6use wdl_ast::Severity;
7use wdl_grammar::SyntaxKind;
8
9use crate::RuleMap;
10
11pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
13 let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
14 ids.sort();
15 ids
16});
17
18pub(crate) static RULE_MAP: LazyLock<RuleMap> = LazyLock::new(|| {
20 let rules = rules();
21 let mut map = HashMap::with_capacity(rules.len());
22 for rule in rules {
23 map.insert(String::from(rule.id()), rule.exceptable_nodes());
24 }
25 map
26});
27
28#[derive(Copy, Clone, Debug)]
30pub struct LabeledSnippet {
31 pub label: Option<&'static str>,
33 pub snippet: &'static str,
35}
36
37#[derive(Copy, Clone, Debug)]
39pub struct Example {
40 pub negative: LabeledSnippet,
42 pub revised: Option<LabeledSnippet>,
44}
45
46pub trait Rule: Send + Sync {
48 fn id(&self) -> &'static str;
53
54 fn description(&self) -> &'static str;
56
57 fn explanation(&self) -> &'static str;
59
60 fn examples(&self) -> &'static [Example];
62
63 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
67
68 fn deny(&mut self);
72
73 fn severity(&self) -> Severity;
75}
76
77pub fn rules() -> Vec<Box<dyn Rule>> {
79 let rules: Vec<Box<dyn Rule>> = vec![
80 Box::<UnusedImportRule>::default(),
81 Box::<UnusedInputRule>::default(),
82 Box::<UnusedDeclarationRule>::default(),
83 Box::<UnusedCallRule>::default(),
84 Box::<UnnecessaryFunctionCall>::default(),
85 Box::<UsingFallbackVersion>::default(),
86 Box::<MisleadingDeclarationOrderRule>::default(),
87 Box::<MeaninglessLintDirective>::default(),
88 Box::<KnownRulesRule>::default(),
89 Box::<ExceptDirectiveValidRule>::default(),
90 ];
91
92 #[cfg(debug_assertions)]
94 {
95 use convert_case::Case;
96 use convert_case::Casing;
97 let mut set = std::collections::HashSet::new();
98 for r in rules.iter() {
99 if r.id().to_case(Case::Pascal) != r.id() {
100 panic!("analysis rule id `{id}` is not pascal case", id = r.id());
101 }
102
103 if !set.insert(r.id()) {
104 panic!("duplicate rule id `{id}`", id = r.id());
105 }
106 }
107 }
108
109 rules
110}
111
112#[derive(Debug, Clone, Copy)]
114pub struct UnusedImportRule(Severity);
115
116impl UnusedImportRule {
117 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
119 SyntaxKind::VersionStatementNode,
120 SyntaxKind::ImportStatementNode,
121 ]);
122 pub const ID: &'static str = "UnusedImport";
124
125 pub fn new() -> Self {
127 Self(Severity::Warning)
128 }
129}
130
131impl Default for UnusedImportRule {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137impl Rule for UnusedImportRule {
138 fn id(&self) -> &'static str {
139 Self::ID
140 }
141
142 fn description(&self) -> &'static str {
143 "Ensures that import namespaces are used in the importing document."
144 }
145
146 fn explanation(&self) -> &'static str {
147 "Imported WDL documents should be used in the document that imports them. Unused imports \
148 impact parsing and evaluation performance."
149 }
150
151 fn examples(&self) -> &'static [Example] {
152 &[Example {
153 negative: LabeledSnippet {
154 label: None,
155 snippet: r#"version 1.3
156
157import "bar.wdl"
158import "foo.wdl" as used
159
160workflow example {
161 call used.test
162}
163"#,
164 },
165 revised: Some(LabeledSnippet {
166 label: Some("Consider removing the import entirely"),
167 snippet: r#"version 1.3
168
169import "foo.wdl" as used
170
171workflow example {
172 call used.test
173}
174"#,
175 }),
176 }]
177 }
178
179 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
180 Self::EXCEPTABLE_NODES
181 }
182
183 fn deny(&mut self) {
184 self.0 = Severity::Error;
185 }
186
187 fn severity(&self) -> Severity {
188 self.0
189 }
190}
191
192#[derive(Debug, Clone, Copy)]
194pub struct UnusedInputRule(Severity);
195
196impl UnusedInputRule {
197 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
199 SyntaxKind::VersionStatementNode,
200 SyntaxKind::WorkflowDefinitionNode,
201 SyntaxKind::TaskDefinitionNode,
202 SyntaxKind::BoundDeclNode,
203 SyntaxKind::UnboundDeclNode,
204 ]);
205 pub const ID: &str = "UnusedInput";
207
208 pub fn new() -> Self {
210 Self(Severity::Warning)
211 }
212}
213
214impl Default for UnusedInputRule {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl Rule for UnusedInputRule {
221 fn id(&self) -> &'static str {
222 Self::ID
223 }
224
225 fn description(&self) -> &'static str {
226 "Ensures that task or workspace inputs are used within the declaring task or workspace."
227 }
228
229 fn explanation(&self) -> &'static str {
230 "Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
231 file inputs in tasks can also cause unnecessary file localizations."
232 }
233
234 fn examples(&self) -> &'static [Example] {
235 &[Example {
236 negative: LabeledSnippet {
237 label: None,
238 snippet: r#"version 1.2
239
240workflow example {
241 input {
242 String unused
243 }
244}
245"#,
246 },
247 revised: Some(LabeledSnippet {
248 label: Some("Consider removing the input entirely"),
249 snippet: r#"version 1.2
250
251workflow example {
252 input {
253 }
254}
255"#,
256 }),
257 }]
258 }
259
260 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
261 Self::EXCEPTABLE_NODES
262 }
263
264 fn deny(&mut self) {
265 self.0 = Severity::Error;
266 }
267
268 fn severity(&self) -> Severity {
269 self.0
270 }
271}
272
273#[derive(Debug, Clone, Copy)]
275pub struct UnusedDeclarationRule(Severity);
276
277impl UnusedDeclarationRule {
278 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
280 SyntaxKind::VersionStatementNode,
281 SyntaxKind::WorkflowDefinitionNode,
282 SyntaxKind::TaskDefinitionNode,
283 SyntaxKind::BoundDeclNode,
284 SyntaxKind::UnboundDeclNode,
285 ]);
286 pub const ID: &str = "UnusedDeclaration";
288
289 pub fn new() -> Self {
291 Self(Severity::Warning)
292 }
293}
294
295impl Default for UnusedDeclarationRule {
296 fn default() -> Self {
297 Self::new()
298 }
299}
300
301impl Rule for UnusedDeclarationRule {
302 fn id(&self) -> &'static str {
303 Self::ID
304 }
305
306 fn description(&self) -> &'static str {
307 "Ensures that private declarations in tasks or workspaces are used within the declaring \
308 task or workspace."
309 }
310
311 fn explanation(&self) -> &'static str {
312 "Unused private declarations degrade evaluation performance and reduce the clarity of the \
313 code."
314 }
315
316 fn examples(&self) -> &'static [Example] {
317 &[Example {
318 negative: LabeledSnippet {
319 label: None,
320 snippet: r#"version 1.2
321
322workflow example {
323 String unused = "this will produce a warning"
324}
325"#,
326 },
327 revised: Some(LabeledSnippet {
328 label: Some("Consider removing the declaration entirely"),
329 snippet: r#"version 1.2
330
331workflow example {
332}
333"#,
334 }),
335 }]
336 }
337
338 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
339 Self::EXCEPTABLE_NODES
340 }
341
342 fn deny(&mut self) {
343 self.0 = Severity::Error;
344 }
345
346 fn severity(&self) -> Severity {
347 self.0
348 }
349}
350
351#[derive(Debug, Clone, Copy)]
353pub struct UnusedCallRule(Severity);
354
355impl UnusedCallRule {
356 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
358 SyntaxKind::VersionStatementNode,
359 SyntaxKind::WorkflowDefinitionNode,
360 SyntaxKind::CallStatementNode,
361 ]);
362 pub const ID: &str = "UnusedCall";
364
365 pub fn new() -> Self {
367 Self(Severity::Warning)
368 }
369}
370
371impl Default for UnusedCallRule {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377impl Rule for UnusedCallRule {
378 fn id(&self) -> &'static str {
379 Self::ID
380 }
381
382 fn description(&self) -> &'static str {
383 "Ensures that outputs of a call statement are used in the declaring workflow."
384 }
385
386 fn explanation(&self) -> &'static str {
387 "Unused calls may cause unnecessary consumption of compute resources."
388 }
389
390 fn examples(&self) -> &'static [Example] {
391 &[Example {
392 negative: LabeledSnippet {
393 label: None,
394 snippet: r#"version 1.2
395
396workflow example {
397 # The output of `do_work` is never used
398 call do_work
399}
400
401task do_work {
402 command <<<
403 >>>
404
405 output {
406 Int x = 0
407 }
408}
409"#,
410 },
411 revised: Some(LabeledSnippet {
412 label: Some("Consider removing the call entirely"),
413 snippet: r#"version 1.2
414
415workflow example {
416}
417
418task do_work {
419 command <<<
420 >>>
421
422 output {
423 Int x = 0
424 }
425}
426"#,
427 }),
428 }]
429 }
430
431 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
432 Self::EXCEPTABLE_NODES
433 }
434
435 fn deny(&mut self) {
436 self.0 = Severity::Error;
437 }
438
439 fn severity(&self) -> Severity {
440 self.0
441 }
442}
443
444#[derive(Debug, Clone, Copy)]
446pub struct UnnecessaryFunctionCall(Severity);
447
448impl UnnecessaryFunctionCall {
449 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
451 SyntaxKind::VersionStatementNode,
452 SyntaxKind::WorkflowDefinitionNode,
453 SyntaxKind::TaskDefinitionNode,
454 SyntaxKind::BoundDeclNode,
455 ]);
456 pub const ID: &str = "UnnecessaryFunctionCall";
458
459 pub fn new() -> Self {
461 Self(Severity::Warning)
462 }
463}
464
465impl Default for UnnecessaryFunctionCall {
466 fn default() -> Self {
467 Self::new()
468 }
469}
470
471impl Rule for UnnecessaryFunctionCall {
472 fn id(&self) -> &'static str {
473 Self::ID
474 }
475
476 fn description(&self) -> &'static str {
477 "Ensures that function calls are necessary."
478 }
479
480 fn explanation(&self) -> &'static str {
481 "Unnecessary function calls may impact evaluation performance."
482 }
483
484 fn examples(&self) -> &'static [Example] {
485 &[Example {
486 negative: LabeledSnippet {
487 label: None,
488 snippet: r#"version 1.2
489
490workflow example {
491 # Calls to `defined` on values that are statically
492 # known to be non-None are unnecessary.
493 Boolean exists = defined("hello")
494}
495"#,
496 },
497 revised: None,
498 }]
499 }
500
501 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
502 Self::EXCEPTABLE_NODES
503 }
504
505 fn deny(&mut self) {
506 self.0 = Severity::Error;
507 }
508
509 fn severity(&self) -> Severity {
510 self.0
511 }
512}
513
514#[derive(Debug, Clone, Copy)]
516pub struct UsingFallbackVersion(Severity);
517
518impl UsingFallbackVersion {
519 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
521 Some(&[SyntaxKind::VersionStatementNode]);
522 pub const ID: &str = "UsingFallbackVersion";
524
525 pub fn new() -> Self {
527 Self(Severity::Warning)
528 }
529}
530
531impl Default for UsingFallbackVersion {
532 fn default() -> Self {
533 Self::new()
534 }
535}
536
537impl Rule for UsingFallbackVersion {
538 fn id(&self) -> &'static str {
539 Self::ID
540 }
541
542 fn description(&self) -> &'static str {
543 "Warns if interpretation of a document with an unsupported version falls back to a default."
544 }
545
546 fn explanation(&self) -> &'static str {
547 "A document with an unsupported version may have unpredictable behavior if interpreted as \
548 a different version."
549 }
550
551 fn examples(&self) -> &'static [Example] {
552 &[Example {
553 negative: LabeledSnippet {
554 label: None,
555 snippet: r#"# Not a valid version. If a fallback version is configured,
556# the document will be interpreted as that version.
557version development
558
559workflow example {
560}
561"#,
562 },
563 revised: None,
564 }]
565 }
566
567 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
568 Self::EXCEPTABLE_NODES
569 }
570
571 fn deny(&mut self) {
572 self.0 = Severity::Error;
573 }
574
575 fn severity(&self) -> Severity {
576 self.0
577 }
578}
579
580#[derive(Debug, Clone, Copy)]
582pub struct MeaninglessLintDirective(Severity);
583
584impl MeaninglessLintDirective {
585 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = None;
587 pub const ID: &str = "MeaninglessLintDirective";
589
590 pub fn new() -> Self {
592 Self(Severity::Note)
593 }
594}
595
596impl Default for MeaninglessLintDirective {
597 fn default() -> Self {
598 Self::new()
599 }
600}
601
602impl Rule for MeaninglessLintDirective {
603 fn id(&self) -> &'static str {
604 Self::ID
605 }
606
607 fn description(&self) -> &'static str {
608 "Warns if an `#@ except:` comment doesn't actually suppress a lint."
609 }
610
611 fn explanation(&self) -> &'static str {
612 "Unused `#@ except:` comments are likely leftovers of refactoring or debugging, and can \
613 reduce the clarity of the code. It is best to remove them."
614 }
615
616 fn examples(&self) -> &'static [Example] {
617 &[Example {
618 negative: LabeledSnippet {
619 label: None,
620 snippet: r#"version 1.3
621
622task do_work {
623 command <<<
624 echo "Lots of hard work!"
625 >>>
626
627 output {
628 String result = read_string(stdout())
629 }
630}
631
632# We except `UnusedCall` unnecessarily.
633workflow calculate {
634 #@ except: UnusedCall
635 call do_work
636
637 output {
638 # We're using the result here!
639 String result = do_work.result
640 }
641}
642"#,
643 },
644 revised: Some(LabeledSnippet {
645 label: Some("Consider removing the unused exception"),
646 snippet: r#"version 1.3
647
648task do_work {
649 command <<<
650 echo "Lots of hard work!"
651 >>>
652
653 output {
654 String result = read_string(stdout())
655 }
656}
657
658workflow calculate {
659 call do_work
660
661 output {
662 String result = do_work.result
663 }
664}
665"#,
666 }),
667 }]
668 }
669
670 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
671 Self::EXCEPTABLE_NODES
672 }
673
674 fn deny(&mut self) {
675 self.0 = Severity::Error;
676 }
677
678 fn severity(&self) -> Severity {
679 self.0
680 }
681}
682
683#[derive(Debug, Clone, Copy)]
685pub struct MisleadingDeclarationOrderRule(Severity);
686
687impl MisleadingDeclarationOrderRule {
688 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
690 Some(&[SyntaxKind::TaskDefinitionNode, SyntaxKind::BoundDeclNode]);
691 pub const ID: &str = "MisleadingDeclarationOrder";
693
694 pub fn new() -> Self {
696 Self(Severity::Warning)
697 }
698}
699
700impl Default for MisleadingDeclarationOrderRule {
701 fn default() -> Self {
702 Self::new()
703 }
704}
705
706impl Rule for MisleadingDeclarationOrderRule {
707 fn id(&self) -> &'static str {
708 Self::ID
709 }
710
711 fn description(&self) -> &'static str {
712 "Warns when a variable declaration is placed after a `command` block."
713 }
714
715 fn explanation(&self) -> &'static str {
716 "WDL tasks are evaluated based on their dependency graph, not top-to-bottom. Variable \
717 declarations that appear after `command` sections are visually misleading, as they will \
718 still be evaluated _before_ the command is executed."
719 }
720
721 fn examples(&self) -> &'static [Example] {
722 &[Example {
723 negative: LabeledSnippet {
724 label: None,
725 snippet: r#"version 1.2
726
727task greet {
728 String greeting = "Hello"
729
730 command <<<
731 echo "~{greeting}, ~{name}!"
732 >>>
733
734 String name = "World"
735}
736"#,
737 },
738 revised: Some(LabeledSnippet {
739 label: None,
740 snippet: r#"version 1.2
741
742task greet {
743 String greeting = "Hello"
744 String name = "World"
745
746 command <<<
747 echo "~{greeting}, ~{name}!"
748 >>>
749}
750"#,
751 }),
752 }]
753 }
754
755 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
756 Self::EXCEPTABLE_NODES
757 }
758
759 fn deny(&mut self) {
760 self.0 = Severity::Error;
761 }
762
763 fn severity(&self) -> Severity {
764 self.0
765 }
766}
767
768#[derive(Debug, Clone, Copy)]
770pub struct KnownRulesRule(Severity);
771
772impl KnownRulesRule {
773 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
775 Some(&[SyntaxKind::VersionStatementNode]);
776 pub const ID: &str = "KnownRules";
778
779 pub fn new() -> Self {
781 Self(Severity::Note)
782 }
783}
784
785impl Default for KnownRulesRule {
786 fn default() -> Self {
787 Self::new()
788 }
789}
790
791impl Rule for KnownRulesRule {
792 fn id(&self) -> &'static str {
793 Self::ID
794 }
795
796 fn description(&self) -> &'static str {
797 "Ensures only known rules are used in `except` directives."
798 }
799
800 fn explanation(&self) -> &'static str {
801 "When writing WDL, `except` directives are used to suppress certain rules. If a rule is \
802 unknown, nothing will be suppressed. This rule flags unknown rules as they are often \
803 mistakes."
804 }
805
806 fn examples(&self) -> &'static [Example] {
807 &[Example {
808 negative: LabeledSnippet {
809 label: None,
810 snippet: r#"#@ except: LintThatDoesNotExist
811version 1.2
812
813workflow example {
814}
815"#,
816 },
817 revised: Some(LabeledSnippet {
818 label: None,
819 snippet: r#"version 1.2
820
821workflow example {
822}
823"#,
824 }),
825 }]
826 }
827
828 fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
829 Self::EXCEPTABLE_NODES
830 }
831
832 fn deny(&mut self) {
833 self.0 = Severity::Error;
834 }
835
836 fn severity(&self) -> Severity {
837 self.0
838 }
839}
840
841#[derive(Debug, Clone, Copy)]
843pub struct ExceptDirectiveValidRule(Severity);
844
845impl ExceptDirectiveValidRule {
846 pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
848 Some(&[SyntaxKind::VersionStatementNode]);
849 pub const ID: &str = "ExceptDirectiveValid";
851
852 pub fn new() -> Self {
854 Self(Severity::Note)
855 }
856}
857
858impl Default for ExceptDirectiveValidRule {
859 fn default() -> Self {
860 Self::new()
861 }
862}
863
864impl Rule for ExceptDirectiveValidRule {
865 fn id(&self) -> &'static str {
866 Self::ID
867 }
868
869 fn description(&self) -> &'static str {
870 "Ensures `except` directives are placed correctly to have the intended effect."
871 }
872
873 fn explanation(&self) -> &'static str {
874 "When writing WDL, `except` directives are used to suppress certain rules. If an `except` \
875 directive is misplaced, it will have no effect. This rule flags misplaced `except` \
876 directives to ensure they are in the correct location."
877 }
878
879 fn examples(&self) -> &'static [Example] {
880 &[Example {
881 negative: LabeledSnippet {
882 label: None,
883 snippet: r#"version 1.3
884
885# UsingFallbackVersion exceptions aren't valid
886# in this context
887#@ except: UsingFallbackVersion
888workflow example {
889}
890"#,
891 },
892 revised: Some(LabeledSnippet {
893 label: None,
894 snippet: r#"#@ except: UsingFallbackVersion
895version 1.3
896
897workflow example {
898}
899"#,
900 }),
901 }]
902 }
903
904 fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
905 Self::EXCEPTABLE_NODES
906 }
907
908 fn deny(&mut self) {
909 self.0 = Severity::Error;
910 }
911
912 fn severity(&self) -> Severity {
913 self.0
914 }
915}