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