1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! A lint rule for newlines at the end of the document.
use wdl_analysis::Diagnostics;
use wdl_analysis::Document;
use wdl_analysis::VisitReason;
use wdl_analysis::Visitor;
use wdl_ast::AstNode;
use wdl_ast::Diagnostic;
use wdl_ast::Span;
use wdl_ast::SupportedVersion;
use wdl_ast::SyntaxKind;
use crate::Rule;
use crate::Tag;
use crate::TagSet;
use crate::util::strip_newline;
/// The identifier for the ending newline rule.
const ID: &str = "EndingNewline";
/// Creates a "missing ending newline" diagnostic.
fn missing_ending_newline(span: Span) -> Diagnostic {
Diagnostic::note("missing newline at the end of the file")
.with_rule(ID)
.with_label("expected a newline to follow this", span)
.with_fix("add a newline at the end of the file")
}
/// Creates a "multiple ending newline" diagnostic.
fn multiple_ending_newline(span: Span, count: usize) -> Diagnostic {
Diagnostic::note("multiple empty lines at the end of file")
.with_rule(ID)
.with_label(
if count > 1 {
"duplicate newlines here"
} else {
"duplicate newline here"
},
span,
)
.with_fix("remove all but one empty line at the end of the file")
}
/// Detects missing newline at the end of the document.
#[derive(Default, Debug, Clone, Copy)]
pub struct EndingNewlineRule;
impl Rule for EndingNewlineRule {
fn id(&self) -> &'static str {
ID
}
fn description(&self) -> &'static str {
"Ensures that documents end with a single newline character."
}
fn explanation(&self) -> &'static str {
"The file should end with one and only one newline character to conform to POSIX standards. See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_206."
}
fn tags(&self) -> TagSet {
TagSet::new(&[Tag::Spacing, Tag::Portability])
}
fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
Some(&[SyntaxKind::VersionStatementNode])
}
fn related_rules(&self) -> &[&'static str] {
&[]
}
}
impl Visitor for EndingNewlineRule {
fn reset(&mut self) {
*self = Self;
}
fn document(
&mut self,
diagnostics: &mut Diagnostics,
reason: VisitReason,
doc: &Document,
_: SupportedVersion,
) {
if reason == VisitReason::Enter {
return;
}
// Get the last token in the document and see if it's whitespace
match doc.root().inner().last_child_or_token() {
Some(last) if last.kind() == SyntaxKind::Whitespace => {
// It's whitespace, check if it ends with a newline
let last = last.into_token().expect("whitespace should be a token");
let start = usize::from(last.text_range().start());
let text = last.text();
let len = text.len();
match strip_newline(last.text()) {
Some(mut text) => {
// Count the number of extra newlines
let mut extra = 0;
while let Some(stripped) = strip_newline(text) {
extra += 1;
text = stripped;
}
if extra > 0 {
// Since this rule can only be excepted in a document-wide fashion,
// if the rule is running we can directly add the diagnostic
// without checking for the exceptable nodes
diagnostics.add(multiple_ending_newline(
Span::new(start + text.len(), len - text.len() - 1),
extra,
));
}
}
// Since this rule can only be excepted in a document-wide fashion,
// if the rule is running we can directly add the diagnostic
// without checking for the exceptable nodes
None => {
diagnostics.add(missing_ending_newline(Span::new(start + (len - 1), 1)))
}
}
}
Some(last) => {
// Since this rule can only be excepted in a document-wide fashion,
// if the rule is running we can directly add the diagnostic
// without checking for the exceptable nodes
diagnostics.add(missing_ending_newline(Span::new(
usize::from(last.text_range().end()) - 1,
1,
)));
}
None => {
// Completely empty file is okay, at least with regard to this
// lint rule
}
}
}
}