Skip to main content

wdl_lint/rules/
except_directive_valid.rs

1//! A lint rule for flagging misplaced except directives.
2
3use std::collections::HashMap;
4use std::sync::LazyLock;
5
6use wdl_analysis::Diagnostics;
7use wdl_analysis::Example;
8use wdl_analysis::LabeledSnippet;
9use wdl_analysis::Visitor;
10use wdl_ast::AstToken;
11use wdl_ast::Comment;
12use wdl_ast::Diagnostic;
13use wdl_ast::Span;
14use wdl_ast::SyntaxElement;
15use wdl_ast::SyntaxKind;
16
17use crate::Config;
18use crate::Rule;
19use crate::Tag;
20use crate::TagSet;
21use crate::rules;
22
23/// The identifier for the except directive valid rule.
24const ID: &str = "ExceptDirectiveValid";
25
26/// Creates a "misplaced directive" diagnostic.
27fn misplaced_except_directive(
28    id: &str,
29    span: Span,
30    wrong_element: &SyntaxElement,
31    exceptable_nodes: &[SyntaxKind],
32) -> Diagnostic {
33    let locations = exceptable_nodes
34        .iter()
35        .map(|node| node.describe())
36        .collect::<Vec<_>>()
37        .join(", ");
38
39    Diagnostic::note(format!(
40        "`except` directive `{id}` has no effect above {elem}",
41        elem = wrong_element.kind().describe()
42    ))
43    .with_rule(ID)
44    .with_label("cannot make an exception for this rule", span)
45    .with_label(
46        "invalid element for this `except` directive",
47        wrong_element.text_range(),
48    )
49    .with_fix(format!(
50        "valid locations for this directive are above: {locations}"
51    ))
52}
53
54/// Creates a static LazyLock of the rules' excepatable nodes.
55pub static RULE_MAP: LazyLock<HashMap<&'static str, Option<&'static [SyntaxKind]>>> =
56    LazyLock::new(|| {
57        let mut map = HashMap::new();
58        for rule in rules(&Config::default()) {
59            map.insert(rule.id(), rule.exceptable_nodes());
60        }
61        map
62    });
63
64/// Detects unknown rules within lint directives.
65#[derive(Default, Debug, Clone, Copy)]
66pub struct ExceptDirectiveValidRule;
67
68impl Rule for ExceptDirectiveValidRule {
69    fn id(&self) -> &'static str {
70        ID
71    }
72
73    fn description(&self) -> &'static str {
74        "Ensures `except` directives are placed correctly to have the intended effect."
75    }
76
77    fn explanation(&self) -> &'static str {
78        "When writing WDL, `except` directives are used to suppress certain rules. If an `except` \
79         directive is misplaced, it will have no effect. This rule flags misplaced `except` \
80         directives to ensure they are in the correct location."
81    }
82
83    fn examples(&self) -> &'static [Example] {
84        &[Example {
85            negative: LabeledSnippet {
86                label: None,
87                snippet: r#"version 1.2
88
89workflow example {
90    output {
91        # MatchingOutputMeta exceptions aren't valid
92        # in this context
93        #@ except: MatchingOutputMeta
94        String name = "Jimmy"
95    }
96}
97"#,
98            },
99            revised: Some(LabeledSnippet {
100                label: None,
101                snippet: r#"version 1.2
102
103#@ except: MatchingOutputMeta
104workflow example {
105    output {
106        String name = "Jimmy"
107    }
108}
109"#,
110            }),
111        }]
112    }
113
114    fn tags(&self) -> TagSet {
115        TagSet::new(&[Tag::Clarity, Tag::Correctness, Tag::SprocketCompatibility])
116    }
117
118    fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
119        Some(&[SyntaxKind::VersionStatementNode])
120    }
121
122    fn related_rules(&self) -> &'static [&'static str] {
123        &[]
124    }
125}
126
127impl Visitor for ExceptDirectiveValidRule {
128    fn reset(&mut self) {
129        *self = Self;
130    }
131
132    fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {
133        if let Some(wdl_ast::Directive::Except(rules)) = comment.directive() {
134            let start: usize = comment.span().start();
135
136            let excepted_element = comment
137                .inner()
138                .siblings_with_tokens(rowan::Direction::Next)
139                .find_map(|s| {
140                    if s.kind() == SyntaxKind::Whitespace || s.kind() == SyntaxKind::Comment {
141                        None
142                    } else {
143                        Some(s)
144                    }
145                });
146
147            for rule in rules {
148                let id = &rule.name;
149
150                if let Some(elem) = &excepted_element
151                    && let Some(Some(exceptable_nodes)) = RULE_MAP.get(id.as_str())
152                    && !exceptable_nodes.contains(&elem.kind())
153                {
154                    diagnostics.add(misplaced_except_directive(
155                        id,
156                        Span::new(start + comment.text().find(id).unwrap(), id.len()),
157                        elem,
158                        exceptable_nodes,
159                    ));
160                }
161            }
162        }
163    }
164}