use std::iter::Peekable;
use crate::error::Error;
use crate::event::Event;
use crate::pos::Span;
use super::{LoadError, Result};
#[expect(
clippy::inline_always,
reason = "rustc declined #[inline] for this 3-line allocating helper; #[inline(always)] forces inlining at 4 call sites with minimal code-size impact"
)]
#[inline(always)]
pub(super) fn with_hash_prefix(text: &str) -> String {
let mut s = String::with_capacity(text.len() + 1);
s.push('#');
s.push_str(text);
s
}
type EventStream<'a> =
Peekable<Box<dyn Iterator<Item = std::result::Result<(Event<'a>, Span), Error>> + 'a>>;
#[inline]
pub(super) fn next_from<'a>(stream: &mut EventStream<'a>) -> Result<Option<(Event<'a>, Span)>> {
match stream.next() {
None => Ok(None),
Some(Ok(item)) => Ok(Some(item)),
Some(Err(e)) => Err(LoadError::Parse {
pos: e.pos,
message: e.message,
}),
}
}
pub(super) fn consume_leading_doc_comments(
stream: &mut EventStream<'_>,
doc_comments: &mut Vec<String>,
) -> Result<()> {
while matches!(stream.peek(), Some(Ok((Event::Comment { .. }, _)))) {
if let Some((Event::Comment { text }, span)) = next_from(stream)? {
if span.end.line > span.start.line {
doc_comments.push(with_hash_prefix(text));
}
}
}
Ok(())
}
#[expect(
clippy::inline_always,
reason = "#[inline] was declined by rustc for this wrapper (confirmed via LLVM IR); #[inline(always)] forces inlining of the tiny peek+branch fast path at its 2 call sites"
)]
#[inline(always)]
pub(super) fn consume_leading_comments(stream: &mut EventStream<'_>) -> Result<Vec<String>> {
if !matches!(stream.peek(), Some(Ok((Event::Comment { .. }, _)))) {
return Ok(Vec::new());
}
consume_leading_comments_slow(stream)
}
fn consume_leading_comments_slow(stream: &mut EventStream<'_>) -> Result<Vec<String>> {
let mut leading = Vec::new();
while matches!(stream.peek(), Some(Ok((Event::Comment { .. }, _)))) {
if let Some((Event::Comment { text }, _)) = next_from(stream)? {
leading.push(with_hash_prefix(text));
}
}
Ok(leading)
}
pub(super) fn peek_trailing_comment(
stream: &mut EventStream<'_>,
preceding_end_line: usize,
) -> Result<Option<String>> {
if matches!(
stream.peek(),
Some(Ok((Event::Comment { .. }, span))) if span.start.line == preceding_end_line
) {
if let Some((Event::Comment { text }, _)) = next_from(stream)? {
return Ok(Some(with_hash_prefix(text)));
}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Error;
use crate::event::{Event, ScalarStyle};
use crate::pos::{Pos, Span};
fn make_stream<'a>(
items: Vec<std::result::Result<(Event<'a>, Span), Error>>,
) -> EventStream<'a> {
let boxed: Box<dyn Iterator<Item = _> + 'a> = Box::new(items.into_iter());
boxed.peekable()
}
fn span(start_line: usize, end_line: usize) -> Span {
Span {
start: Pos {
byte_offset: 0,
line: start_line,
column: 0,
},
end: Pos {
byte_offset: 0,
line: end_line,
column: 0,
},
}
}
fn pos(line: usize) -> Pos {
Pos {
byte_offset: 0,
line,
column: 0,
}
}
#[test]
fn next_from_on_empty_stream_returns_ok_none() {
let mut stream = make_stream(vec![]);
assert_eq!(next_from(&mut stream), Ok(None));
}
#[test]
fn next_from_forwards_ok_event() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::StreamStart, sp))]);
let result = next_from(&mut stream);
assert_eq!(result, Ok(Some((Event::StreamStart, sp))));
}
#[test]
fn next_from_propagates_parse_error_as_load_error() {
let p = pos(3);
let err = Error {
pos: p,
message: "unexpected token".to_string(),
};
let mut stream = make_stream(vec![Err(err)]);
let result = next_from(&mut stream);
assert_eq!(
result,
Err(LoadError::Parse {
pos: p,
message: "unexpected token".to_string(),
})
);
}
#[test]
fn consume_leading_doc_comments_empty_when_first_event_is_not_comment() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::StreamStart, sp))]);
let mut doc_comments: Vec<String> = Vec::new();
let result = consume_leading_doc_comments(&mut stream, &mut doc_comments);
assert_eq!(result, Ok(()));
assert!(doc_comments.is_empty());
assert!(stream.peek().is_some());
}
#[test]
fn consume_leading_doc_comments_accumulates_block_comments() {
let sp = span(1, 2);
let trailing_sp = span(3, 3);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " note" }, sp)),
Ok((Event::Comment { text: " note" }, sp)),
Ok((Event::StreamStart, trailing_sp)),
]);
let mut doc_comments: Vec<String> = Vec::new();
assert_eq!(
consume_leading_doc_comments(&mut stream, &mut doc_comments),
Ok(())
);
assert_eq!(doc_comments, vec!["# note", "# note"]);
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_doc_comments_skips_single_line_comment() {
let inline_sp = span(1, 1);
let trailing_sp = span(2, 2);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " inline" }, inline_sp)),
Ok((Event::StreamStart, trailing_sp)),
]);
let mut doc_comments: Vec<String> = Vec::new();
assert_eq!(
consume_leading_doc_comments(&mut stream, &mut doc_comments),
Ok(())
);
assert!(doc_comments.is_empty());
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_comments_returns_empty_vec_on_empty_stream() {
let mut stream = make_stream(vec![]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec![]));
assert!(stream.peek().is_none());
}
#[test]
fn consume_leading_comments_returns_empty_vec_when_next_is_stream_start() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::StreamStart, sp))]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec![]));
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_comments_returns_empty_vec_when_next_is_scalar() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((
Event::Scalar {
value: "x".into(),
style: ScalarStyle::Plain,
anchor: None,
tag: None,
},
sp,
))]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec![]));
assert!(stream.peek().is_some());
}
#[test]
fn consume_leading_comments_returns_empty_vec_when_next_is_error_event() {
let p = pos(1);
let err = Error {
pos: p,
message: "bad".to_string(),
};
let mut stream = make_stream(vec![Err(err)]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec![]));
assert!(stream.peek().is_some());
}
#[test]
fn consume_leading_comments_returns_empty_vec_when_first_event_is_not_comment() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::StreamStart, sp))]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec![]));
assert!(stream.peek().is_some());
}
#[test]
fn consume_leading_comments_single_comment_delegated_to_slow_path() {
let sp = span(1, 1);
let trailing_sp = span(2, 2);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " hi" }, sp)),
Ok((Event::StreamStart, trailing_sp)),
]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec!["# hi".to_string()]));
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_comments_accumulates_all_comment_events() {
let sp = span(1, 1);
let trailing_sp = span(2, 2);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " a" }, sp)),
Ok((Event::Comment { text: " b" }, sp)),
Ok((Event::StreamStart, trailing_sp)),
]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec!["# a".to_string(), "# b".to_string()]));
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_comments_multiple_consecutive_comments_all_consumed() {
let sp = span(1, 1);
let trailing_sp = span(4, 4);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " a" }, sp)),
Ok((Event::Comment { text: " b" }, sp)),
Ok((Event::Comment { text: " c" }, sp)),
Ok((Event::StreamStart, trailing_sp)),
]);
let result = consume_leading_comments(&mut stream);
assert_eq!(
result,
Ok(vec![
"# a".to_string(),
"# b".to_string(),
"# c".to_string()
])
);
assert!(matches!(stream.peek(), Some(Ok((Event::StreamStart, _)))));
}
#[test]
fn consume_leading_comments_drains_all_consecutive_comments() {
let sp = span(1, 1);
let mut stream = make_stream(vec![
Ok((Event::Comment { text: " x" }, sp)),
Ok((Event::Comment { text: " y" }, sp)),
Ok((Event::Comment { text: " z" }, sp)),
]);
let result = consume_leading_comments(&mut stream);
assert_eq!(
result,
Ok(vec![
"# x".to_string(),
"# y".to_string(),
"# z".to_string()
])
);
assert!(stream.peek().is_none());
}
#[test]
fn consume_leading_comments_stops_after_last_comment_leaves_non_comment_unconsumed() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::Comment { text: " only" }, sp))]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec!["# only".to_string()]));
assert!(stream.peek().is_none());
}
#[test]
fn consume_leading_comments_stops_before_error_event_in_stream() {
let sp = span(1, 1);
let p = pos(2);
let err = Error {
pos: p,
message: "oops".to_string(),
};
let mut stream = make_stream(vec![Ok((Event::Comment { text: " ok" }, sp)), Err(err)]);
let result = consume_leading_comments(&mut stream);
assert_eq!(result, Ok(vec!["# ok".to_string()]));
assert!(stream.peek().is_some());
}
#[test]
fn peek_trailing_comment_returns_none_when_next_is_not_comment() {
let sp = span(1, 1);
let mut stream = make_stream(vec![Ok((Event::StreamStart, sp))]);
let result = peek_trailing_comment(&mut stream, 1);
assert_eq!(result, Ok(None));
assert!(stream.peek().is_some());
}
#[test]
fn peek_trailing_comment_returns_none_when_stream_empty() {
let mut stream = make_stream(vec![]);
let result = peek_trailing_comment(&mut stream, 1);
assert_eq!(result, Ok(None));
}
#[test]
fn peek_trailing_comment_returns_some_when_comment_on_same_line() {
let sp = Span {
start: Pos {
byte_offset: 10,
line: 5,
column: 20,
},
end: Pos {
byte_offset: 15,
line: 5,
column: 25,
},
};
let mut stream = make_stream(vec![Ok((Event::Comment { text: " text" }, sp))]);
let result = peek_trailing_comment(&mut stream, 5);
assert_eq!(result, Ok(Some("# text".to_string())));
assert!(stream.peek().is_none());
}
#[test]
fn peek_trailing_comment_returns_none_when_comment_on_later_line() {
let sp = Span {
start: Pos {
byte_offset: 0,
line: 7,
column: 0,
},
end: Pos {
byte_offset: 5,
line: 7,
column: 5,
},
};
let mut stream = make_stream(vec![Ok((Event::Comment { text: " later" }, sp))]);
let result = peek_trailing_comment(&mut stream, 5);
assert_eq!(result, Ok(None));
assert!(stream.peek().is_some());
}
#[test]
fn with_hash_prefix_prepends_hash_to_empty_str() {
assert_eq!(with_hash_prefix(""), "#");
}
#[test]
fn with_hash_prefix_prepends_hash_to_nonempty_text() {
assert_eq!(with_hash_prefix(" comment text"), "# comment text");
}
#[test]
fn with_hash_prefix_preserves_inner_hashes() {
assert_eq!(with_hash_prefix(" foo # bar"), "# foo # bar");
}
}