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 = "ParameterDescription";
const DESCRIPTION_KEY: &str = "description";
const OUTPUTS_KEY: &str = "outputs";
fn missing_description_diagnostic(name: &str, is_output: bool, span: Span) -> Diagnostic {
let item_type = if is_output { "output" } else { "parameter" };
let location = if is_output { " in `meta.outputs`" } else { "" };
Diagnostic::note(format!(
"{} `{}` is missing a description{}",
item_type, name, location
))
.with_rule(ID)
.with_highlight(span)
.with_fix(format!("add a description for `{}`", name))
}
fn has_valid_description(value: &MetadataValue) -> bool {
match value {
MetadataValue::String(_) => true,
MetadataValue::Object(obj) => obj
.items()
.any(|item| item.name().text() == DESCRIPTION_KEY),
_ => true,
}
}
#[derive(Default, Debug, Clone, Copy)]
pub struct ParameterDescriptionRule;
impl Rule for ParameterDescriptionRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that parameters and outputs have proper descriptions."
}
fn explanation(&self) -> &'static str {
"Documentation is expected for each parameter (in `parameter_meta`) and each output (in \
`meta.outputs`). A valid description is either a simple `String` value or an object \
containing a `description` key with a `String` value."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
task greet {
meta {
outputs: {
greeting: {},
}
}
parameter_meta {
name: {}
}
input {
String name
}
command <<<
echo "Hello, ~{name}"
>>>
output {
String greeting = stdout()
}
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task greet {
meta {
outputs: {
greeting: "The generated greeting message.",
}
}
parameter_meta {
name: "The name of the person to greet."
}
input {
String name
}
command <<<
echo "Hello, ~{name}"
>>>
output {
String greeting = stdout()
}
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Documentation, Tag::Completeness])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::MetadataSectionNode,
SyntaxKind::ParameterMetadataSectionNode,
SyntaxKind::MetadataObjectItemNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&["DescriptionLength", "MatchingOutputMeta", "MetaDescription"]
}
}
impl Visitor for ParameterDescriptionRule {
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() {
if item.name().text() != OUTPUTS_KEY {
continue;
}
if let MetadataValue::Object(outputs_obj) = item.value() {
for output_item in outputs_obj.items() {
if !has_valid_description(&output_item.value()) {
diagnostics.exceptable_add(
missing_description_diagnostic(
output_item.name().text(),
true,
output_item.name().span(),
),
output_item.inner(),
&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() {
if !has_valid_description(&item.value()) {
diagnostics.exceptable_add(
missing_description_diagnostic(item.name().text(), false, item.name().span()),
item.inner(),
&self.exceptable_nodes(),
);
}
}
}
}