wdl_lint/rules/
empty_doc_comment.rs1use wdl_analysis::Diagnostics;
4use wdl_analysis::Example;
5use wdl_analysis::LabeledSnippet;
6use wdl_analysis::Visitor;
7use wdl_ast::AstToken;
8use wdl_ast::Comment;
9use wdl_ast::CommentKind;
10use wdl_ast::DOC_COMMENT_PREFIX;
11use wdl_ast::Diagnostic;
12use wdl_ast::Span;
13use wdl_ast::SyntaxKind;
14use wdl_ast::TreeToken;
15
16use crate::Rule;
17use crate::Tag;
18use crate::TagSet;
19
20const ID: &str = "EmptyDocComment";
22
23fn empty_doc_comment(span: Span) -> Diagnostic {
25 Diagnostic::note("empty doc comment block")
26 .with_rule(ID)
27 .with_highlight(span)
28 .with_help("consider adding meaningful documentation text or removing the comment block")
29}
30
31#[derive(Default, Debug, Clone, Copy)]
33pub struct EmptyDocCommentRule {
34 skip_count: usize,
39}
40
41impl Rule for EmptyDocCommentRule {
42 fn id(&self) -> &'static str {
43 ID
44 }
45
46 fn description(&self) -> &'static str {
47 "Ensures that documentation comment blocks are not empty."
48 }
49
50 fn explanation(&self) -> &'static str {
51 "Documentation comment blocks (consecutive lines starting with `##`) where all lines are \
52 empty serve no purpose. Either add meaningful text to the documentation comment block or \
53 remove it entirely."
54 }
55
56 fn examples(&self) -> &'static [Example] {
57 &[Example {
58 negative: LabeledSnippet {
59 label: None,
60 snippet: r#"version 1.2
61
62# This will render nothing!
63
64##
65struct Person {
66 String name
67 Int age
68}"#,
69 },
70 revised: None,
71 }]
72 }
73
74 fn tags(&self) -> TagSet {
75 TagSet::new(&[Tag::Clarity, Tag::Documentation])
76 }
77
78 fn exceptable_nodes(&self) -> Option<&'static [SyntaxKind]> {
79 None
80 }
81
82 fn related_rules(&self) -> &'static [&'static str] {
83 &["UnusedDocComments"]
84 }
85}
86
87impl Visitor for EmptyDocCommentRule {
88 fn reset(&mut self) {
89 *self = Self::default();
90 }
91
92 fn comment(&mut self, diagnostics: &mut Diagnostics, comment: &Comment) {
93 if self.skip_count > 0 {
94 self.skip_count -= 1;
95 return;
96 }
97
98 if comment.kind() != CommentKind::Documentation {
99 return;
100 }
101
102 let first_span = comment.span();
103 let mut last_span = first_span;
104 let mut all_empty = {
105 let text = comment.text();
106 let content = text.strip_prefix(DOC_COMMENT_PREFIX).unwrap_or(text);
107 content.trim().is_empty()
108 };
109
110 let mut current = comment.inner().next_sibling_or_token();
111
112 while let Some(sibling) = current {
113 match sibling.kind() {
114 SyntaxKind::Comment => {
115 let Some(c) = Comment::cast(sibling.as_token().unwrap().clone()) else {
116 break;
117 };
118
119 if c.kind() != CommentKind::Documentation {
120 break;
121 }
122
123 let text = c.text();
124 let content = text.strip_prefix(DOC_COMMENT_PREFIX).unwrap_or(text);
125 if !content.trim().is_empty() {
126 all_empty = false;
127 }
128
129 last_span = c.span();
130 self.skip_count += 1;
131 }
132 SyntaxKind::Whitespace => {}
133 _ => {
134 break;
135 }
136 }
137
138 current = sibling.next_sibling_or_token();
139 }
140
141 if all_empty {
142 let span = Span::new(first_span.start(), last_span.end() - first_span.start());
143
144 diagnostics.exceptable_add(
145 empty_doc_comment(span),
146 &TreeToken::parent(comment.inner()),
147 &self.exceptable_nodes(),
148 );
149 }
150 }
151}