use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::OnceLock;
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::Span;
use wdl_ast::SupportedVersion;
use wdl_ast::SyntaxKind;
use wdl_ast::v1::RuntimeItem;
use wdl_ast::v1::RuntimeSection;
use wdl_ast::v1::TASK_HINT_INPUTS;
use wdl_ast::v1::TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS;
use wdl_ast::v1::TASK_HINT_MAX_CPU_ALIAS;
use wdl_ast::v1::TASK_HINT_MAX_MEMORY_ALIAS;
use wdl_ast::v1::TASK_HINT_OUTPUTS;
use wdl_ast::v1::TASK_HINT_SHORT_TASK_ALIAS;
use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER;
use wdl_ast::v1::TASK_REQUIREMENT_CONTAINER_ALIAS;
use wdl_ast::v1::TASK_REQUIREMENT_CPU;
use wdl_ast::v1::TASK_REQUIREMENT_DISKS;
use wdl_ast::v1::TASK_REQUIREMENT_GPU;
use wdl_ast::v1::TASK_REQUIREMENT_MAX_RETRIES_ALIAS;
use wdl_ast::v1::TASK_REQUIREMENT_MEMORY;
use wdl_ast::v1::TASK_REQUIREMENT_RETURN_CODES_ALIAS;
use wdl_ast::version::V1;
use crate::Config;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
use crate::util::serialize_oxford_comma;
const ID: &str = "ExpectedRuntimeKeys";
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum KeyKind {
Deprecated(
&'static str,
),
Recommended,
ReservedHint,
ReservedMandatory,
}
impl KeyKind {
pub fn is_recommended(&self) -> bool {
*self == KeyKind::Recommended
}
}
fn keys_v1_0() -> &'static HashMap<&'static str, KeyKind> {
static KEYS_V1_0: OnceLock<HashMap<&'static str, KeyKind>> = OnceLock::new();
KEYS_V1_0.get_or_init(|| {
let mut keys = HashMap::new();
keys.insert(TASK_REQUIREMENT_CONTAINER_ALIAS, KeyKind::Recommended);
keys.insert(TASK_REQUIREMENT_MEMORY, KeyKind::Recommended);
keys
})
}
fn keys_v1_1() -> &'static HashMap<&'static str, KeyKind> {
static KEYS_V1_1: OnceLock<HashMap<&'static str, KeyKind>> = OnceLock::new();
KEYS_V1_1.get_or_init(|| {
let mut keys = HashMap::new();
keys.insert(TASK_REQUIREMENT_CONTAINER, KeyKind::Recommended);
keys.insert(
TASK_REQUIREMENT_CONTAINER_ALIAS,
KeyKind::Deprecated(TASK_REQUIREMENT_CONTAINER),
);
keys.insert(TASK_REQUIREMENT_CPU, KeyKind::ReservedMandatory);
keys.insert(TASK_REQUIREMENT_MEMORY, KeyKind::ReservedMandatory);
keys.insert(TASK_REQUIREMENT_GPU, KeyKind::ReservedMandatory);
keys.insert(TASK_REQUIREMENT_DISKS, KeyKind::ReservedMandatory);
keys.insert(
TASK_REQUIREMENT_MAX_RETRIES_ALIAS,
KeyKind::ReservedMandatory,
);
keys.insert(
TASK_REQUIREMENT_RETURN_CODES_ALIAS,
KeyKind::ReservedMandatory,
);
keys.insert(TASK_HINT_MAX_CPU_ALIAS, KeyKind::ReservedHint);
keys.insert(TASK_HINT_MAX_MEMORY_ALIAS, KeyKind::ReservedHint);
keys.insert(TASK_HINT_SHORT_TASK_ALIAS, KeyKind::ReservedHint);
keys.insert(TASK_HINT_LOCALIZATION_OPTIONAL_ALIAS, KeyKind::ReservedHint);
keys.insert(TASK_HINT_INPUTS, KeyKind::ReservedHint);
keys.insert(TASK_HINT_OUTPUTS, KeyKind::ReservedHint);
keys
})
}
fn deprecated_runtime_key(key: &Ident, replacement: &str) -> Diagnostic {
Diagnostic::note(format!(
"the `{key}` runtime key has been deprecated in favor of `{replacement}`",
key = key.text()
))
.with_rule(ID)
.with_highlight(key.span())
.with_fix(format!(
"replace the `{key}` key with `{replacement}`",
key = key.text()
))
}
fn report_non_reserved_runtime_key(key: &str, span: Span, specification: &str) -> Diagnostic {
Diagnostic::warning(format!(
"the runtime key `{key}` is not reserved in {specification}; arbitrary runtime keys are \
deprecated"
))
.with_rule(ID)
.with_highlight(span)
.with_fix(format!("remove the `{key}` key"))
}
fn report_missing_recommended_keys(
mut keys: Vec<&str>,
runtime_span: Span,
specification: &str,
) -> Diagnostic {
assert!(!keys.is_empty());
keys.sort();
let (message, fix) = if keys.len() == 1 {
let key = keys.first().unwrap();
(
format!("the following runtime key is recommended by {specification}: `{key}`"),
format!("include an entry for the `{key}` key in the `runtime` section"),
)
} else {
let keys = serialize_oxford_comma(
&keys
.iter()
.map(|key| format!("`{key}`"))
.collect::<Vec<_>>(),
)
.unwrap();
(
format!("the following runtime keys are recommended by {specification}: {keys}"),
format!("include entries for the {keys} keys in the `runtime` section"),
)
};
Diagnostic::note(message)
.with_rule(ID)
.with_highlight(runtime_span)
.with_fix(fix)
}
#[derive(Debug, Clone)]
pub struct ExpectedRuntimeKeysRule {
version: Option<SupportedVersion>,
runtime_processed_for_task: bool,
encountered_keys: Vec<String>,
allowed_runtime_keys: HashSet<String>,
}
impl ExpectedRuntimeKeysRule {
pub fn new(config: &Config) -> Self {
Self {
version: None,
runtime_processed_for_task: false,
encountered_keys: Vec::new(),
allowed_runtime_keys: HashSet::from_iter(config.allowed_runtime_keys.iter().cloned()),
}
}
}
impl Rule for ExpectedRuntimeKeysRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that `runtime` sections have the appropriate keys."
}
fn explanation(&self) -> &'static str {
"The behavior of this rule is different depending on the WDL version:
For WDL v1.0 documents, the `docker` and `memory` keys are recommended, but the inclusion of any \
number of other keys is permitted.
For WDL v1.1 documents:
- A list of mandatory, reserved keywords will be recommended for inclusion if they are not \
present. Here, 'mandatory' refers to the requirement that all execution engines support \
this key—not that the key must be present in the `runtime` section.
- Optional, reserved \"hint\" keys are also permitted but not flagged when they are missing (as \
their support in execution engines is not guaranteed).
- The WDL v1.1 specification deprecates the inclusion of non-reserved keys in a `runtime` \
section. As such, any non-reserved keys will be flagged for removal.
For WDL v1.2 documents and later, this rule does not evaluate because `runtime` sections were \
deprecated in this version."
}
fn examples(&self) -> &'static [Example] {
&[
Example {
negative: LabeledSnippet {
label: Some("The following is missing a mandatory key"),
snippet: r#"version 1.1
task missing_required_keys {
runtime {
# Missing `container` key
}
}
"#,
},
revised: None,
},
Example {
negative: LabeledSnippet {
label: Some("The following has an unexpected key"),
snippet: r#"version 1.1
task unexpected_runtime_key {
runtime {
container: "ubuntu"
foo: "bar"
}
}
"#,
},
revised: None,
},
]
}
fn tags(&self) -> crate::TagSet {
TagSet::new(&[Tag::Completeness, Tag::Deprecated])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[
SyntaxKind::VersionStatementNode,
SyntaxKind::RuntimeSectionNode,
SyntaxKind::RuntimeItemNode,
])
}
fn related_rules(&self) -> &'static [&'static str] {
&["DeprecatedObject", "DeprecatedPlaceholder"]
}
}
fn recommended_keys<'a, 'k>(
keys: &'a HashMap<&'k str, KeyKind>,
) -> impl Iterator<Item = (&'k str, &'a KeyKind)> {
keys.iter()
.filter(|(_, kind)| kind.is_recommended())
.map(|(key, kind)| (*key, kind))
}
impl Visitor for ExpectedRuntimeKeysRule {
fn reset(&mut self) {
self.version = None;
self.encountered_keys.clear();
}
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,
_: &mut Diagnostics,
reason: VisitReason,
_: &wdl_ast::v1::TaskDefinition,
) {
if reason == VisitReason::Exit {
self.runtime_processed_for_task = false;
}
}
fn runtime_section(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
section: &RuntimeSection,
) {
if self.runtime_processed_for_task {
return;
}
match reason {
VisitReason::Enter => {}
VisitReason::Exit => {
if let SupportedVersion::V1(minor_version) = self.version.unwrap() {
let specification = format!("the WDL {minor_version} specification");
let recommended_keys = match minor_version {
V1::Zero => recommended_keys(keys_v1_0()),
V1::One => recommended_keys(keys_v1_1()),
_ => return,
};
let missing_keys = recommended_keys
.filter(|(key, _)| !self.encountered_keys.iter().any(|s| s == *key))
.map(|(key, _)| key)
.collect::<Vec<_>>();
if !missing_keys.is_empty() {
diagnostics.exceptable_add(
report_missing_recommended_keys(
missing_keys,
section
.inner()
.first_token()
.expect("runtime section should have tokens")
.text_range()
.into(),
&specification,
),
section.inner(),
&self.exceptable_nodes(),
);
}
self.encountered_keys.clear();
self.runtime_processed_for_task = true;
}
}
}
}
fn runtime_item(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
item: &RuntimeItem,
) {
if self.runtime_processed_for_task || reason == VisitReason::Exit {
return;
}
let key_name = item.name();
if let SupportedVersion::V1(minor_version) = self.version.unwrap() {
if minor_version == V1::One {
match keys_v1_1().get(key_name.text()) {
Some(kind) => {
if let KeyKind::Deprecated(replacement) = kind {
diagnostics.exceptable_add(
deprecated_runtime_key(&key_name, replacement),
item.inner(),
&self.exceptable_nodes(),
);
}
}
None => {
let specification = format!("the WDL {minor_version} specification");
let key_text = key_name.text();
if !self.allowed_runtime_keys.contains(key_text) {
let text_for_key_span = item
.inner()
.first_token()
.expect("RuntimeItem must have text in first token")
.text_range()
.into();
diagnostics.exceptable_add(
report_non_reserved_runtime_key(
key_text,
text_for_key_span,
&specification,
),
item.inner(),
&self.exceptable_nodes(),
);
}
}
}
}
}
self.encountered_keys.push(key_name.text().to_string());
}
}