use std::collections::HashSet;
use std::sync::LazyLock;
use wdl_analysis::Diagnostics;
use wdl_analysis::Visitor;
use wdl_analysis::rules as analysis_rules;
use wdl_ast::AstToken;
use wdl_ast::Comment;
use wdl_ast::Diagnostic;
use wdl_ast::Directive;
use wdl_ast::Span;
use wdl_ast::SyntaxKind;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
use crate::rules::RULE_MAP;
use crate::util::find_nearest_rule;
static ANALYSIS_RULES: LazyLock<HashSet<String>> = LazyLock::new(|| {
analysis_rules()
.iter()
.map(|r| r.id().to_string())
.collect()
});
static LINT_RULES: LazyLock<HashSet<String>> =
LazyLock::new(|| RULE_MAP.keys().map(|name| name.to_string()).collect());
const ID: &str = "KnownRules";
fn unknown_rule(id: &str, span: Span) -> Diagnostic {
let mut diagnostic = Diagnostic::note(format!("unknown rule `{id}`"))
.with_rule(ID)
.with_label("cannot make an exception for this rule", span);
if let Some(nearest_rule) = find_nearest_rule(id) {
diagnostic = diagnostic.with_fix(format!("did you mean `{nearest_rule}`?"));
} else {
diagnostic = diagnostic.with_fix("remove the unknown rule from the exception list");
}
diagnostic
}
#[derive(Default, Debug, Clone, Copy)]
pub struct KnownRulesRule;
impl Rule for KnownRulesRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures only known rules are used in `except` directives."
}
fn explanation(&self) -> &'static str {
"When writing WDL, `except` directives are used to suppress certain rules. If a rule is \
unknown, nothing will be suppressed. This rule flags unknown rules as they are often \
mistakes."
}
fn examples(&self) -> &'static [&'static str] {
&[
r#"```wdl
#@ except: LintThatDoesNotExist
version 1.2
workflow example {
meta {}
output {}
}
```"#,
r#"Use instead:
```wdl
version 1.2
workflow example {
meta {}
output {}
}
```"#,
]
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Clarity, Tag::Correctness, Tag::SprocketCompatibility])
}
fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
Some(&[SyntaxKind::VersionStatementNode])
}
fn related_rules(&self) -> &'static [&'static str] {
&[]
}
}
impl Visitor for KnownRulesRule {
fn reset(&mut self) {
*self = Self;
}
fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {
if let Some(Directive::Except(ids)) = comment.directive() {
let start: usize = comment.span().start();
for id in ids {
if !ANALYSIS_RULES.contains(&id) && !LINT_RULES.contains(&id) {
diagnostics.add(unknown_rule(
&id,
Span::new(start + comment.text().find(&id).unwrap(), id.len()),
));
}
}
}
}
}