use wdl_analysis::Diagnostics;
use wdl_analysis::Visitor;
use wdl_ast::AstToken;
use wdl_ast::Comment;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
use crate::util::is_inline_comment;
const ID: &str = "LintDirectiveFormatted";
const ACCEPTED_LINT_DIRECTIVES: [&str; 1] = ["except"];
fn excessive_whitespace(span: Span) -> Diagnostic {
Diagnostic::warning("expected exactly one space before lint directive")
.with_rule(ID)
.with_label("this whitespace is unexpected", span)
.with_fix("replace this whitespace with a single space")
}
fn inline_lint_directive(span: Span) -> Diagnostic {
Diagnostic::warning("lint directive must be on its own line")
.with_rule(ID)
.with_highlight(span)
.with_fix("move the lint directive to its own line")
}
fn invalid_lint_directive(name: &str, span: Span) -> Diagnostic {
let accepted_directives = ACCEPTED_LINT_DIRECTIVES.join(", ");
Diagnostic::warning(format!("lint directive `{name}` is not recognized"))
.with_rule(ID)
.with_highlight(span)
.with_fix(format!(
"use any of the recognized lint directives: [{accepted_directives:#?}]"
))
}
fn missing_lint_directive(span: Span) -> Diagnostic {
Diagnostic::warning("lint directive not found")
.with_rule(ID)
.with_label("missing lint directive", span)
.with_fix("add a lint directive or change `#@` prefix")
}
fn missing_whitespace(span: Span) -> Diagnostic {
Diagnostic::warning("expected exactly one space before lint directive")
.with_rule(ID)
.with_label("expected a space before this", span)
.with_fix("add a single space")
}
fn no_colon_detected(span: Span) -> Diagnostic {
Diagnostic::warning("expected a colon to follow a lint directive")
.with_rule(ID)
.with_label("expected a colon here", span)
.with_fix("add a colon after the lint directive")
}
#[derive(Default, Debug, Clone, Copy)]
pub struct LintDirectiveFormattedRule;
impl Rule for LintDirectiveFormattedRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures lint directives are correctly formatted."
}
fn explanation(&self) -> &'static str {
"This rule checks that lint directives are properly formatted.\nLint directives must be on \
their own line, only preceded by whitespace. They should follow the pattern `#@ \
<directive>: <value>` _exactly_. Currently the only accepted lint directive is `except`. \
For example, `#@ except: LintDirectiveFormatted`."
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Clarity, Tag::Correctness])
}
fn exceptable_nodes(&self) -> Option<&'static [wdl_ast::SyntaxKind]> {
None
}
fn related_rules(&self) -> &[&'static str] {
&[]
}
}
impl Visitor for LintDirectiveFormattedRule {
fn reset(&mut self) {
*self = Self;
}
fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {
if let Some(lint_directive) = comment.text().strip_prefix("#@") {
let base_offset = comment.span().start();
if is_inline_comment(comment) {
diagnostics.add(inline_lint_directive(comment.span()));
}
if lint_directive.trim().is_empty() {
diagnostics.add(missing_lint_directive(Span::new(
base_offset + 2,
lint_directive.len(),
)));
return;
}
if !lint_directive.starts_with(" ") {
diagnostics.add(missing_whitespace(Span::new(base_offset + 2, 1)));
}
if lint_directive.starts_with(" ") {
let leading_whitespace_len =
lint_directive.len() - lint_directive.trim_start().len();
diagnostics.add(excessive_whitespace(Span::new(
base_offset + 2,
leading_whitespace_len,
)));
}
if let Some(mut directive) = lint_directive.trim().split(" ").next() {
if !directive.ends_with(":") {
diagnostics.add(no_colon_detected(Span::new(
base_offset + 3 + directive.chars().count(),
1,
)));
} else if let Some(stripped_directive) = directive.strip_suffix(":") {
directive = stripped_directive;
}
if !ACCEPTED_LINT_DIRECTIVES.contains(&directive) {
diagnostics.add(invalid_lint_directive(
directive,
Span::new(base_offset + 3, directive.chars().count()),
));
}
}
}
}
}