mago_syntax/comments/docblock.rs
1use mago_database::file::File;
2use mago_span::HasSpan;
3
4use crate::ast::Program;
5use crate::ast::Trivia;
6use crate::ast::TriviaKind;
7
8/// Retrieves the docblock comment associated with a given node in the program.
9/// If the node is preceded by a docblock comment, it returns that comment.
10///
11/// This function searches for the last docblock comment that appears before the node's start position,
12/// ensuring that it is directly preceding the node without any non-whitespace characters in between.
13///
14/// # Arguments
15///
16/// * `program` - The program containing the trivia.
17/// * `file` - The file from which the trivia is derived.
18/// * `node` - The node for which to find the preceding docblock comment.
19///
20/// # Returns
21///
22/// An `Option` containing a reference to the `Trivia` representing the docblock comment if found,
23/// or `None` if no suitable docblock comment exists before the node.
24#[inline]
25pub fn get_docblock_for_node<'arena>(
26 program: &'arena Program<'arena>,
27 file: &File,
28 node: impl HasSpan,
29) -> Option<&'arena Trivia<'arena>> {
30 get_docblock_before_position(file, program.trivia.as_slice(), node.span().start.offset)
31}
32
33/// Retrieves the docblock comment that appears before a specific position in the source code.
34///
35/// This function scans the trivia associated with the source code and returns the last docblock comment
36/// that appears before the specified position, ensuring that it is directly preceding the node
37/// without any non-whitespace characters in between.
38///
39/// # Arguments
40///
41/// * `file` - The file from which the trivia is derived.
42/// * `trivias` - A slice of trivia associated with the source code.
43/// * `node_start_offset` - The start offset of the node for which to find the preceding docblock comment.
44///
45/// # Returns
46///
47/// An `Option` containing a reference to the `Trivia` representing the docblock comment if found,
48pub fn get_docblock_before_position<'arena>(
49 file: &File,
50 trivias: &'arena [Trivia<'arena>],
51 node_start_offset: u32,
52) -> Option<&'arena Trivia<'arena>> {
53 let candidate_partition_idx = trivias.partition_point(|trivia| trivia.span.start.offset < node_start_offset);
54 if candidate_partition_idx == 0 {
55 return None;
56 }
57
58 // Track the earliest position we've "covered" by trivia.
59 // Start from node_start_offset and work backwards.
60 // As we iterate, we verify that each trivia connects to the next (no code gaps).
61 let mut covered_from = node_start_offset;
62
63 for i in (0..candidate_partition_idx).rev() {
64 let trivia = &trivias[i];
65 let trivia_end = trivia.span.end_offset();
66
67 // Check if there's a gap between this trivia and our covered region.
68 // If there's non-whitespace content in the gap, there's actual code between them.
69 let gap_slice = file.contents.as_bytes().get(trivia_end as usize..covered_from as usize).unwrap_or(&[]);
70
71 if !gap_slice.iter().all(u8::is_ascii_whitespace) {
72 // There's actual code in the gap. No docblock applies.
73 return None;
74 }
75
76 match trivia.kind {
77 TriviaKind::DocBlockComment => {
78 // Found a docblock with no code between it and the node.
79 return Some(trivia);
80 }
81 TriviaKind::WhiteSpace
82 | TriviaKind::SingleLineComment
83 | TriviaKind::MultiLineComment
84 | TriviaKind::HashComment => {
85 covered_from = trivia.span.start_offset();
86 }
87 }
88 }
89
90 // Iterated through all preceding trivia without finding a suitable docblock.
91 None
92}