use wdl_analysis::Diagnostics;
use wdl_analysis::Example;
use wdl_analysis::LabeledSnippet;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
use wdl_ast::AstNode;
use wdl_ast::AstToken;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SyntaxKind;
use wdl_ast::v1::MetadataSection;
use wdl_ast::v1::MetadataValue;
use wdl_ast::v1::ParameterMetadataSection;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
const ID: &str = "DocMetaStrings";
const RESERVED_KEYS: &[&str] = &[
"description",
"help",
"external_help",
"warning",
"category",
"group",
];
fn non_string_value_diagnostic(key: &str, value_type: &str, span: Span) -> Diagnostic {
Diagnostic::warning(format!(
"metadata key `{}` should have a `String` value, found {}",
key, value_type
))
.with_rule(ID)
.with_label(
format!(
"`{}` must be a `String` for proper documentation rendering",
key
),
span,
)
.with_fix(format!("change the value of `{}` to a `String`", key))
}
fn get_value_type_name(value: &MetadataValue) -> &'static str {
match value {
MetadataValue::Null(_) => "null",
MetadataValue::Boolean(_) => "boolean",
MetadataValue::Integer(_) => "integer",
MetadataValue::Float(_) => "float",
MetadataValue::String(_) => "string",
MetadataValue::Array(_) => "array",
MetadataValue::Object(_) => "object",
}
}
fn is_string_value(value: &MetadataValue) -> bool {
matches!(value, MetadataValue::String(_))
}
fn check_object_items(
obj: &wdl_ast::v1::MetadataObject,
diagnostics: &mut Diagnostics,
exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
for item in obj.items() {
let name = item.name();
let key = name.text();
let value = item.value();
if RESERVED_KEYS.contains(&key) && !is_string_value(&value) {
let value_type = get_value_type_name(&value);
diagnostics.exceptable_add(
non_string_value_diagnostic(key, value_type, item.span()),
item.inner(),
exceptable_nodes,
);
}
if let MetadataValue::Object(ref nested_obj) = value {
check_object_items(nested_obj, diagnostics, exceptable_nodes);
}
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct DocMetaStringsRule;
impl Rule for DocMetaStringsRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that reserved meta keys have string values."
}
fn explanation(&self) -> &'static str {
"Sprocket's documentation command reserves certain keys in `meta` and `parameter_meta` \
sections for documentation generation. These keys (`description`, `help`, \
`external_help`, `warning`, `category`, and `group`) must have `String` values. Using \
non-`String` values will cause the documentation to be rendered incorrectly or not at \
all. This rule ensures all reserved keys have `String` values for proper documentation \
generation."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
workflow example {
meta {
description: 123
}
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
workflow example {
meta {
description: "123"
}
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::SprocketCompatibility])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::MetadataSectionNode,
SyntaxKind::ParameterMetadataSectionNode,
SyntaxKind::MetadataObjectItemNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&[
"MetaDescription",
"MetaSections",
"ParameterMetaMatched",
"ParameterDescription",
"DescriptionLength",
]
}
}
impl Visitor for DocMetaStringsRule {
fn reset(&mut self) {
*self = Default::default();
}
fn metadata_section(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
section: &MetadataSection,
) {
if reason == VisitReason::Exit {
return;
}
for item in section.items() {
let name = item.name();
let key = name.text();
let value = item.value();
if RESERVED_KEYS.contains(&key) && !is_string_value(&value) {
let value_type = get_value_type_name(&value);
diagnostics.exceptable_add(
non_string_value_diagnostic(key, value_type, item.span()),
item.inner(),
&self.exceptable_nodes(),
);
}
if let MetadataValue::Object(ref obj) = value {
check_object_items(obj, diagnostics, &self.exceptable_nodes());
}
}
}
fn parameter_metadata_section(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
section: &ParameterMetadataSection,
) {
if reason == VisitReason::Exit {
return;
}
for item in section.items() {
let value = item.value();
match value {
MetadataValue::String(_) => {}
MetadataValue::Object(obj) => {
check_object_items(&obj, diagnostics, &self.exceptable_nodes());
}
_ => {
let value_type = get_value_type_name(&value);
diagnostics.exceptable_add(
non_string_value_diagnostic("description", value_type, item.span()),
item.inner(),
&self.exceptable_nodes(),
);
}
}
}
}
}