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::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SyntaxKind;
use wdl_ast::SyntaxNode;
use wdl_ast::v1::RequirementsSection;
use wdl_ast::v1::RuntimeSection;
use wdl_ast::v1::common::container::Kind;
use wdl_ast::v1::common::container::value::Value;
use wdl_ast::v1::common::container::value::uri::ANY_CONTAINER_VALUE;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
const ID: &str = "ContainerUri";
#[derive(Default, Debug, Clone, Copy)]
pub struct ContainerUriRule;
fn missing_tag(span: Span) -> Diagnostic {
Diagnostic::warning(String::from("container URI is missing a tag"))
.with_rule(ID)
.with_highlight(span)
.with_fix(
"add a tag to the container URI (e.g., `ubuntu@sha256:foobar` instead of `ubuntu`)",
)
}
fn mutable_tag(span: Span) -> Diagnostic {
Diagnostic::note(String::from("container URI uses a mutable tag"))
.with_rule(ID)
.with_highlight(span)
.with_fix(
"replace the mutable tag with its SHA256 equivalent (e.g., `ubuntu@sha256:foobar` \
instead of `ubuntu:latest`)",
)
}
fn empty_array(span: Span) -> Diagnostic {
Diagnostic::warning(String::from(
"empty arrays are ambiguous and should contain at least one entry",
))
.with_rule(ID)
.with_highlight(span)
.with_fix("add an entry or remove the entry altogether")
}
fn array_to_string_literal(span: Span) -> Diagnostic {
Diagnostic::note(String::from(
"an array with a single value should be a string literal",
))
.with_rule(ID)
.with_highlight(span)
.with_fix("change the array to a string literal representing the first value")
}
fn array_containing_anys(spans: impl Iterator<Item = Span>) -> Diagnostic {
let mut diagnostic = Diagnostic::warning(format!(
"container arrays containing `{ANY_CONTAINER_VALUE}` are ambiguous"
))
.with_rule(ID)
.with_fix(format!(
"remove these entries or change the array to a string literal with the value of \
`{ANY_CONTAINER_VALUE}`"
));
for span in spans {
diagnostic = diagnostic.with_highlight(span)
}
diagnostic
}
impl Rule for ContainerUriRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that values for the `container` key within `runtime`/`requirements` sections are \
well-formed."
}
fn explanation(&self) -> &'static str {
"This rule checks the following:
- Containers should have a tag, as container URIs with no tags have no expectation that the \
behavior of the containers won't change between runs.
- Further, immutable containers tagged with SHA256 sums are preferred. This is due to the \
requirement from the WDL specification that tasks produce functionally equivalent output \
across runs. When a mutable tag is used, there is a risk that changes to the container \
will cause different behavior between runs.
- Use of the 'any' container URI (`*`) within an array of container URIs is ambiguous and should \
be avoided.
- Empty container URI arrays are not disallowed by the specification but are ambiguous and should \
be avoided.
- An array of container URIs with a single element should be changed to a single string value."
}
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}!"
>>>
# No tag
requirements {
container: "ubuntu"
}
}
task say_goodbye {
input {
String name
}
command <<<
echo "Goodbye, ~{name}!"
>>>
# Unnecessary array
requirements {
container: [
"ubuntu@sha256:cc925e589b7543b910fea57a240468940003fbfc0515245a495dd0ad8fe7cef1",
]
}
}
"#,
},
revised: Some(LabeledSnippet {
label: None,
snippet: r#"version 1.2
task say_hello {
input {
String name
}
command <<<
echo "Hello, ~{name}!"
>>>
requirements {
container: "ubuntu@sha256:cc925e589b7543b910fea57a240468940003fbfc0515245a495dd0ad8fe7cef1"
}
}
task say_goodbye {
input {
String name
}
command <<<
echo "Goodbye, ~{name}!"
>>>
requirements {
container: "ubuntu@sha256:cc925e589b7543b910fea57a240468940003fbfc0515245a495dd0ad8fe7cef1"
}
}
"#,
}),
}]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Clarity, Tag::Portability])
}
fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::RuntimeSectionNode,
SyntaxKind::RequirementsSectionNode,
SyntaxKind::RequirementsItemNode,
SyntaxKind::RuntimeItemNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&[]
}
}
impl Visitor for ContainerUriRule {
fn reset(&mut self) {
*self = Self;
}
fn runtime_section(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
section: &RuntimeSection,
) {
if reason == VisitReason::Exit {
return;
}
if let Some(container) = section.container()
&& let Ok(value) = container.value()
{
check_container_value(
diagnostics,
value,
container.inner(),
&self.exceptable_nodes(),
);
}
}
fn requirements_section(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
section: &RequirementsSection,
) {
if reason == VisitReason::Exit {
return;
}
if let Some(container) = section.container()
&& let Ok(value) = container.value()
{
check_container_value(
diagnostics,
value,
container.inner(),
&self.exceptable_nodes(),
);
}
}
}
fn check_container_value(
diagnostics: &mut Diagnostics,
value: Value,
node: &SyntaxNode,
exceptable_nodes: &Option<&'static [SyntaxKind]>,
) {
if let Kind::Array(array) = value.kind() {
if array.is_empty() {
diagnostics.exceptable_add(empty_array(value.expr().span()), node, exceptable_nodes);
} else if array.len() == 1 {
let uri = array.iter().next().unwrap();
diagnostics.exceptable_add(
array_to_string_literal(uri.literal_string().span()),
node,
exceptable_nodes,
);
} else {
let mut anys = array.iter().filter(|uri| uri.kind().is_any()).peekable();
if anys.peek().is_some() {
diagnostics.exceptable_add(
array_containing_anys(anys.map(|any| any.literal_string().span())),
node,
exceptable_nodes,
);
}
}
}
for uri in value.uris() {
if let Some(entry) = uri.kind().as_entry() {
if entry.tag().is_none() {
diagnostics.exceptable_add(
missing_tag(uri.literal_string().span()),
node,
exceptable_nodes,
);
} else if !entry.immutable() {
diagnostics.exceptable_add(
mutable_tag(uri.literal_string().span()),
node,
exceptable_nodes,
);
}
}
}
}