wdl_lint/rules/
doc_meta_strings.rs1use wdl_analysis::Diagnostics;
4use wdl_analysis::Example;
5use wdl_analysis::LabeledSnippet;
6use wdl_analysis::VisitReason;
7use wdl_analysis::Visitor;
8use wdl_ast::AstNode;
9use wdl_ast::AstToken;
10use wdl_ast::Diagnostic;
11use wdl_ast::Span;
12use wdl_ast::SyntaxKind;
13use wdl_ast::v1::MetadataSection;
14use wdl_ast::v1::MetadataValue;
15use wdl_ast::v1::ParameterMetadataSection;
16
17use crate::Rule;
18use crate::Tag;
19use crate::TagSet;
20
21const ID: &str = "DocMetaStrings";
23
24const RESERVED_KEYS: &[&str] = &[
26 "description",
27 "help",
28 "external_help",
29 "warning",
30 "category",
31 "group",
32];
33
34fn non_string_value_diagnostic(key: &str, value_type: &str, span: Span) -> Diagnostic {
36 Diagnostic::warning(format!(
37 "metadata key `{}` should have a `String` value, found {}",
38 key, value_type
39 ))
40 .with_rule(ID)
41 .with_label(
42 format!(
43 "`{}` must be a `String` for proper documentation rendering",
44 key
45 ),
46 span,
47 )
48 .with_fix(format!("change the value of `{}` to a `String`", key))
49}
50
51fn get_value_type_name(value: &MetadataValue) -> &'static str {
53 match value {
54 MetadataValue::Null(_) => "null",
55 MetadataValue::Boolean(_) => "boolean",
56 MetadataValue::Integer(_) => "integer",
57 MetadataValue::Float(_) => "float",
58 MetadataValue::String(_) => "string",
59 MetadataValue::Array(_) => "array",
60 MetadataValue::Object(_) => "object",
61 }
62}
63
64fn is_string_value(value: &MetadataValue) -> bool {
66 matches!(value, MetadataValue::String(_))
67}
68
69fn check_object_items(
73 obj: &wdl_ast::v1::MetadataObject,
74 diagnostics: &mut Diagnostics,
75 exceptable_nodes: &Option<&'static [SyntaxKind]>,
76) {
77 for item in obj.items() {
78 let name = item.name();
79 let key = name.text();
80 let value = item.value();
81
82 if RESERVED_KEYS.contains(&key) && !is_string_value(&value) {
84 let value_type = get_value_type_name(&value);
85 diagnostics.exceptable_add(
86 non_string_value_diagnostic(key, value_type, item.span()),
87 item.inner(),
88 exceptable_nodes,
89 );
90 }
91
92 if let MetadataValue::Object(ref nested_obj) = value {
94 check_object_items(nested_obj, diagnostics, exceptable_nodes);
95 }
96 }
97}
98
99#[derive(Default, Debug, Clone, Copy)]
101pub struct DocMetaStringsRule;
102
103impl Rule for DocMetaStringsRule {
104 fn id(&self) -> &'static str {
105 ID
106 }
107
108 fn description(&self) -> &'static str {
109 "Ensures that reserved meta keys have string values."
110 }
111
112 fn explanation(&self) -> &'static str {
113 "Sprocket's documentation command reserves certain keys in `meta` and `parameter_meta` \
114 sections for documentation generation. These keys (`description`, `help`, \
115 `external_help`, `warning`, `category`, and `group`) must have `String` values. Using \
116 non-`String` values will cause the documentation to be rendered incorrectly or not at \
117 all. This rule ensures all reserved keys have `String` values for proper documentation \
118 generation."
119 }
120
121 fn examples(&self) -> &'static [Example] {
122 &[Example {
123 negative: LabeledSnippet {
124 label: None,
125 snippet: r#"version 1.2
126
127workflow example {
128 meta {
129 description: 123
130 }
131}
132"#,
133 },
134 revised: Some(LabeledSnippet {
135 label: None,
136 snippet: r#"version 1.2
137
138workflow example {
139 meta {
140 description: "123"
141 }
142}
143"#,
144 }),
145 }]
146 }
147
148 fn tags(&self) -> TagSet {
149 TagSet::new(&[Tag::SprocketCompatibility])
150 }
151
152 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
153 Some(&[
154 SyntaxKind::VersionStatementNode,
155 SyntaxKind::MetadataSectionNode,
156 SyntaxKind::ParameterMetadataSectionNode,
157 SyntaxKind::MetadataObjectItemNode,
158 ])
159 }
160
161 fn related_rules(&self) -> &'static [&'static str] {
162 &[
163 "MetaDescription",
164 "MetaSections",
165 "ParameterMetaMatched",
166 "ParameterDescription",
167 "DescriptionLength",
168 ]
169 }
170}
171
172impl Visitor for DocMetaStringsRule {
173 fn reset(&mut self) {
174 *self = Default::default();
175 }
176
177 fn metadata_section(
178 &mut self,
179 diagnostics: &mut Diagnostics,
180 reason: VisitReason,
181 section: &MetadataSection,
182 ) {
183 if reason == VisitReason::Exit {
184 return;
185 }
186
187 for item in section.items() {
189 let name = item.name();
190 let key = name.text();
191 let value = item.value();
192
193 if RESERVED_KEYS.contains(&key) && !is_string_value(&value) {
195 let value_type = get_value_type_name(&value);
196 diagnostics.exceptable_add(
197 non_string_value_diagnostic(key, value_type, item.span()),
198 item.inner(),
199 &self.exceptable_nodes(),
200 );
201 }
202
203 if let MetadataValue::Object(ref obj) = value {
206 check_object_items(obj, diagnostics, &self.exceptable_nodes());
207 }
208 }
209 }
210
211 fn parameter_metadata_section(
212 &mut self,
213 diagnostics: &mut Diagnostics,
214 reason: VisitReason,
215 section: &ParameterMetadataSection,
216 ) {
217 if reason == VisitReason::Exit {
218 return;
219 }
220
221 for item in section.items() {
223 let value = item.value();
224
225 match value {
226 MetadataValue::String(_) => {}
228
229 MetadataValue::Object(obj) => {
231 check_object_items(&obj, diagnostics, &self.exceptable_nodes());
232 }
233
234 _ => {
236 let value_type = get_value_type_name(&value);
237 diagnostics.exceptable_add(
238 non_string_value_diagnostic("description", value_type, item.span()),
239 item.inner(),
240 &self.exceptable_nodes(),
241 );
242 }
243 }
244 }
245 }
246}