wdl_lint/rules/
except_directive_valid.rs1use 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
23const ID: &str = "ExceptDirectiveValid";
25
26fn 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
54pub 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#[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}