use std::sync::LazyLock;
use wdl_ast::Severity;
pub const UNUSED_IMPORT_RULE_ID: &str = "UnusedImport";
pub const UNUSED_INPUT_RULE_ID: &str = "UnusedInput";
pub const UNUSED_DECL_RULE_ID: &str = "UnusedDeclaration";
pub const UNUSED_CALL_RULE_ID: &str = "UnusedCall";
pub const UNNECESSARY_FUNCTION_CALL: &str = "UnnecessaryFunctionCall";
pub const USING_FALLBACK_VERSION: &str = "UsingFallbackVersion";
pub static ALL_RULE_IDS: LazyLock<Vec<String>> = LazyLock::new(|| {
let mut ids: Vec<String> = rules().iter().map(|r| r.id().to_string()).collect();
ids.sort();
ids
});
pub trait Rule: Send + Sync {
fn id(&self) -> &'static str;
fn description(&self) -> &'static str;
fn explanation(&self) -> &'static str;
fn examples(&self) -> &'static [&'static str];
fn deny(&mut self);
fn severity(&self) -> Severity;
}
pub fn rules() -> Vec<Box<dyn Rule>> {
let rules: Vec<Box<dyn Rule>> = vec![
Box::<UnusedImportRule>::default(),
Box::<UnusedInputRule>::default(),
Box::<UnusedDeclarationRule>::default(),
Box::<UnusedCallRule>::default(),
Box::<UnnecessaryFunctionCall>::default(),
Box::<UsingFallbackVersion>::default(),
];
#[cfg(debug_assertions)]
{
use convert_case::Case;
use convert_case::Casing;
let mut set = std::collections::HashSet::new();
for r in rules.iter() {
if r.id().to_case(Case::Pascal) != r.id() {
panic!("analysis rule id `{id}` is not pascal case", id = r.id());
}
if !set.insert(r.id()) {
panic!("duplicate rule id `{id}`", id = r.id());
}
}
}
rules
}
#[derive(Debug, Clone, Copy)]
pub struct UnusedImportRule(Severity);
impl UnusedImportRule {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UnusedImportRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for UnusedImportRule {
fn id(&self) -> &'static str {
UNUSED_IMPORT_RULE_ID
}
fn description(&self) -> &'static str {
"Ensures that import namespaces are used in the importing document."
}
fn explanation(&self) -> &'static str {
"Imported WDL documents should be used in the document that imports them. Unused imports \
impact parsing and evaluation performance."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
version 1.2
import "example2.wdl"
workflow example {
meta {}
output {}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnusedInputRule(Severity);
impl UnusedInputRule {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UnusedInputRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for UnusedInputRule {
fn id(&self) -> &'static str {
UNUSED_INPUT_RULE_ID
}
fn description(&self) -> &'static str {
"Ensures that task or workspace inputs are used within the declaring task or workspace."
}
fn explanation(&self) -> &'static str {
"Unused inputs degrade evaluation performance and reduce the clarity of the code. Unused \
file inputs in tasks can also cause unnecessary file localizations."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
version 1.2
workflow example {
meta {}
input {
String unused
}
output {}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnusedDeclarationRule(Severity);
impl UnusedDeclarationRule {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UnusedDeclarationRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for UnusedDeclarationRule {
fn id(&self) -> &'static str {
UNUSED_DECL_RULE_ID
}
fn description(&self) -> &'static str {
"Ensures that private declarations in tasks or workspaces are used within the declaring \
task or workspace."
}
fn explanation(&self) -> &'static str {
"Unused private declarations degrade evaluation performance and reduce the clarity of the \
code."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
version 1.2
workflow example {
meta {}
String unused = "this will produce a warning"
output {}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnusedCallRule(Severity);
impl UnusedCallRule {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UnusedCallRule {
fn default() -> Self {
Self::new()
}
}
impl Rule for UnusedCallRule {
fn id(&self) -> &'static str {
UNUSED_CALL_RULE_ID
}
fn description(&self) -> &'static str {
"Ensures that outputs of a call statement are used in the declaring workflow."
}
fn explanation(&self) -> &'static str {
"Unused calls may cause unnecessary consumption of compute resources."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
version 1.2
workflow example {
meta {}
# The output of `do_work` is never used
call do_work
output {}
}
task do_work {
command <<<>>>
output {
Int x = 0
}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct UnnecessaryFunctionCall(Severity);
impl UnnecessaryFunctionCall {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UnnecessaryFunctionCall {
fn default() -> Self {
Self::new()
}
}
impl Rule for UnnecessaryFunctionCall {
fn id(&self) -> &'static str {
UNNECESSARY_FUNCTION_CALL
}
fn description(&self) -> &'static str {
"Ensures that function calls are necessary."
}
fn explanation(&self) -> &'static str {
"Unnecessary function calls may impact evaluation performance."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
version 1.2
workflow example {
meta {}
# Calls to `defined` on values that are statically
# known to be non-None are unnecessary.
Boolean exists = defined("hello")
output {}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}
#[derive(Debug, Clone, Copy)]
pub struct UsingFallbackVersion(Severity);
impl UsingFallbackVersion {
pub fn new() -> Self {
Self(Severity::Warning)
}
}
impl Default for UsingFallbackVersion {
fn default() -> Self {
Self::new()
}
}
impl Rule for UsingFallbackVersion {
fn id(&self) -> &'static str {
USING_FALLBACK_VERSION
}
fn description(&self) -> &'static str {
"Warns if interpretation of a document with an unsupported version falls back to a default."
}
fn explanation(&self) -> &'static str {
"A document with an unsupported version may have unpredictable behavior if interpreted as \
a different version."
}
fn examples(&self) -> &'static [&'static str] {
&[r#"```wdl
# Not a valid version. If a fallback version is configured,
# the document will be interpreted as that version.
version development
workflow example {
meta {}
output {}
}
```"#]
}
fn deny(&mut self) {
self.0 = Severity::Error;
}
fn severity(&self) -> Severity {
self.0
}
}