use mago_span::HasSpan;
use crate::ast::Program;
use crate::ast::Trivia;
use crate::ast::TriviaKind;
pub struct PrecedingDocblocks<'arena, 'pat> {
trivia: &'arena [Trivia<'arena>],
start: u32,
important_patterns: &'pat [&'pat str],
}
impl<'arena> PrecedingDocblocks<'arena, 'static> {
pub fn new(trivia: &'arena [Trivia<'arena>], start_offset: u32) -> Self {
Self { trivia, start: start_offset, important_patterns: &[] }
}
}
impl<'arena, 'pat> PrecedingDocblocks<'arena, 'pat> {
pub fn important_only<'new_pat>(self, patterns: &'new_pat [&'new_pat str]) -> PrecedingDocblocks<'arena, 'new_pat> {
PrecedingDocblocks { trivia: self.trivia, start: self.start, important_patterns: patterns }
}
}
impl<'arena, 'pat> Iterator for PrecedingDocblocks<'arena, 'pat> {
type Item = &'arena Trivia<'arena>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let trivia = get_docblock_before_position(self.trivia, self.start)?;
self.start = trivia.span.start_offset();
if self.important_patterns.is_empty() || self.important_patterns.iter().any(|p| trivia.value.contains(*p)) {
return Some(trivia);
}
}
}
}
#[inline]
pub fn get_docblock_for_node<'arena>(
program: &'arena Program<'arena>,
node: impl HasSpan,
) -> Option<&'arena Trivia<'arena>> {
get_docblock_before_position(program.trivia.as_slice(), node.span().start.offset)
}
pub fn get_docblock_before_position<'arena>(
trivias: &'arena [Trivia<'arena>],
node_start_offset: u32,
) -> Option<&'arena Trivia<'arena>> {
let candidate_partition_idx = trivias.partition_point(|trivia| trivia.span.start.offset < node_start_offset);
if candidate_partition_idx == 0 {
return None;
}
let mut covered_from = node_start_offset;
for i in (0..candidate_partition_idx).rev() {
let trivia = &trivias[i];
let trivia_end = trivia.span.end_offset();
if trivia_end != covered_from {
return None;
}
match trivia.kind {
TriviaKind::DocBlockComment => {
return Some(trivia);
}
TriviaKind::WhiteSpace
| TriviaKind::SingleLineComment
| TriviaKind::MultiLineComment
| TriviaKind::HashComment => {
covered_from = trivia.span.start_offset();
}
}
}
None
}
#[cfg(test)]
mod tests {
use bumpalo::Bump;
use mago_database::file::FileId;
use mago_span::HasSpan;
use crate::parser::parse_file_content;
use super::get_docblock_before_position;
#[test]
fn whitespace_between_docblock_and_class_is_trivia() {
let arena = Bump::new();
let program = parse_file_content(&arena, FileId::zero(), "<?php\n\n/** @return int */\n\nclass Foo {}");
let class_start = program.statements.iter().nth(1).unwrap().span().start.offset;
let docblock = get_docblock_before_position(program.trivia.as_slice(), class_start);
assert!(docblock.is_some(), "expected docblock to be found across whitespace");
assert!(docblock.unwrap().value.contains("@return int"));
}
#[test]
fn code_between_docblock_and_function_blocks_attribution() {
let arena = Bump::new();
let program =
parse_file_content(&arena, FileId::zero(), "<?php\n/** @return int */\necho 1;\nfunction foo() {}");
let func_start = program.statements.iter().nth(2).unwrap().span().start.offset;
let docblock = get_docblock_before_position(program.trivia.as_slice(), func_start);
assert!(docblock.is_none(), "expected no docblock when code intervenes");
}
}