use wdl_analysis::Diagnostics;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
use wdl_ast::AstToken;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SyntaxElement;
use wdl_ast::SyntaxKind;
use wdl_ast::VersionStatement;
use wdl_ast::Whitespace;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
use crate::util::lines_with_offset;
const ID: &str = "Whitespace";
fn only_whitespace(span: Span) -> Diagnostic {
Diagnostic::note("line contains only whitespace")
.with_rule(ID)
.with_highlight(span)
.with_fix("remove the whitespace")
}
fn trailing_whitespace(span: Span) -> Diagnostic {
Diagnostic::note("line contains trailing whitespace")
.with_rule(ID)
.with_highlight(span)
.with_fix("remove the trailing whitespace")
}
fn more_than_one_blank_line(span: Span) -> Diagnostic {
Diagnostic::note("more than one blank line in a row")
.with_rule(ID)
.with_highlight(span)
.with_fix("remove the extra blank lines")
}
#[derive(Default, Debug, Clone, Copy)]
pub struct WhitespaceRule {
version_seen: bool,
}
impl Rule for WhitespaceRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that a document does not contain undesired whitespace."
}
fn explanation(&self) -> &'static str {
"Whitespace should be used judiciously. Spurious whitespace can cause issues with parsing, \
automation, and rendering. There should never be trailing whitespace at the end of lines \
and blank lines should be completely empty with no whitespace characters between \
newlines. There should be at most one empty line in a row."
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Style, Tag::Spacing])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
None
}
fn related_rules(&self) -> &[&'static str] {
&[]
}
}
impl Visitor for WhitespaceRule {
fn reset(&mut self) {
*self = Self::default();
}
fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &wdl_ast::Comment) {
let comment_str = comment.text();
let span = comment.span();
let trimmed_end = comment_str.trim_end();
if comment_str != trimmed_end {
diagnostics.exceptable_add(
trailing_whitespace(Span::new(
span.start() + trimmed_end.len(),
comment_str.len() - trimmed_end.len(),
)),
SyntaxElement::from(comment.inner().clone()),
&self.exceptable_nodes(),
)
}
}
fn version_statement(
&mut self,
_: &mut Diagnostics,
reason: VisitReason,
_: &VersionStatement,
) {
if reason != VisitReason::Exit {
return;
}
self.version_seen = true;
}
fn whitespace(&mut self, diagnostics: &mut Diagnostics, whitespace: &Whitespace) {
if !self.version_seen {
return;
}
let is_last = whitespace
.inner()
.parent()
.expect("should have a parent")
.kind()
== SyntaxKind::RootNode
&& whitespace.inner().next_sibling_or_token().is_none();
let text = whitespace.text();
let span = whitespace.span();
let mut blank_start = None;
for (i, (line, start, next_start)) in lines_with_offset(text).enumerate() {
let ends_with_newline = text.as_bytes().get(next_start - 1) == Some(&b'\n');
if !line.is_empty() && (ends_with_newline || is_last) {
if i == 0 {
diagnostics.exceptable_add(
trailing_whitespace(Span::new(span.start() + start, line.len())),
SyntaxElement::from(whitespace.inner().clone()),
&self.exceptable_nodes(),
);
} else {
diagnostics.exceptable_add(
only_whitespace(Span::new(span.start() + start, line.len())),
SyntaxElement::from(whitespace.inner().clone()),
&self.exceptable_nodes(),
);
}
}
if i == 2 && ends_with_newline {
blank_start = Some(start);
}
}
if !is_last && blank_start.is_some() {
diagnostics.exceptable_add(
more_than_one_blank_line(span),
SyntaxElement::from(whitespace.inner().clone()),
&self.exceptable_nodes(),
);
}
}
}