Skip to main content

wdl_analysis/
rules.rs

1//! Implementation of analysis rules.
2
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6use wdl_ast::Severity;
7use wdl_grammar::SyntaxKind;
8
9use crate::RuleMap;
10
11/// All rule IDs sorted alphabetically.
12pub 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
18/// All rules and their exceptable nodes.
19pub(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/// A labeled WDL code snippet.
29#[derive(Copy, Clone, Debug)]
30pub struct LabeledSnippet {
31    /// A label for the snippet.
32    pub label: Option<&'static str>,
33    /// A WDL code snippet.
34    pub snippet: &'static str,
35}
36
37/// A lint rule example.
38#[derive(Copy, Clone, Debug)]
39pub struct Example {
40    /// A snippet that will trigger the target lint rule.
41    pub negative: LabeledSnippet,
42    /// A revision of the negative snippet that will no longer trigger the rule.
43    pub revised: Option<LabeledSnippet>,
44}
45
46/// A trait implemented by analysis rules.
47pub trait Rule: Send + Sync {
48    /// The unique identifier for the rule.
49    ///
50    /// The identifier is required to be pascal case and it is the identifier by
51    /// which a rule is excepted or denied.
52    fn id(&self) -> &'static str;
53
54    /// A short, single sentence description of the rule.
55    fn description(&self) -> &'static str;
56
57    /// Get the long-form explanation of the rule.
58    fn explanation(&self) -> &'static str;
59
60    /// Get a list of examples that would trigger this rule.
61    fn examples(&self) -> &'static [Example];
62
63    /// Gets the nodes that are exceptable for this rule.
64    ///
65    /// If `None` is returned, all nodes are exceptable.
66    fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]>;
67
68    /// Denies the rule.
69    ///
70    /// Denying the rule treats any diagnostics it emits as an error.
71    fn deny(&mut self);
72
73    /// Gets the severity of the rule.
74    fn severity(&self) -> Severity;
75}
76
77/// Gets the list of all analysis rules.
78pub 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    // Ensure all the rule ids are unique and pascal case
93    #[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/// Represents the unused import rule.
113#[derive(Debug, Clone, Copy)]
114pub struct UnusedImportRule(Severity);
115
116impl UnusedImportRule {
117    /// See [`Self::exceptable_nodes()`].
118    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
119        SyntaxKind::VersionStatementNode,
120        SyntaxKind::ImportStatementNode,
121    ]);
122    /// The rule identifier for unused import warnings.
123    pub const ID: &'static str = "UnusedImport";
124
125    /// Creates a new unused import rule.
126    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/// Represents the unused input rule.
193#[derive(Debug, Clone, Copy)]
194pub struct UnusedInputRule(Severity);
195
196impl UnusedInputRule {
197    /// See [`Self::exceptable_nodes()`].
198    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    /// The rule identifier for unused input warnings.
206    pub const ID: &str = "UnusedInput";
207
208    /// Creates a new unused input rule.
209    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/// Represents the unused declaration rule.
274#[derive(Debug, Clone, Copy)]
275pub struct UnusedDeclarationRule(Severity);
276
277impl UnusedDeclarationRule {
278    /// See [`Self::exceptable_nodes()`].
279    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    /// The rule identifier for unused declaration warnings.
287    pub const ID: &str = "UnusedDeclaration";
288
289    /// Creates a new unused declaration rule.
290    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/// Represents the unused call rule.
352#[derive(Debug, Clone, Copy)]
353pub struct UnusedCallRule(Severity);
354
355impl UnusedCallRule {
356    /// See [`Self::exceptable_nodes()`].
357    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
358        SyntaxKind::VersionStatementNode,
359        SyntaxKind::WorkflowDefinitionNode,
360        SyntaxKind::CallStatementNode,
361    ]);
362    /// The rule identifier for unused call warnings.
363    pub const ID: &str = "UnusedCall";
364
365    /// Creates a new unused call rule.
366    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/// Represents the unnecessary call rule.
445#[derive(Debug, Clone, Copy)]
446pub struct UnnecessaryFunctionCall(Severity);
447
448impl UnnecessaryFunctionCall {
449    /// See [`Self::exceptable_nodes()`].
450    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = Some(&[
451        SyntaxKind::VersionStatementNode,
452        SyntaxKind::WorkflowDefinitionNode,
453        SyntaxKind::TaskDefinitionNode,
454        SyntaxKind::BoundDeclNode,
455    ]);
456    /// The rule identifier for unnecessary function call warnings.
457    pub const ID: &str = "UnnecessaryFunctionCall";
458
459    /// Creates a new unnecessary function call rule.
460    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/// Represents the using fallback version rule.
515#[derive(Debug, Clone, Copy)]
516pub struct UsingFallbackVersion(Severity);
517
518impl UsingFallbackVersion {
519    /// See [`Self::exceptable_nodes()`].
520    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
521        Some(&[SyntaxKind::VersionStatementNode]);
522    /// The rule identifier for unsupported version fallback warnings.
523    pub const ID: &str = "UsingFallbackVersion";
524
525    /// Creates a new using fallback version rule.
526    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/// Represents the meaningless lint directive rule.
581#[derive(Debug, Clone, Copy)]
582pub struct MeaninglessLintDirective(Severity);
583
584impl MeaninglessLintDirective {
585    /// See [`Self::exceptable_nodes()`].
586    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> = None;
587    /// The rule identifier for meaningless lint directive warnings.
588    pub const ID: &str = "MeaninglessLintDirective";
589
590    /// Creates a new meaningless lint directive rule.
591    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/// Represents the using misleading declaration order rule.
684#[derive(Debug, Clone, Copy)]
685pub struct MisleadingDeclarationOrderRule(Severity);
686
687impl MisleadingDeclarationOrderRule {
688    /// See [`Self::exceptable_nodes()`].
689    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
690        Some(&[SyntaxKind::TaskDefinitionNode, SyntaxKind::BoundDeclNode]);
691    /// The rule identifier for misleading declaration order warnings.
692    pub const ID: &str = "MisleadingDeclarationOrder";
693
694    /// Creates a new misleading declaration order rule.
695    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/// Detects unknown rules within lint directives.
769#[derive(Debug, Clone, Copy)]
770pub struct KnownRulesRule(Severity);
771
772impl KnownRulesRule {
773    /// See [`Self::exceptable_nodes()`].
774    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
775        Some(&[SyntaxKind::VersionStatementNode]);
776    /// The rule identifier for known rules warnings.
777    pub const ID: &str = "KnownRules";
778
779    /// Creates a new "known rules" rule.
780    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/// Detects improperly placed `except` directives.
842#[derive(Debug, Clone, Copy)]
843pub struct ExceptDirectiveValidRule(Severity);
844
845impl ExceptDirectiveValidRule {
846    /// See [`Self::exceptable_nodes()`].
847    pub const EXCEPTABLE_NODES: Option<&'static [SyntaxKind]> =
848        Some(&[SyntaxKind::VersionStatementNode]);
849    /// The rule identifier for except directive warnings.
850    pub const ID: &str = "ExceptDirectiveValid";
851
852    /// Creates a new "except directive valid" rule.
853    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}