use std::fmt;
use wdl_analysis::Diagnostics;
use wdl_analysis::Document;
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::Ident;
use wdl_ast::SupportedVersion;
use wdl_ast::SyntaxKind;
use wdl_ast::v1::TaskDefinition;
use wdl_ast::v1::WorkflowDefinition;
use wdl_ast::version::V1;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
enum Section {
Meta,
ParameterMeta,
}
impl fmt::Display for Section {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Meta => write!(f, "meta"),
Self::ParameterMeta => write!(f, "parameter_meta"),
}
}
}
enum Context {
Task,
Workflow,
Struct,
}
impl fmt::Display for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Task => write!(f, "task"),
Self::Workflow => write!(f, "workflow"),
Self::Struct => write!(f, "struct"),
}
}
}
const ID: &str = "MetaSections";
fn missing_section(name: Ident, section: Section, context: Context) -> Diagnostic {
Diagnostic::note(format!(
"{context} `{name}` is missing a `{section}` section",
name = name.text(),
))
.with_rule(ID)
.with_label(
format!("this {context} is missing a `{section}` section"),
name.span(),
)
.with_fix("add the missing section")
}
fn missing_sections(name: Ident, context: Context) -> Diagnostic {
Diagnostic::note(format!(
"{context} `{name}` is missing both `meta` and `parameter_meta` sections",
name = name.text(),
))
.with_rule(ID)
.with_label(
format!("this {context} is missing both `meta` and `parameter_meta` sections"),
name.span(),
)
.with_fix("add both the `meta` and `parameter_meta` sections")
}
#[derive(Default, Debug, Clone, Copy)]
pub struct MetaSectionsRule {
version: Option<SupportedVersion>,
}
impl Rule for MetaSectionsRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that tasks and workflows have the required `meta` and `parameter_meta` sections."
}
fn explanation(&self) -> &'static str {
"It is important that WDL code is well-documented. Every task and workflow should have \
both a meta and parameter_meta section. Tasks without an `input` section are permitted to \
skip the `parameter_meta` section."
}
fn examples(&self) -> &'static [Example] {
&[Example {
negative: LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
input {
String name
}
command <<<
echo "Hello, ~{name}!"
>>>
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
meta {
description: "Says hello for the given name"
}
parameter_meta {
name: "The name of the person to greet"
}
input {
String name
}
command <<<
echo "Hello, ~{name}!"
>>>
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Completeness, Tag::Clarity, Tag::Documentation])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::TaskDefinitionNode,
SyntaxKind::WorkflowDefinitionNode,
SyntaxKind::StructDefinitionNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&[
"MetaDescription",
"ParameterMetaMatched",
"OutputSection",
"RequirementsSection",
"RuntimeSection",
"MatchingOutputMeta",
"DescriptionLength",
]
}
}
impl Visitor for MetaSectionsRule {
fn reset(&mut self) {
*self = Default::default();
}
fn document(
&mut self,
_: &mut Diagnostics,
reason: VisitReason,
_: &Document,
version: SupportedVersion,
) {
if reason == VisitReason::Exit {
return;
}
self.version = Some(version);
}
fn task_definition(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
task: &TaskDefinition,
) {
if reason == VisitReason::Exit {
return;
}
let inputs_present = task.input().is_some();
if inputs_present && task.metadata().is_none() && task.parameter_metadata().is_none() {
diagnostics.exceptable_add(
missing_sections(task.name(), Context::Task),
task.inner(),
&self.exceptable_nodes(),
);
} else if task.metadata().is_none() {
diagnostics.exceptable_add(
missing_section(task.name(), Section::Meta, Context::Task),
task.inner(),
&self.exceptable_nodes(),
);
} else if inputs_present && task.parameter_metadata().is_none() {
diagnostics.exceptable_add(
missing_section(task.name(), Section::ParameterMeta, Context::Task),
task.inner(),
&self.exceptable_nodes(),
);
}
}
fn workflow_definition(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
workflow: &WorkflowDefinition,
) {
if reason == VisitReason::Exit {
return;
}
let inputs_present = workflow.input().is_some();
if inputs_present
&& workflow.metadata().is_none()
&& workflow.parameter_metadata().is_none()
{
diagnostics.exceptable_add(
missing_sections(workflow.name(), Context::Workflow),
workflow.inner(),
&self.exceptable_nodes(),
);
} else if workflow.metadata().is_none() {
diagnostics.exceptable_add(
missing_section(workflow.name(), Section::Meta, Context::Workflow),
workflow.inner(),
&self.exceptable_nodes(),
);
} else if inputs_present && workflow.parameter_metadata().is_none() {
diagnostics.exceptable_add(
missing_section(workflow.name(), Section::ParameterMeta, Context::Workflow),
workflow.inner(),
&self.exceptable_nodes(),
);
}
}
fn struct_definition(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
def: &wdl_ast::v1::StructDefinition,
) {
if reason == VisitReason::Exit {
return;
}
if self.version.expect("should have version") < SupportedVersion::V1(V1::Two) {
return;
}
if def.metadata().next().is_none() && def.parameter_metadata().next().is_none() {
diagnostics.exceptable_add(
missing_sections(def.name(), Context::Struct),
def.inner(),
&self.exceptable_nodes(),
);
} else if def.metadata().next().is_none() {
diagnostics.exceptable_add(
missing_section(def.name(), Section::Meta, Context::Struct),
def.inner(),
&self.exceptable_nodes(),
);
} else if def.parameter_metadata().next().is_none() {
diagnostics.exceptable_add(
missing_section(def.name(), Section::ParameterMeta, Context::Struct),
def.inner(),
&self.exceptable_nodes(),
);
}
}
}