mago_syntax/comments/
docblock.rs1use mago_span::HasSpan;
2
3use crate::ast::Program;
4use crate::ast::Trivia;
5use crate::ast::TriviaKind;
6
7pub struct PrecedingDocblocks<'arena, 'pat> {
15 trivia: &'arena [Trivia<'arena>],
16 start: u32,
17 important_patterns: &'pat [&'pat [u8]],
18}
19
20impl<'arena> PrecedingDocblocks<'arena, 'static> {
21 #[must_use]
22 pub fn new(trivia: &'arena [Trivia<'arena>], start_offset: u32) -> Self {
23 Self { trivia, start: start_offset, important_patterns: &[] }
24 }
25}
26
27impl<'arena> PrecedingDocblocks<'arena, '_> {
28 #[must_use]
32 pub fn important_only<'new_pat>(
33 self,
34 patterns: &'new_pat [&'new_pat [u8]],
35 ) -> PrecedingDocblocks<'arena, 'new_pat> {
36 PrecedingDocblocks { trivia: self.trivia, start: self.start, important_patterns: patterns }
37 }
38}
39
40impl<'arena> Iterator for PrecedingDocblocks<'arena, '_> {
41 type Item = &'arena Trivia<'arena>;
42
43 fn next(&mut self) -> Option<Self::Item> {
44 loop {
45 let trivia = get_docblock_before_position(self.trivia, self.start)?;
46 self.start = trivia.span.start_offset();
47 if self.important_patterns.is_empty()
48 || self.important_patterns.iter().any(|p| memchr::memmem::find(trivia.value, p).is_some())
49 {
50 return Some(trivia);
51 }
52 }
53 }
54}
55
56#[inline]
72#[must_use]
73pub fn get_docblock_for_node<'arena>(
74 program: &'arena Program<'arena>,
75 node: impl HasSpan,
76) -> Option<&'arena Trivia<'arena>> {
77 get_docblock_before_position(program.trivia.as_slice(), node.span().start.offset)
78}
79
80#[must_use]
95pub fn get_docblock_before_position<'arena>(
96 trivias: &'arena [Trivia<'arena>],
97 node_start_offset: u32,
98) -> Option<&'arena Trivia<'arena>> {
99 let candidate_partition_idx = trivias.partition_point(|trivia| trivia.span.start.offset < node_start_offset);
100 if candidate_partition_idx == 0 {
101 return None;
102 }
103
104 let mut covered_from = node_start_offset;
110
111 for i in (0..candidate_partition_idx).rev() {
112 let trivia = &trivias[i];
113 let trivia_end = trivia.span.end_offset();
114
115 if trivia_end != covered_from {
116 return None;
118 }
119
120 match trivia.kind {
121 TriviaKind::DocBlockComment => {
122 return Some(trivia);
124 }
125 TriviaKind::WhiteSpace
126 | TriviaKind::SingleLineComment
127 | TriviaKind::MultiLineComment
128 | TriviaKind::HashComment => {
129 covered_from = trivia.span.start_offset();
130 }
131 }
132 }
133
134 None
136}
137
138#[cfg(test)]
139#[allow(clippy::unwrap_used)]
140mod tests {
141 use bumpalo::Bump;
142 use mago_database::file::FileId;
143 use mago_span::HasSpan;
144
145 use crate::parser::parse_file_content;
146
147 use super::get_docblock_before_position;
148
149 #[test]
150 fn whitespace_between_docblock_and_class_is_trivia() {
151 let arena = Bump::new();
155 let program = parse_file_content(&arena, FileId::zero(), b"<?php\n\n/** @return int */\n\nclass Foo {}");
156 let class_start = program.statements.iter().nth(1).unwrap().span().start.offset;
158 let docblock = get_docblock_before_position(program.trivia.as_slice(), class_start);
159 assert!(docblock.is_some(), "expected docblock to be found across whitespace");
160 assert!(memchr::memmem::find(docblock.unwrap().value, b"@return int").is_some());
161 }
162
163 #[test]
164 fn code_between_docblock_and_function_blocks_attribution() {
165 let arena = Bump::new();
166 let program =
167 parse_file_content(&arena, FileId::zero(), b"<?php\n/** @return int */\necho 1;\nfunction foo() {}");
168 let func_start = program.statements.iter().nth(2).unwrap().span().start.offset;
170 let docblock = get_docblock_before_position(program.trivia.as_slice(), func_start);
171 assert!(docblock.is_none(), "expected no docblock when code intervenes");
172 }
173}