Skip to main content

wdl_lint/rules/
empty_doc_comment.rs

1//! A lint rule for empty documentation comments.
2
3use 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
20/// The identifier for the empty doc comment rule.
21const ID: &str = "EmptyDocComment";
22
23/// Creates a diagnostic when an empty documentation comment block is found.
24fn 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/// Detects empty documentation comment blocks.
32#[derive(Default, Debug, Clone, Copy)]
33pub struct EmptyDocCommentRule {
34    /// The number of comment tokens to skip.
35    ///
36    /// This is used to avoid processing comments that have already been
37    /// handled as part of a block.
38    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}