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 str],
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>(self, patterns: &'new_pat [&'new_pat str]) -> PrecedingDocblocks<'arena, 'new_pat> {
33 PrecedingDocblocks { trivia: self.trivia, start: self.start, important_patterns: patterns }
34 }
35}
36
37impl<'arena> Iterator for PrecedingDocblocks<'arena, '_> {
38 type Item = &'arena Trivia<'arena>;
39
40 fn next(&mut self) -> Option<Self::Item> {
41 loop {
42 let trivia = get_docblock_before_position(self.trivia, self.start)?;
43 self.start = trivia.span.start_offset();
44 if self.important_patterns.is_empty() || self.important_patterns.iter().any(|p| trivia.value.contains(*p)) {
45 return Some(trivia);
46 }
47 }
48 }
49}
50
51#[inline]
67#[must_use]
68pub fn get_docblock_for_node<'arena>(
69 program: &'arena Program<'arena>,
70 node: impl HasSpan,
71) -> Option<&'arena Trivia<'arena>> {
72 get_docblock_before_position(program.trivia.as_slice(), node.span().start.offset)
73}
74
75#[must_use]
90pub fn get_docblock_before_position<'arena>(
91 trivias: &'arena [Trivia<'arena>],
92 node_start_offset: u32,
93) -> Option<&'arena Trivia<'arena>> {
94 let candidate_partition_idx = trivias.partition_point(|trivia| trivia.span.start.offset < node_start_offset);
95 if candidate_partition_idx == 0 {
96 return None;
97 }
98
99 let mut covered_from = node_start_offset;
105
106 for i in (0..candidate_partition_idx).rev() {
107 let trivia = &trivias[i];
108 let trivia_end = trivia.span.end_offset();
109
110 if trivia_end != covered_from {
111 return None;
113 }
114
115 match trivia.kind {
116 TriviaKind::DocBlockComment => {
117 return Some(trivia);
119 }
120 TriviaKind::WhiteSpace
121 | TriviaKind::SingleLineComment
122 | TriviaKind::MultiLineComment
123 | TriviaKind::HashComment => {
124 covered_from = trivia.span.start_offset();
125 }
126 }
127 }
128
129 None
131}
132
133#[cfg(test)]
134#[allow(clippy::unwrap_used)]
135mod tests {
136 use bumpalo::Bump;
137 use mago_database::file::FileId;
138 use mago_span::HasSpan;
139
140 use crate::parser::parse_file_content;
141
142 use super::get_docblock_before_position;
143
144 #[test]
145 fn whitespace_between_docblock_and_class_is_trivia() {
146 let arena = Bump::new();
150 let program = parse_file_content(&arena, FileId::zero(), "<?php\n\n/** @return int */\n\nclass Foo {}");
151 let class_start = program.statements.iter().nth(1).unwrap().span().start.offset;
153 let docblock = get_docblock_before_position(program.trivia.as_slice(), class_start);
154 assert!(docblock.is_some(), "expected docblock to be found across whitespace");
155 assert!(docblock.unwrap().value.contains("@return int"));
156 }
157
158 #[test]
159 fn code_between_docblock_and_function_blocks_attribution() {
160 let arena = Bump::new();
161 let program =
162 parse_file_content(&arena, FileId::zero(), "<?php\n/** @return int */\necho 1;\nfunction foo() {}");
163 let func_start = program.statements.iter().nth(2).unwrap().span().start.offset;
165 let docblock = get_docblock_before_position(program.trivia.as_slice(), func_start);
166 assert!(docblock.is_none(), "expected no docblock when code intervenes");
167 }
168}