🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

granit-parser

Package Overview
Dependencies
Maintainers
0
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

granit-parser - cargo Package Compare versions

Comparing version
0.0.6
to
0.0.7
+84
tests/parser_input_regressions.rs
use granit_parser::{BufferedInput, Input, Parser, StrInput};
#[test]
fn parser_iterator_terminates_after_scan_error() {
let parser = Parser::new_from_str("foo:\n bar\ninvalid\n");
let mut errors = 0usize;
let mut events = 0usize;
for item in parser {
events += 1;
if item.is_err() {
errors += 1;
}
assert!(
events < 1000,
"Parser iterator did not terminate after a scan error"
);
}
assert_eq!(errors, 1, "error should be yielded exactly once");
}
#[test]
fn buffered_skip_n_matches_str_input_and_saturates_at_eof() {
let mut buffered = BufferedInput::new("abc".chars());
buffered.lookahead(1);
buffered.skip_n(2);
buffered.lookahead(1);
let mut str_input = StrInput::new("abc");
str_input.lookahead(1);
str_input.skip_n(2);
str_input.lookahead(1);
assert_eq!(buffered.peek(), str_input.peek());
buffered.skip_n(8);
str_input.skip_n(8);
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn buffered_skip_without_lookahead_matches_str_input() {
let mut buffered = BufferedInput::new("ab".chars());
buffered.skip();
buffered.lookahead(1);
let mut str_input = StrInput::new("ab");
str_input.skip();
str_input.lookahead(1);
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn buffered_raw_reads_use_logical_stream_front() {
let mut buffered = BufferedInput::new("ab".chars());
buffered.lookahead(1);
let mut str_input = StrInput::new("ab");
str_input.lookahead(1);
assert_eq!(buffered.raw_read_ch(), str_input.raw_read_ch());
buffered.lookahead(1);
str_input.lookahead(1);
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn buffered_buflen_matches_str_input_lookahead_window() {
let mut buffered = BufferedInput::new("ab".chars());
buffered.lookahead(2);
buffered.skip();
buffered.skip();
let mut str_input = StrInput::new("ab");
str_input.lookahead(2);
str_input.skip();
str_input.skip();
assert_eq!(buffered.buflen(), str_input.buflen());
assert_eq!(buffered.buf_is_empty(), str_input.buf_is_empty());
assert_eq!(buffered.peek(), str_input.peek());
}
//! Coverage tests for rarely-exercised parser and input paths.
//!
//! These tests target scattered error paths and default trait implementations that the
//! regular test suite does not reach.
use granit_parser::{
input::SkipTabs, BufferedInput, Event, Input, Parser, Placement, ScanError, StrInput,
TryEventReceiver, TryLoadError,
};
fn parse_events(input: &str) -> Result<Vec<Event<'_>>, ScanError> {
Parser::new_from_str(input)
.map(|event| event.map(|(event, _)| event))
.collect()
}
fn first_error_info(input: &str) -> String {
for event in Parser::new_from_str(input) {
if let Err(error) = event {
return error.info().to_owned();
}
}
panic!("expected parser error");
}
/// A receiver that rejects the terminal `StreamEnd` event.
struct RejectStreamEnd;
impl<'input> TryEventReceiver<'input> for RejectStreamEnd {
type Error = &'static str;
fn on_event(&mut self, ev: Event<'input>) -> Result<(), Self::Error> {
if matches!(ev, Event::StreamEnd) {
Err("stream end rejected")
} else {
Ok(())
}
}
}
/// A receiver that accepts everything.
struct AcceptAll;
impl<'input> TryEventReceiver<'input> for AcceptAll {
type Error = &'static str;
fn on_event(&mut self, _ev: Event<'input>) -> Result<(), Self::Error> {
Ok(())
}
}
// --- input.rs: default `Input::skip_ws_to_eol` (only reachable through `BufferedInput`,
// --- since `StrInput` overrides it and the scanner uses `skip_ws_to_eol_blanks` whenever
// --- comments are possible).
#[test]
fn buffered_default_skip_ws_to_eol_consumes_blanks_and_comment() {
let mut input = BufferedInput::new(" \t # note\nx".chars());
let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
// 2 spaces + 1 tab + 1 space + '#' + " note" (5 chars) = 10 characters.
assert_eq!(consumed, 10);
let skipped = result.expect("whitespace with a comment must be accepted");
assert!(skipped.found_tabs());
assert!(skipped.has_valid_yaml_ws());
// The line break must not be consumed.
assert_eq!(input.look_ch(), '\n');
}
#[test]
fn buffered_default_skip_ws_to_eol_rejects_comment_without_whitespace() {
let mut input = BufferedInput::new("#no-space".chars());
let (consumed, result) = input.skip_ws_to_eol(SkipTabs::Yes);
assert_eq!(consumed, 0);
let Err(message) = result else {
panic!("expected an error for a comment without leading whitespace");
};
assert_eq!(
message,
"comments must be separated from other tokens by whitespace"
);
// The '#' itself must not be consumed.
assert_eq!(input.look_ch(), '#');
}
#[test]
fn buffered_default_skip_ws_to_eol_stops_at_tab_when_tabs_disallowed() {
let mut input = BufferedInput::new("\tx".chars());
let (consumed, result) = input.skip_ws_to_eol(SkipTabs::No);
assert_eq!(consumed, 0);
let skipped = result.expect("stopping at a tab is not an error");
assert!(!skipped.found_tabs());
assert!(!skipped.has_valid_yaml_ws());
assert_eq!(input.look_ch(), '\t');
}
// --- input/buffered.rs
#[test]
fn buffered_raw_read_ch_pads_after_source_is_exhausted() {
let mut input = BufferedInput::new("a".chars());
assert_eq!(input.raw_read_ch(), 'a');
// First EOF read marks the source as exhausted...
assert_eq!(input.raw_read_ch(), '\0');
// ...and subsequent reads keep returning the EOF padding character.
assert_eq!(input.raw_read_ch(), '\0');
}
#[test]
fn buffered_raw_read_non_breakz_ch_uses_buffered_front() {
let mut input = BufferedInput::new("a\nb".chars());
// Fill the buffer so `raw_read_non_breakz_ch` takes the buffered-front path.
input.lookahead(2);
assert_eq!(input.raw_read_non_breakz_ch(), Some('a'));
// The buffered front is now the line break: it must be left in place.
assert_eq!(input.raw_read_non_breakz_ch(), None);
assert_eq!(input.peek(), '\n');
}
// --- input/str.rs: `skip_ws_to_eol_blanks` with `SkipTabs::No`
#[test]
fn str_input_skip_ws_to_eol_blanks_stops_before_tab_when_tabs_disallowed() {
let mut input = StrInput::new(" \tfoo");
let (consumed, skipped) = input.skip_ws_to_eol_blanks(SkipTabs::No);
assert_eq!(consumed, 2);
assert!(!skipped.found_tabs());
assert!(skipped.has_valid_yaml_ws());
assert_eq!(input.look_ch(), '\t');
}
// --- parser.rs: `parse_node` error reporting per state
#[test]
fn stray_flow_entry_in_block_sequence_reports_block_sequence_error() {
// `,` is tokenized as a flow entry even outside a flow collection; `parse_node`
// then rejects it while in `BlockNode` state.
assert_eq!(
first_error_info("- ,\n"),
"unexpected EOF while parsing a block sequence"
);
}
#[test]
fn stray_flow_entry_in_block_mapping_value_reports_block_mapping_error() {
assert_eq!(
first_error_info("a: ,\n"),
"unexpected EOF while parsing a block mapping"
);
}
// --- parser.rs: comment right after `:` in a flow-sequence explicit key/value pair
// --- (`FlowSequenceEntryMappingValue` state)
#[test]
fn comment_after_value_in_flow_sequence_explicit_pair_is_emitted() {
let events = parse_events("[? a : # note\n b]\n").unwrap();
let comment_pos = events
.iter()
.position(|event| matches!(event, Event::Comment(text, _) if text == " note"))
.expect("expected the inline comment event");
// The explicit `?` key inside a flow sequence opens a single-pair mapping.
assert!(matches!(
events[comment_pos - 2],
Event::MappingStart(granit_parser::StructureStyle::Flow, 0, None)
));
// The comment is emitted between the key and the value of the explicit pair.
assert!(matches!(
events[comment_pos - 1],
Event::Scalar(ref value, ..) if value == "a"
));
assert!(matches!(
events[comment_pos + 1],
Event::Scalar(ref value, ..) if value == "b"
));
assert!(matches!(
events[comment_pos],
Event::Comment(_, Placement::Right)
));
}
// --- parser.rs: `try_load` returning an error buffered by `peek`
#[test]
fn try_load_returns_error_buffered_by_peek() {
let mut parser = Parser::new_from_str("a: *missing\n");
// Drive the parser through `peek` until the unknown-alias error is buffered.
let buffered_error = loop {
match parser.peek() {
Some(Ok(_)) => {
parser.next_event().unwrap().unwrap();
}
Some(Err(error)) => break error,
None => panic!("expected an unknown alias error"),
}
};
assert_eq!(
buffered_error.info(),
"while parsing node, found unknown anchor"
);
let mut receiver = AcceptAll;
let err = parser.try_load(&mut receiver, true).unwrap_err();
assert_eq!(err, TryLoadError::Scan(buffered_error));
// The buffered error is consumed; the parser is exhausted afterwards.
assert!(parser.next_event().is_none());
}
// --- parser.rs: resuming after a receiver error on `StreamEnd`
#[test]
fn next_event_after_receiver_error_on_stream_end_returns_stream_end() {
let mut parser = Parser::new_from_str("foo\n");
let mut receiver = RejectStreamEnd;
let err = parser.try_load(&mut receiver, true).unwrap_err();
assert_eq!(err, TryLoadError::Receiver("stream end rejected"));
// The parser already reached its terminal state; asking for another event
// re-emits `StreamEnd` instead of scanning further.
let (event, _) = parser.next_event().expect("stream end event").unwrap();
assert_eq!(event, Event::StreamEnd);
assert!(parser.next_event().is_none());
}
#[test]
fn next_event_after_receiver_error_on_stream_end_with_comments_reports_eof() {
// With comments possible, resuming after the scanner is exhausted asks the
// scanner for another token and reports an EOF error instead.
let mut parser = Parser::new_from_str("# hello\nfoo\n");
let mut receiver = RejectStreamEnd;
let err = parser.try_load(&mut receiver, true).unwrap_err();
assert_eq!(err, TryLoadError::Receiver("stream end rejected"));
let error = parser
.next_event()
.expect("expected an EOF error")
.unwrap_err();
assert_eq!(error.info(), "unexpected eof");
assert!(parser.next_event().is_none());
}
use granit_parser::{Event, Parser, ScalarStyle, ScanError, Span, StructureStyle};
use std::{fs, path::Path};
fn collect_ok_events(yaml: &str) -> Vec<(Event<'_>, Span)> {
Parser::new_from_str(yaml)
.map(|result| result.expect("regression input should parse"))
.collect()
}
fn first_error(input: &str) -> ScanError {
Parser::new_from_str(input)
.find_map(Result::err)
.expect("regression input should produce an error")
}
fn scalar_values(input: &str) -> Vec<String> {
collect_ok_events(input)
.into_iter()
.filter_map(|(event, _)| match event {
Event::Scalar(value, ..) => Some(value.into_owned()),
_ => None,
})
.collect()
}
fn scalar_values_with_style(input: &str, style: ScalarStyle) -> Vec<String> {
collect_ok_events(input)
.into_iter()
.filter_map(|(event, _)| match event {
Event::Scalar(value, scalar_style, ..) if scalar_style == style => {
Some(value.into_owned())
}
_ => None,
})
.collect()
}
#[test]
fn alias_anchor_edge_cases() {
assert_eq!(
first_error("a: *nope\n").info(),
"while parsing node, found unknown anchor"
);
assert_eq!(
first_error("--- &x 1\n--- *x\n").info(),
"while parsing node, found unknown anchor"
);
let self_reference = collect_ok_events("&x [*x]\n");
assert!(self_reference
.iter()
.any(|(event, _)| matches!(event, Event::SequenceStart(StructureStyle::Flow, 1, None))));
assert!(self_reference
.iter()
.any(|(event, _)| matches!(event, Event::Alias(1))));
let redefinition = collect_ok_events("[&x 1, &x 2, *x]\n");
assert!(redefinition.iter().any(|(event, _)| matches!(
event,
Event::Scalar(value, _, 1, _) if value.as_ref() == "1"
)));
assert!(redefinition.iter().any(|(event, _)| matches!(
event,
Event::Scalar(value, _, 2, _) if value.as_ref() == "2"
)));
assert!(redefinition
.iter()
.any(|(event, _)| matches!(event, Event::Alias(2))));
}
#[test]
fn crlf_and_wide_character_spans() {
assert_eq!(scalar_values("a: 1\r\nb: 2\r\n"), ["a", "1", "b", "2"]);
assert_eq!(scalar_values("a: 1\rb: 2\r"), ["a", "1", "b", "2"]);
assert_eq!(
scalar_values_with_style("k: |\r\n line1\r\n line2\r\n", ScalarStyle::Literal),
["line1\nline2\n"]
);
assert_eq!(
scalar_values_with_style("k: \"a\r\n b\"\r\n", ScalarStyle::DoubleQuoted),
["a b"]
);
let yaml = "\u{5B57}: \u{503C}\n# \u{2605}\u{6CE8}\nb: 2\n";
let interesting: Vec<_> = collect_ok_events(yaml)
.into_iter()
.filter_map(|(event, span)| match event {
Event::Scalar(value, ..) => Some((
value.into_owned(),
span.byte_range(),
span.slice(yaml).map(ToOwned::to_owned),
)),
Event::Comment(text, _) => Some((
format!("#{text}"),
span.byte_range(),
span.slice(yaml).map(ToOwned::to_owned),
)),
_ => None,
})
.collect();
assert_eq!(
interesting,
vec![
(
"\u{5B57}".to_string(),
Some(0..3),
Some("\u{5B57}".to_string())
),
(
"\u{503C}".to_string(),
Some(5..8),
Some("\u{503C}".to_string())
),
(
"# \u{2605}\u{6CE8}".to_string(),
Some(9..17),
Some("# \u{2605}\u{6CE8}".to_string())
),
("b".to_string(), Some(18..19), Some("b".to_string())),
("2".to_string(), Some(21..22), Some("2".to_string())),
]
);
}
#[test]
fn nel_and_double_bom_probes() {
assert_eq!(scalar_values("a\u{85}b\n"), ["a\u{85}b"]);
assert_eq!(scalar_values("\u{FEFF}\u{FEFF}a: b\n"), ["a", "b"]);
}
#[test]
fn yaml_suite_str_spans_have_valid_byte_offsets() {
if cfg!(miri) {
return;
}
let suite_dir = Path::new("tests/yaml-test-suite/src");
if !suite_dir.is_dir() {
return;
}
let mut files = fs::read_dir(suite_dir)
.expect("yaml-test-suite directory should be readable")
.map(|entry| {
entry
.expect("yaml-test-suite entry should be readable")
.path()
})
.collect::<Vec<_>>();
files.sort();
let mut checked = 0usize;
let mut failures = Vec::new();
for path in files {
let content = fs::read_to_string(&path).unwrap_or_default();
for yaml in extract_yaml_inputs(&content) {
for result in Parser::new_from_str(&yaml).take(100_000) {
match result {
Ok((event, span)) => {
checked += 1;
check_span(&path, &yaml, &event, span, &mut failures);
}
Err(_) => break,
}
}
}
}
assert!(
checked > 3000,
"span regression checked too few events: {checked}"
);
assert!(
failures.is_empty(),
"span regression found invalid spans:\n{}",
failures.join("\n")
);
}
fn check_span(path: &Path, yaml: &str, event: &Event<'_>, span: Span, failures: &mut Vec<String>) {
if span.end.index() < span.start.index() {
push_failure(
failures,
format!(
"{}: {event:?} span end before start: {span:?}",
path.display()
),
);
}
for (which, marker) in [("start", span.start), ("end", span.end)] {
let Some(byte) = marker.byte_offset() else {
push_failure(
failures,
format!(
"{}: {event:?} {which} marker has no byte offset",
path.display()
),
);
continue;
};
if byte > yaml.len() || !yaml.is_char_boundary(byte) {
push_failure(
failures,
format!(
"{}: {event:?} {which} byte offset {byte} is not a char boundary",
path.display()
),
);
continue;
}
let chars_before_byte = yaml[..byte].chars().count();
if chars_before_byte != marker.index() {
push_failure(
failures,
format!(
"{}: {event:?} {which} byte offset {byte} maps to char index \
{chars_before_byte}, marker says {}",
path.display(),
marker.index()
),
);
}
}
}
fn push_failure(failures: &mut Vec<String>, failure: String) {
if failures.len() < 20 {
failures.push(failure);
}
}
fn extract_yaml_inputs(file: &str) -> Vec<String> {
let mut out = Vec::new();
let lines: Vec<&str> = file.lines().collect();
let mut i = 0;
while i < lines.len() {
let line = lines[i];
let trimmed = line.trim_start();
if trimmed.starts_with("yaml:") && trimmed.trim_end().ends_with('|') {
let base_indent = line.len() - trimmed.len();
let mut block = String::new();
i += 1;
while i < lines.len() {
let block_line = lines[i];
if block_line.trim().is_empty() {
block.push('\n');
i += 1;
continue;
}
let indent = block_line.len() - block_line.trim_start().len();
if indent <= base_indent {
break;
}
block.push_str(&block_line[(base_indent + 2).min(block_line.len())..]);
block.push('\n');
i += 1;
}
out.push(decode_yaml_suite_markers(&block));
} else {
i += 1;
}
}
out
}
fn decode_yaml_suite_markers(block: &str) -> String {
block
.replace("<SPC>", " ")
.replace("<TAB>", "\t")
.replace("\u{2014}\u{2014}\u{2014}\u{2014}\u{BB}", "\t")
.replace("\u{2014}\u{2014}\u{2014}\u{BB}", "\t")
.replace("\u{2014}\u{2014}\u{BB}", "\t")
.replace("\u{2014}\u{BB}", "\t")
.replace('\u{BB}', "\t")
.replace('\u{220E}', "")
.replace('\u{21D4}', "\u{FEFF}")
.replace('\u{21B5}', "")
}
//! Coverage tests for block scalars, quoted scalars, plain scalars and error paths in
//! `src/scanner.rs`.
//!
//! Some tests use custom [`Input`] implementations (a legitimate use of the public `Input` /
//! `BorrowedInput` traits) to exercise code paths that `StrInput` and `BufferedInput` cannot
//! reach:
//!
//! - [`WindowedInput`] models a streaming input whose lookahead window drains as characters are
//! consumed, which forces the scanner to fall back to `raw_read_non_breakz_ch` when reading
//! block scalar content lines.
//! - [`SliceableStreamInput`] models an input with stable byte offsets (`byte_offset` /
//! `slice_bytes`) but without zero-copy borrowing (`slice_borrowed` returns `None`), which
//! forces the owned-copy fallbacks when finalizing quoted scalars.
use granit_parser::{
input::{is_breakz, BorrowedInput, Input},
Event, Parser, ScalarStyle, ScanError, StructureStyle,
};
fn parse_events(input: &str) -> Result<Vec<Event<'_>>, ScanError> {
Parser::new_from_str(input)
.map(|event| event.map(|(event, _)| event))
.collect()
}
fn first_error_info(input: &str) -> String {
for event in Parser::new_from_str(input) {
if let Err(error) = event {
return error.info().to_owned();
}
}
panic!("expected parser error");
}
fn scalars_of(events: &[Event<'_>]) -> Vec<(String, ScalarStyle)> {
events
.iter()
.filter_map(|event| {
if let Event::Scalar(value, style, ..) = event {
Some((value.to_string(), *style))
} else {
None
}
})
.collect()
}
// -------------------------------------------------------------------------------------------
// Custom inputs
// -------------------------------------------------------------------------------------------
/// Maximum lookahead window of [`WindowedInput`].
const WINDOW: usize = 8;
/// A streaming input whose lookahead window shrinks as characters are consumed.
///
/// Unlike `BufferedInput` (which keeps its buffer topped up after every consumption), this input
/// only promises the characters requested by the latest `lookahead` call. Consuming characters
/// drains the window, so `buf_is_empty` eventually becomes `true` mid-line.
struct WindowedInput {
chars: Vec<char>,
pos: usize,
window: usize,
}
impl WindowedInput {
fn new(source: &str) -> Self {
Self {
chars: source.chars().collect(),
pos: 0,
window: 0,
}
}
fn consume_one(&mut self) {
if self.pos < self.chars.len() {
self.pos += 1;
}
self.window = self.window.saturating_sub(1);
}
}
impl Input for WindowedInput {
fn lookahead(&mut self, count: usize) {
self.window = self.window.max(count.min(WINDOW));
}
fn buflen(&self) -> usize {
self.window
}
fn bufmaxlen(&self) -> usize {
WINDOW
}
fn raw_read_ch(&mut self) -> char {
let c = self.chars.get(self.pos).copied().unwrap_or('\0');
self.consume_one();
c
}
fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
let c = self.chars.get(self.pos).copied()?;
if is_breakz(c) {
None
} else {
self.consume_one();
Some(c)
}
}
fn skip(&mut self) {
self.consume_one();
}
fn skip_n(&mut self, count: usize) {
for _ in 0..count {
self.consume_one();
}
}
fn peek(&self) -> char {
self.chars.get(self.pos).copied().unwrap_or('\0')
}
fn peek_nth(&self, n: usize) -> char {
self.chars.get(self.pos + n).copied().unwrap_or('\0')
}
}
impl BorrowedInput<'static> for WindowedInput {
fn slice_borrowed(&self, _start: usize, _end: usize) -> Option<&'static str> {
None
}
}
/// An input with stable byte offsets and (optionally) `slice_bytes`, but no zero-copy borrowing.
///
/// This models an input that owns its backing storage: it can hand out `&str` slices tied to
/// `&self` (`slice_bytes`) but not slices with the `'input` lifetime (`slice_borrowed`).
struct SliceableStreamInput {
source: String,
/// Current byte offset into `source`.
pos: usize,
/// Sticky lookahead window, mirroring `StrInput`.
window: usize,
/// Whether `slice_bytes` is offered.
provide_slices: bool,
}
impl SliceableStreamInput {
fn new(source: &str, provide_slices: bool) -> Self {
Self {
source: source.to_owned(),
pos: 0,
window: 0,
provide_slices,
}
}
fn rest(&self) -> &str {
&self.source[self.pos..]
}
}
impl Input for SliceableStreamInput {
fn lookahead(&mut self, count: usize) {
self.window = self.window.max(count);
}
fn buflen(&self) -> usize {
self.window
}
fn bufmaxlen(&self) -> usize {
128
}
fn raw_read_ch(&mut self) -> char {
match self.rest().chars().next() {
Some(c) => {
self.pos += c.len_utf8();
c
}
None => '\0',
}
}
fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
let c = self.rest().chars().next()?;
if is_breakz(c) {
None
} else {
self.pos += c.len_utf8();
Some(c)
}
}
fn skip(&mut self) {
if let Some(c) = self.rest().chars().next() {
self.pos += c.len_utf8();
}
}
fn skip_n(&mut self, count: usize) {
for _ in 0..count {
self.skip();
}
}
fn peek(&self) -> char {
self.rest().chars().next().unwrap_or('\0')
}
fn peek_nth(&self, n: usize) -> char {
self.rest().chars().nth(n).unwrap_or('\0')
}
fn byte_offset(&self) -> Option<usize> {
Some(self.pos)
}
fn slice_bytes(&self, start: usize, end: usize) -> Option<&str> {
if self.provide_slices {
self.source.get(start..end)
} else {
None
}
}
}
impl BorrowedInput<'static> for SliceableStreamInput {
fn slice_borrowed(&self, _start: usize, _end: usize) -> Option<&'static str> {
None
}
}
// -------------------------------------------------------------------------------------------
// Block scalars
// -------------------------------------------------------------------------------------------
/// A block scalar content line that outlives the input's lookahead window must be completed
/// through `raw_read_non_breakz_ch` (scanner.rs `scan_block_scalar_content_line`, raw-read
/// fallback).
#[test]
fn block_scalar_line_longer_than_lookahead_window_is_read_raw() {
let events: Result<Vec<Event<'static>>, ScanError> = Parser::new(WindowedInput::new(
"|\n abcdefghijklmnopqrstuvwxyz 0123456789\n",
))
.map(|event| event.map(|(event, _)| event))
.collect();
let events = events.expect("valid literal scalar must parse");
assert_eq!(
scalars_of(&events),
vec![(
"abcdefghijklmnopqrstuvwxyz 0123456789\n".to_owned(),
ScalarStyle::Literal
)]
);
}
/// A folded scalar with two long lines exercises the raw-read fallback together with line
/// folding.
#[test]
fn folded_scalar_long_lines_with_windowed_input_fold_to_spaces() {
let events: Result<Vec<Event<'static>>, ScanError> = Parser::new(WindowedInput::new(
">\n the quick brown fox jumps over\n the lazy dog and runs away\n",
))
.map(|event| event.map(|(event, _)| event))
.collect();
let events = events.expect("valid folded scalar must parse");
assert_eq!(
scalars_of(&events),
vec![(
"the quick brown fox jumps over the lazy dog and runs away\n".to_owned(),
ScalarStyle::Folded
)]
);
}
/// A block scalar indented deeper than the input's buffer size must skip indentation through the
/// chunked loop (scanner.rs `skip_block_scalar_indent`, `indent >= bufmaxlen - 2` branch),
/// including its early exit when a line holds content before the indent level is reached.
#[test]
fn deeply_indented_block_scalar_with_buffered_input() {
let indent = " ".repeat(16);
let yaml = format!("k:\n{indent}|\n{indent}aaaa\n\n{indent}bbbb\n");
let events: Result<Vec<Event<'static>>, ScanError> =
Parser::new_from_iter(yaml.chars().collect::<Vec<char>>().into_iter())
.map(|event| event.map(|(event, _)| event))
.collect();
let events = events.expect("valid literal scalar must parse");
assert_eq!(
scalars_of(&events),
vec![
("k".to_owned(), ScalarStyle::Plain),
("aaaa\n\nbbbb\n".to_owned(), ScalarStyle::Literal),
]
);
}
/// A block scalar whose indentation is deeper than the input's lookahead buffer must re-request
/// lookahead in the middle of skipping a single line's indentation (scanner.rs
/// `skip_block_scalar_indent`, inner loop repeating because the buffer drained before the indent
/// level was reached).
#[test]
fn deeply_indented_block_scalar_with_windowed_input() {
let indent = " ".repeat(10);
let yaml = format!("k:\n{indent}|\n{indent}aaaa\n\n{indent}bbbb\n");
let events: Result<Vec<Event<'static>>, ScanError> = Parser::new(WindowedInput::new(&yaml))
.map(|event| event.map(|(event, _)| event))
.collect();
let events = events.expect("valid literal scalar must parse");
assert_eq!(
scalars_of(&events),
vec![
("k".to_owned(), ScalarStyle::Plain),
("aaaa\n\nbbbb\n".to_owned(), ScalarStyle::Literal),
]
);
}
// -------------------------------------------------------------------------------------------
// Quoted scalars
// -------------------------------------------------------------------------------------------
/// Trailing blanks before a line break inside a double-quoted scalar are discarded; with
/// `StrInput` this forces the zero-copy buffer to be promoted to an owned string
/// (scanner.rs `scan_flow_scalar`, pending-whitespace discard branch).
#[test]
fn double_quoted_trailing_blank_before_break_folds_to_space() {
let events = parse_events("\"a \nb\"\n").unwrap();
assert_eq!(
scalars_of(&events),
vec![("a b".to_owned(), ScalarStyle::DoubleQuoted)]
);
}
/// Same as above for single-quoted scalars, with several blanks before the break.
#[test]
fn single_quoted_trailing_blanks_before_break_fold_to_space() {
let events = parse_events("'a \n b'\n").unwrap();
assert_eq!(
scalars_of(&events),
vec![("a b".to_owned(), ScalarStyle::SingleQuoted)]
);
}
/// An input that advertises byte offsets but no zero-copy borrowing makes the scanner fall back
/// to copying the quoted scalar out of `slice_bytes` (scanner.rs `scan_flow_scalar`, borrowed
/// contents fallback).
#[test]
fn quoted_scalar_from_offsets_without_borrowing_is_copied() {
let events: Result<Vec<Event<'static>>, ScanError> =
Parser::new(SliceableStreamInput::new("\"hello world\"\n", true))
.map(|event| event.map(|(event, _)| event))
.collect();
let events = events.expect("valid double quoted scalar must parse");
assert_eq!(
scalars_of(&events),
vec![("hello world".to_owned(), ScalarStyle::DoubleQuoted)]
);
}
/// If the input advertises byte offsets but then refuses to provide the slice, the scanner
/// reports an internal error rather than panicking.
#[test]
fn quoted_scalar_from_offsets_without_slices_is_internal_error() {
let mut parser = Parser::new(SliceableStreamInput::new("\"hello world\"\n", false));
let error = parser.find_map(Result::err).expect("expected parser error");
assert_eq!(
error.info(),
"internal error: input advertised offsets but did not provide a slice"
);
}
// -------------------------------------------------------------------------------------------
// Keys and error paths
// -------------------------------------------------------------------------------------------
/// An explicit key indicator right after a flow sequence on the same line is invalid
/// (scanner.rs `fetch_key`, "mapping keys are not allowed in this context").
#[test]
fn explicit_key_after_flow_sequence_end_is_rejected() {
assert_eq!(
first_error_info("[a] ? b\n"),
"mapping keys are not allowed in this context"
);
}
/// A `,` after a flow sequence that opened at a required-simple-key position trips the
/// required-key check in `remove_simple_key` (scanner.rs, "simple key expected ':'").
#[test]
fn flow_entry_after_required_simple_key_is_rejected() {
assert_eq!(
first_error_info("x: y\n[a], b\n"),
"simple key expected ':'"
);
}
/// An empty flow sequence used as a mapping key in an already-open block mapping calls
/// `roll_indent` while the current indent is still the one-column indent prepared by the `[`
/// indicator, so no new block mapping is started (scanner.rs `roll_indent`,
/// `self.indent > col` case).
#[test]
fn empty_flow_sequence_key_in_existing_block_mapping() {
let events = parse_events("x: y\n[]: b\n").unwrap();
assert_eq!(
events,
vec![
Event::StreamStart,
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),
Event::Scalar("x".into(), ScalarStyle::Plain, 0, None),
Event::Scalar("y".into(), ScalarStyle::Plain, 0, None),
Event::SequenceStart(StructureStyle::Flow, 0, None),
Event::SequenceEnd,
Event::Scalar("b".into(), ScalarStyle::Plain, 0, None),
Event::MappingEnd,
Event::DocumentEnd,
Event::StreamEnd,
]
);
}
/// A block sequence indented one column under its mapping key replaces the non-block indent
/// prepared by the `:` value with a block sequence indent (scanner.rs `roll_indent`, non-block
/// indent removal).
#[test]
fn sequence_indented_one_column_under_mapping_key() {
let events = parse_events("a:\n - b\n").unwrap();
assert_eq!(
events,
vec![
Event::StreamStart,
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),
Event::Scalar("a".into(), ScalarStyle::Plain, 0, None),
Event::SequenceStart(StructureStyle::Block, 0, None),
Event::Scalar("b".into(), ScalarStyle::Plain, 0, None),
Event::SequenceEnd,
Event::MappingEnd,
Event::DocumentEnd,
Event::StreamEnd,
]
);
}
//! Coverage tests for scanner fallback paths around tags, tag directives, anchors and flow
//! scalars.
//!
//! Many of the exercised branches are only taken when the input does not support zero-copy
//! borrowing. They are reached through:
//! - [`BufferedInput`] (streaming input: `byte_offset()` is `None`), and
//! - custom [`Input`] implementations that advertise byte offsets but cannot (always) provide
//! slices, which is a legal implementation of the optional slicing capability.
use std::cell::Cell;
use granit_parser::{
BorrowedInput, BufferedInput, Event, Input, Parser, ScalarStyle, ScanError, Scanner, StrInput,
Tag,
};
// --- Custom inputs ------------------------------------------------------------------------
/// Controls how [`OpaqueInput::slice_bytes`] behaves.
#[derive(Clone, Copy)]
enum SliceMode {
/// `slice_bytes` delegates to the inner `StrInput` (offsets available, no zero-copy borrow).
Delegate,
/// `slice_bytes` always returns `None` (offsets available, no slicing at all).
Never,
/// `slice_bytes` returns `None` only for empty ranges.
NoneWhenEmpty,
/// `slice_bytes` delegates for the first call, then returns `None`.
FirstCallOnly,
}
/// A `StrInput` wrapper that never hands out `'input`-borrowed slices, with configurable
/// `slice_bytes` behavior. This models streaming inputs that track byte offsets but have no
/// stable backing storage.
struct OpaqueInput<'a> {
inner: StrInput<'a>,
mode: SliceMode,
slice_calls: Cell<usize>,
}
impl<'a> OpaqueInput<'a> {
fn new(source: &'a str, mode: SliceMode) -> Self {
Self {
inner: StrInput::new(source),
mode,
slice_calls: Cell::new(0),
}
}
}
impl Input for OpaqueInput<'_> {
fn lookahead(&mut self, count: usize) {
self.inner.lookahead(count);
}
fn buflen(&self) -> usize {
self.inner.buflen()
}
fn bufmaxlen(&self) -> usize {
self.inner.bufmaxlen()
}
fn raw_read_ch(&mut self) -> char {
self.inner.raw_read_ch()
}
fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
self.inner.raw_read_non_breakz_ch()
}
fn skip(&mut self) {
self.inner.skip();
}
fn skip_n(&mut self, count: usize) {
self.inner.skip_n(count);
}
fn peek(&self) -> char {
self.inner.peek()
}
fn peek_nth(&self, n: usize) -> char {
self.inner.peek_nth(n)
}
fn byte_offset(&self) -> Option<usize> {
self.inner.byte_offset()
}
fn slice_bytes(&self, start: usize, end: usize) -> Option<&str> {
match self.mode {
SliceMode::Delegate => self.inner.slice_bytes(start, end),
SliceMode::Never => None,
SliceMode::NoneWhenEmpty => {
if start == end {
None
} else {
self.inner.slice_bytes(start, end)
}
}
SliceMode::FirstCallOnly => {
let calls = self.slice_calls.get();
self.slice_calls.set(calls + 1);
if calls == 0 {
self.inner.slice_bytes(start, end)
} else {
None
}
}
}
}
}
impl<'a> BorrowedInput<'a> for OpaqueInput<'a> {
fn slice_borrowed(&self, _start: usize, _end: usize) -> Option<&'a str> {
None
}
}
// --- Helpers ------------------------------------------------------------------------------
fn collect_events<'input, T: BorrowedInput<'input>>(
input: T,
) -> Result<Vec<Event<'input>>, ScanError> {
Parser::new(input)
.map(|event| event.map(|(event, _)| event))
.collect()
}
fn first_error<'input, T: BorrowedInput<'input>>(input: T) -> ScanError {
for event in Parser::new(input) {
if let Err(error) = event {
return error;
}
}
panic!("expected parser error");
}
fn buffered(source: &str) -> BufferedInput<std::str::Chars<'_>> {
BufferedInput::new(source.chars())
}
/// Extract `(value, tag)` from the first tagged scalar event.
fn first_tagged_scalar(events: &[Event<'_>]) -> (String, Tag) {
events
.iter()
.find_map(|event| {
if let Event::Scalar(value, _, _, Some(tag)) = event {
Some((value.to_string(), tag.clone().into_owned()))
} else {
None
}
})
.expect("expected a tagged scalar event")
}
// --- Streaming input (`byte_offset() == None`): owned tag scanning paths -------------------
#[test]
fn streaming_tag_directive_resolves_shorthand_tag() {
let events = collect_events(buffered(
"%TAG !e! tag:example.com,2000:app/\n---\n!e!foo bar\n",
))
.expect("valid YAML must parse from a streaming input");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "bar");
assert_eq!(tag.handle, "tag:example.com,2000:app/");
assert_eq!(tag.suffix, "foo");
assert_eq!(tag.original_handle, "!e!");
}
#[test]
fn streaming_tag_directive_handle_without_trailing_bang_errors() {
let error = first_error(buffered("%TAG !e tag:example.com\n---\nx\n"));
assert_eq!(
error.info(),
"while parsing a tag directive, did not find expected '!'"
);
}
#[test]
fn streaming_tag_directive_prefix_rejects_invalid_global_tag_char() {
let error = first_error(buffered("%TAG !e! }bad\n---\nx\n"));
assert_eq!(error.info(), "invalid global tag character");
}
#[test]
fn streaming_tag_directive_prefix_decodes_uri_escape() {
let events = collect_events(buffered("%TAG !e! tag:ex%61mple/\n---\n!e!x 1\n"))
.expect("escaped tag prefix must parse from a streaming input");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "tag:example/");
assert_eq!(tag.suffix, "x");
}
#[test]
fn streaming_anchor_without_name_errors() {
let error = first_error(buffered("&\n"));
assert_eq!(
error.info(),
"while scanning an anchor or alias, did not find expected alphabetic or numeric character"
);
}
// --- Plain `StrInput` edge cases ------------------------------------------------------------
#[test]
fn tag_directive_prefix_starting_with_uri_escape_is_decoded() {
let events = collect_events(StrInput::new("%TAG !e! %61pp/\n--- !e!x 1\n"))
.expect("prefix starting with an escape must parse");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "app/");
assert_eq!(tag.suffix, "x");
}
#[test]
fn verbatim_tag_with_uri_escape_is_decoded() {
let events = collect_events(StrInput::new("--- !<tag:ex%61mple> 1\n"))
.expect("verbatim tag with escape must parse");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(format!("{}{}", tag.handle, tag.suffix), "tag:example");
}
#[test]
fn directive_name_with_control_character_errors() {
let error = first_error(StrInput::new("%YA\u{7}ML 1.2\n---\nx\n"));
assert_eq!(
error.info(),
"while scanning a directive, found unexpected non-alphabetical character"
);
}
#[test]
fn required_simple_key_at_eof_without_newline_errors() {
// `b` opens a required simple key inside the block mapping; the stream ends on the same
// line, so the error is reported by the stream-end check rather than key staleness.
let error = first_error(StrInput::new("a: 1\nb"));
assert_eq!(error.info(), "simple key expected ':'");
assert_eq!(error.marker().line(), 2);
assert_eq!(error.marker().col(), 0);
}
#[test]
fn scanner_reports_misplaced_bracket_when_resumed_after_unclosed_bracket() {
// The public `Scanner` API can be driven past a first error. The `---` marker inside the
// unclosed flow sequence pops the flow marker while reporting the first error; resuming
// then reaches `]` with a non-zero flow level but no recorded opening bracket.
let mut scanner = Scanner::new(StrInput::new("[\n--- ]\n"));
let first = loop {
match scanner.next_token() {
Ok(Some(_)) => {}
Ok(None) => panic!("expected a first scan error"),
Err(error) => break error,
}
};
assert_eq!(first.info(), "unclosed bracket '['");
let second = loop {
match scanner.next_token() {
Ok(Some(_)) => {}
Ok(None) => panic!("expected a second scan error"),
Err(error) => break error,
}
};
assert_eq!(second.info(), "misplaced bracket");
}
// --- Offsets available but no zero-copy borrowing (`SliceMode::Delegate`) -------------------
#[test]
fn no_borrow_input_resolves_tag_directive_via_owned_slices() {
let events = collect_events(OpaqueInput::new(
"%TAG !e! tag:e/\n---\n!e!foo 1\n",
SliceMode::Delegate,
))
.expect("tag directive must parse without zero-copy borrowing");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "tag:e/");
assert_eq!(tag.suffix, "foo");
assert_eq!(tag.original_handle, "!e!");
}
#[test]
fn no_borrow_input_rejects_tag_directive_handle_without_bang() {
let error = first_error(OpaqueInput::new(
"%TAG !e tag:e/\n---\nx\n",
SliceMode::Delegate,
));
assert_eq!(
error.info(),
"while parsing a tag directive, did not find expected '!'"
);
}
#[test]
fn no_borrow_input_scans_primary_handle_tag_as_owned() {
let events = collect_events(OpaqueInput::new("!foo 1\n", SliceMode::Delegate))
.expect("local tag must parse without zero-copy borrowing");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "!");
assert_eq!(tag.suffix, "foo");
}
#[test]
fn no_borrow_input_scans_anchor_and_alias_as_owned() {
let events = collect_events(OpaqueInput::new("- &a 1\n- *a\n", SliceMode::Delegate))
.expect("anchors must parse without zero-copy borrowing");
let anchor_id = events
.iter()
.find_map(|event| {
if let Event::Scalar(value, ScalarStyle::Plain, anchor_id, None) = event {
(value == "1").then_some(*anchor_id)
} else {
None
}
})
.expect("expected the anchored scalar");
assert_ne!(anchor_id, 0, "anchored scalar must be assigned an id");
assert!(
events.contains(&Event::Alias(anchor_id)),
"alias must reference the anchored scalar id"
);
}
// --- Offsets available but `slice_bytes` unavailable (`SliceMode::Never`) -------------------
#[test]
fn sliceless_input_reports_internal_error_when_promoting_flow_scalar() {
let error = first_error(OpaqueInput::new("'a''b'\n", SliceMode::Never));
assert_eq!(
error.info(),
"internal error: input advertised offsets but did not provide a slice"
);
}
#[test]
fn sliceless_input_reports_internal_error_in_tag_directive_handle() {
let error = first_error(OpaqueInput::new(
"%TAG !e! tag:e/\n---\nx\n",
SliceMode::Never,
));
assert_eq!(
error.info(),
"internal error: input advertised slicing but did not provide a slice"
);
}
#[test]
fn sliceless_input_reports_internal_error_in_tag_handle() {
let error = first_error(OpaqueInput::new("!!str 1\n", SliceMode::Never));
assert_eq!(
error.info(),
"internal error: input advertised slicing but did not provide a slice"
);
}
#[test]
fn sliceless_input_reports_internal_error_in_anchor() {
let error = first_error(OpaqueInput::new("&a 1\n", SliceMode::Never));
assert_eq!(
error.info(),
"internal error: input advertised slicing but did not provide a slice"
);
}
// --- `slice_bytes` unavailable only for empty ranges (`SliceMode::NoneWhenEmpty`) -----------
#[test]
fn empty_range_sliceless_input_decodes_tag_directive_prefix_escape() {
let events = collect_events(OpaqueInput::new(
"%TAG !e! %61pp/\n--- !e!x 1\n",
SliceMode::NoneWhenEmpty,
))
.expect("prefix starting with an escape must parse without empty-range slices");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "app/");
assert_eq!(tag.suffix, "x");
}
#[test]
fn empty_range_sliceless_input_decodes_tag_suffix_escape() {
let events = collect_events(OpaqueInput::new("!%61bc 1\n", SliceMode::NoneWhenEmpty))
.expect("tag suffix starting with an escape must parse without empty-range slices");
let (value, tag) = first_tagged_scalar(&events);
assert_eq!(value, "1");
assert_eq!(tag.handle, "!");
assert_eq!(tag.suffix, "abc");
}
// --- `slice_bytes` fails after the first call (`SliceMode::FirstCallOnly`) ------------------
#[test]
fn slice_loss_between_handle_and_prefix_reports_internal_error() {
// The tag directive handle is sliced successfully (first call), then slicing the prefix
// fails, hitting the defensive fallback in the prefix scanner.
let error = first_error(OpaqueInput::new(
"%TAG !e! tag:e/\n---\nx\n",
SliceMode::FirstCallOnly,
));
assert_eq!(
error.info(),
"internal error: input advertised slicing but did not provide a slice"
);
}
#[test]
fn slice_loss_between_tag_handle_and_suffix_reports_internal_error() {
// The tag handle `!e!` is sliced successfully (first call), then slicing the suffix fails,
// hitting the defensive fallback in the shorthand suffix scanner.
let error = first_error(OpaqueInput::new("!e!foo 1\n", SliceMode::FirstCallOnly));
assert_eq!(
error.info(),
"internal error: input advertised slicing but did not provide a slice"
);
}
+1
-1
{
"git": {
"sha1": "db5bf9f1b85fc1de1e32e08ddf296872c1403ee8"
"sha1": "66f3df3edcbeed550f4d9f5665fde4fd544de1d4"
},
"path_in_vcs": ""
}

@@ -115,3 +115,3 @@ # This file is automatically @generated by Cargo.

name = "granit-parser"
version = "0.0.6"
version = "0.0.7"
dependencies = [

@@ -118,0 +118,0 @@ "arraydeque",

@@ -16,3 +16,3 @@ # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO

name = "granit-parser"
version = "0.0.6"
version = "0.0.7"
authors = [

@@ -139,2 +139,10 @@ "Ethiraric <ethiraric@gmail.com>",

[[test]]
name = "parser_input"
path = "tests/parser_input.rs"
[[test]]
name = "parser_input_regressions"
path = "tests/parser_input_regressions.rs"
[[test]]
name = "plain_scalar_indicators"

@@ -152,2 +160,6 @@ path = "tests/plain_scalar_indicators.rs"

[[test]]
name = "regressions"
path = "tests/regressions.rs"
[[test]]
name = "reserved_directive"

@@ -157,2 +169,10 @@ path = "tests/reserved_directive.rs"

[[test]]
name = "scanner_blocks"
path = "tests/scanner_blocks.rs"
[[test]]
name = "scanner_tags_flow"
path = "tests/scanner_tags_flow.rs"
[[test]]
name = "span"

@@ -195,3 +215,3 @@ path = "tests/span.rs"

[dependencies.smallvec]
version = ">= 1.0.0, < 1.16.0"
version = ">= 1.0.0, < 2.0.0"

@@ -198,0 +218,0 @@ [dev-dependencies.libtest-mimic]

# Changelog
## v0.0.7
- Added `YamlVersion` and changed `Event::DocumentStart` to
`DocumentStart(bool, Option<YamlVersion>)`, emitting the `%YAML` directive
version for each document start when present.
- Added `Tag::suffix_in_namespace`, which returns the type name a tag resolves to within
an arbitrary namespace prefix (matching the resolved `handle ++ suffix` URI across every
spelling, including `%TAG` mid-name splits). This generalizes `Tag::core_suffix`, which
now delegates to it.
- Add Tag::suffix_in_namespace: resolved type name within an arbitrary namespace
- Summarizing results of automated fuzzing and static analysis, robustness against malformed (bad intent)
YAML input was further hardened.
## v0.0.6

@@ -3,0 +15,0 @@ - Added `Tag::core_suffix` and made core-tag helpers match the resolved YAML 1.2.2

+42
-34

@@ -20,3 +20,3 @@ # granit-parser

**granit-parser** is both YAML 1.1 and 1.2 compliant parser in pure Rust with strict compliance, comment and style support, no-std support, and spans for parser events. “Granit” is a correct word in many European languages (English *granite*).
**granit-parser** is a YAML 1.2 parser in pure Rust with comment and style support, no-std support, and spans for parser events. It accepts many real-world YAML documents from the YAML 1.1 era where the syntax overlaps with YAML 1.2. “Granit” is a correct word in many European languages (English *granite*).

@@ -42,2 +42,4 @@ This crate started as a fork of [saphyr-parser](https://crates.io/crates/saphyr-parser) that descends from [yaml-rust](https://github.com/chyh1990/yaml-rust), with influences from [libyaml](https://crates.io/crates/libyaml) and [yaml-cpp](https://github.com/jbeder/yaml-cpp). The project has since diverged significantly and is now maintained as an independent project.

Document starts are emitted as `Event::DocumentStart(explicit, version)`, where `version` is the optional `YamlVersion` declared by a preceding `%YAML` directive for that document.
```rust

@@ -48,2 +50,3 @@ use granit_parser::Parser;

let yaml = r#"
%YAML 1.2
%TAG !example! tag:example.com,2000:

@@ -102,35 +105,35 @@ %TAG !core! tag:yaml.org,2002:i

StreamStart bytes=Some(0..0) source=Some("")
DocumentStart(true) bytes=Some(70..73) source=Some("---")
MappingStart(Block, 0, None) bytes=Some(74..74) source=Some("")
Scalar("items", Plain, 0, None) bytes=Some(74..79) source=Some("items")
node tag: !shopping custom=true tag_start(line,col,byte)=Some((5, 7, Some(81)))
SequenceStart(Block, 0, Some(Tag { handle: "!", suffix: "shopping", original_handle: "!" })) bytes=Some(93..93) source=Some("")
Scalar("milk", Plain, 0, None) bytes=Some(95..99) source=Some("milk")
scalar tag: tag:example.com,2000:sliced core-suffix=None core-str=false tag_start(line,col,byte)=Some((7, 4, Some(104))) for "bread"
Scalar("bread", Plain, 0, Some(Tag { handle: "tag:example.com,2000:", suffix: "sliced", original_handle: "!example!" })) bytes=Some(120..125) source=Some("bread")
scalar tag: tag:yaml.org,2002:str core-suffix=Some("str") core-str=true tag_start(line,col,byte)=Some((8, 4, Some(130))) for "bread"
Scalar("bread", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:", suffix: "str", original_handle: "!!" })) bytes=Some(136..141) source=Some("bread")
scalar tag: tag:yaml.org,2002:int core-suffix=Some("int") core-str=false tag_start(line,col,byte)=Some((9, 4, Some(146))) for "1"
Scalar("1", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:i", suffix: "nt", original_handle: "!core!" })) bytes=Some(155..156) source=Some("1")
SequenceEnd bytes=Some(157..157) source=Some("")
Scalar("locations", Plain, 0, None) bytes=Some(157..166) source=Some("locations")
Comment(" Example with composite keys", Right) bytes=Some(168..197) source=Some("# Example with composite keys")
MappingStart(Block, 0, None) bytes=Some(200..200) source=Some("")
SequenceStart(Flow, 0, None) bytes=Some(200..201) source=Some("[")
Scalar("47.3769", Plain, 0, None) bytes=Some(201..208) source=Some("47.3769")
Scalar("8.5417", Plain, 0, None) bytes=Some(210..216) source=Some("8.5417")
SequenceEnd bytes=Some(216..217) source=Some("]")
Scalar("local", Plain, 0, None) bytes=Some(219..224) source=Some("local")
SequenceStart(Flow, 0, None) bytes=Some(227..228) source=Some("[")
Scalar("40.7128", Plain, 0, None) bytes=Some(228..235) source=Some("40.7128")
Scalar("-74.0060", Plain, 0, None) bytes=Some(237..245) source=Some("-74.0060")
SequenceEnd bytes=Some(245..246) source=Some("]")
Scalar("remote", Plain, 0, None) bytes=Some(248..254) source=Some("remote")
Comment(" JSON-style \\uXXXX surrogate pairs:", Above) bytes=Some(256..292) source=Some("# JSON-style \\uXXXX surrogate pairs:")
MappingEnd bytes=Some(293..293) source=Some("")
Scalar("music", Plain, 0, None) bytes=Some(293..298) source=Some("music")
Scalar("𝄞🎵🎶", DoubleQuoted, 0, None) bytes=Some(300..338) source=Some("\"\\uD834\\uDD1E\\uD83C\\uDFB5\\uD83C\\uDFB6\"")
MappingEnd bytes=Some(339..339) source=Some("")
DocumentEnd bytes=Some(339..339) source=Some("")
StreamEnd bytes=Some(339..339) source=Some("")
DocumentStart(true, Some(YamlVersion { major: 1, minor: 2 })) bytes=Some(80..83) source=Some("---")
MappingStart(Block, 0, None) bytes=Some(84..84) source=Some("")
Scalar("items", Plain, 0, None) bytes=Some(84..89) source=Some("items")
node tag: !shopping custom=true tag_start(line,col,byte)=Some((6, 7, Some(91)))
SequenceStart(Block, 0, Some(Tag { handle: "!", suffix: "shopping", original_handle: "!" })) bytes=Some(103..103) source=Some("")
Scalar("milk", Plain, 0, None) bytes=Some(105..109) source=Some("milk")
scalar tag: tag:example.com,2000:sliced core-suffix=None core-str=false tag_start(line,col,byte)=Some((8, 4, Some(114))) for "bread"
Scalar("bread", Plain, 0, Some(Tag { handle: "tag:example.com,2000:", suffix: "sliced", original_handle: "!example!" })) bytes=Some(130..135) source=Some("bread")
scalar tag: tag:yaml.org,2002:str core-suffix=Some("str") core-str=true tag_start(line,col,byte)=Some((9, 4, Some(140))) for "bread"
Scalar("bread", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:", suffix: "str", original_handle: "!!" })) bytes=Some(146..151) source=Some("bread")
scalar tag: tag:yaml.org,2002:int core-suffix=Some("int") core-str=false tag_start(line,col,byte)=Some((10, 4, Some(156))) for "1"
Scalar("1", Plain, 0, Some(Tag { handle: "tag:yaml.org,2002:i", suffix: "nt", original_handle: "!core!" })) bytes=Some(165..166) source=Some("1")
SequenceEnd bytes=Some(167..167) source=Some("")
Scalar("locations", Plain, 0, None) bytes=Some(167..176) source=Some("locations")
Comment(" Example with composite keys", Right) bytes=Some(178..207) source=Some("# Example with composite keys")
MappingStart(Block, 0, None) bytes=Some(210..210) source=Some("")
SequenceStart(Flow, 0, None) bytes=Some(210..211) source=Some("[")
Scalar("47.3769", Plain, 0, None) bytes=Some(211..218) source=Some("47.3769")
Scalar("8.5417", Plain, 0, None) bytes=Some(220..226) source=Some("8.5417")
SequenceEnd bytes=Some(226..227) source=Some("]")
Scalar("local", Plain, 0, None) bytes=Some(229..234) source=Some("local")
SequenceStart(Flow, 0, None) bytes=Some(237..238) source=Some("[")
Scalar("40.7128", Plain, 0, None) bytes=Some(238..245) source=Some("40.7128")
Scalar("-74.0060", Plain, 0, None) bytes=Some(247..255) source=Some("-74.0060")
SequenceEnd bytes=Some(255..256) source=Some("]")
Scalar("remote", Plain, 0, None) bytes=Some(258..264) source=Some("remote")
Comment(" JSON-style \\uXXXX surrogate pairs:", Above) bytes=Some(266..302) source=Some("# JSON-style \\uXXXX surrogate pairs:")
MappingEnd bytes=Some(303..303) source=Some("")
Scalar("music", Plain, 0, None) bytes=Some(303..308) source=Some("music")
Scalar("𝄞🎵🎶", DoubleQuoted, 0, None) bytes=Some(310..348) source=Some("\"\\uD834\\uDD1E\\uD83C\\uDFB5\\uD83C\\uDFB6\"")
MappingEnd bytes=Some(349..349) source=Some("")
DocumentEnd bytes=Some(349..349) source=Some("")
StreamEnd bytes=Some(349..349) source=Some("")
```

@@ -260,2 +263,3 @@

- `Tag::core_suffix`
- `Tag::suffix_in_namespace`
- `Tag::is_custom`

@@ -269,2 +273,6 @@ - `Tag::is_yaml_core_schema_tag`

### Unsupported features
YAML 1.1 line breaks. YAML 1.1 defines NEL (U+0085), LS (U+2028) and PS (U+2029) as line breaks, this parser
uses `\n`/`\r` only line-breaking that as expected in YAML 1.2.
## Tools

@@ -271,0 +279,0 @@

@@ -54,7 +54,17 @@ //! Utilities to create a source of input to the parser.

///
/// Implementers of [`Input`] must _not_ load more than `count` characters into the buffer. The
/// parser tracks how many characters are loaded in the buffer and acts accordingly.
/// Implementers of [`Input`] must _not_ expose a lookahead window larger than
/// [`Input::bufmaxlen`]. They may retain a larger window requested by an earlier call; callers
/// should use [`Input::buflen`] to observe the currently available window.
fn lookahead(&mut self, count: usize);
/// Return the number of buffered characters in `self`.
/// Return the number of characters in the active lookahead window.
///
/// This is the number of characters that the input promises can be read through [`peek`] and
/// [`peek_nth`] after prior [`lookahead`] calls. It is not necessarily the number of source
/// characters remaining: inputs may keep the window available after consuming characters and
/// may pad positions past EOF with `\0`.
///
/// [`lookahead`]: Input::lookahead
/// [`peek`]: Input::peek
/// [`peek_nth`]: Input::peek_nth
#[must_use]

@@ -67,3 +77,10 @@ fn buflen(&self) -> usize;

/// Return whether the lookahead buffer is empty.
/// Return whether the active lookahead window is empty.
///
/// This is equivalent to `self.buflen() == 0`. It does not mean the underlying source is
/// exhausted: after a previous [`lookahead`] call, an input may keep a non-empty lookahead
/// window available even after all source characters have been consumed, with positions past
/// EOF observed as `\0`.
///
/// [`lookahead`]: Input::lookahead
#[inline]

@@ -75,5 +92,6 @@ #[must_use]

/// Read a character from the input stream and return it directly.
/// Read the next character from the logical input stream and return it directly.
///
/// The internal buffer (if any) is bypassed.
/// If an implementation has already fetched characters for lookahead, this consumes the
/// buffered stream front before reading farther from the underlying source.
#[must_use]

@@ -84,3 +102,4 @@ fn raw_read_ch(&mut self) -> char;

///
/// The internal buffer (if any) is bypassed.
/// If an implementation has already fetched characters for lookahead, this consumes from the
/// buffered stream front before reading farther from the underlying source.
///

@@ -87,0 +106,0 @@ /// If the next character is a breakz, it is either not consumed or placed into the buffer (if

@@ -31,2 +31,8 @@ use crate::char_traits::is_breakz;

buffer: ArrayDeque<char, BUFFER_LEN>,
/// Number of front buffer characters that came from the iterator, not EOF padding.
real_buffered: usize,
/// Largest active lookahead window requested by the scanner.
lookahead: usize,
/// Whether the wrapped iterator has reported EOF.
source_exhausted: bool,
}

@@ -40,4 +46,70 @@

buffer: ArrayDeque::default(),
real_buffered: 0,
lookahead: 0,
source_exhausted: false,
}
}
fn push_source_or_padding(&mut self) {
let c = if self.source_exhausted {
'\0'
} else if let Some(c) = self.input.next() {
self.real_buffered += 1;
c
} else {
self.source_exhausted = true;
'\0'
};
self.buffer.push_back(c).unwrap();
}
fn fill_lookahead(&mut self) {
while self.buffer.len() < self.lookahead {
self.push_source_or_padding();
}
}
fn pop_buffered(&mut self) -> Option<(char, bool)> {
let c = self.buffer.pop_front()?;
let is_real = self.real_buffered > 0;
if is_real {
self.real_buffered -= 1;
}
Some((c, is_real))
}
fn read_source_or_eof(&mut self) -> (char, bool) {
if self.source_exhausted {
('\0', false)
} else if let Some(c) = self.input.next() {
(c, true)
} else {
self.source_exhausted = true;
('\0', false)
}
}
fn raw_read_front(&mut self) -> (char, bool) {
let read = self
.pop_buffered()
.unwrap_or_else(|| self.read_source_or_eof());
self.fill_lookahead();
read
}
fn skip_one(&mut self) -> bool {
let skipped = match self.pop_buffered() {
Some((_, true)) => true,
Some((_, false)) => {
self.buffer.push_front('\0').unwrap();
false
}
None => self.read_source_or_eof().1,
};
if skipped {
self.fill_lookahead();
}
skipped
}
}

@@ -48,12 +120,4 @@

fn lookahead(&mut self, count: usize) {
let target = count.min(BUFFER_LEN);
if self.buffer.len() >= target {
return;
}
for _ in 0..(target - self.buffer.len()) {
self.buffer
.push_back(self.input.next().unwrap_or('\0'))
.unwrap();
}
self.lookahead = self.lookahead.max(count.min(BUFFER_LEN));
self.fill_lookahead();
}

@@ -63,3 +127,3 @@

fn buflen(&self) -> usize {
self.buffer.len()
self.lookahead
}

@@ -74,3 +138,3 @@

fn raw_read_ch(&mut self) -> char {
self.input.next().unwrap_or('\0')
self.raw_read_front().0
}

@@ -80,11 +144,20 @@

fn raw_read_non_breakz_ch(&mut self) -> Option<char> {
if let Some(c) = self.input.next() {
if let Some(c) = self.buffer.front().copied() {
if is_breakz(c) {
None
} else {
Some(self.raw_read_front().0)
}
} else {
let (c, is_real) = self.read_source_or_eof();
if !is_real {
None
} else if is_breakz(c) {
self.buffer.push_back(c).unwrap();
self.real_buffered += 1;
None
} else {
self.fill_lookahead();
Some(c)
}
} else {
None
}

@@ -95,3 +168,3 @@ }

fn skip(&mut self) {
self.buffer.pop_front();
self.skip_one();
}

@@ -101,3 +174,7 @@

fn skip_n(&mut self, count: usize) {
self.buffer.drain(0..count);
for _ in 0..count {
if !self.skip_one() {
break;
}
}
}

@@ -107,3 +184,3 @@

fn peek(&self) -> char {
self.buffer[0]
self.buffer.front().copied().unwrap_or('\0')
}

@@ -113,3 +190,3 @@

fn peek_nth(&self, n: usize) -> char {
self.buffer[n]
self.buffer.get(n).copied().unwrap_or('\0')
}

@@ -129,2 +206,4 @@ }

mod tests {
use crate::input::str::StrInput;
use super::*;

@@ -146,3 +225,3 @@

#[test]
fn raw_reads_bypass_buffer_and_report_eof() {
fn raw_reads_use_stream_front_and_report_eof() {
let mut input = BufferedInput::new("a".chars());

@@ -152,6 +231,11 @@

assert_eq!(input.raw_read_ch(), '\0');
let mut input = BufferedInput::new("ab".chars());
input.lookahead(1);
assert_eq!(input.raw_read_ch(), 'a');
assert_eq!(input.peek(), 'b');
}
#[test]
fn raw_read_non_breakz_pushes_break_back_into_buffer() {
fn raw_read_non_breakz_leaves_break_at_stream_front() {
let mut input = BufferedInput::new("a\n".chars());

@@ -161,2 +245,4 @@

assert_eq!(input.raw_read_non_breakz_ch(), None);
assert_eq!(input.peek(), '\n');
input.lookahead(1);
assert_eq!(input.buflen(), 1);

@@ -170,3 +256,3 @@ assert_eq!(input.peek(), '\n');

#[test]
fn skip_n_drains_buffered_characters() {
fn skip_n_consumes_stream_front_and_preserves_lookahead_window() {
let mut input = BufferedInput::new("abcdef".chars());

@@ -177,8 +263,54 @@

assert_eq!(input.buflen(), 3);
assert_eq!(input.buflen(), 5);
assert_eq!(input.peek(), 'c');
assert_eq!(input.peek_nth(2), 'e');
assert_eq!(input.peek_nth(3), 'f');
assert_eq!(input.peek_nth(4), '\0');
}
#[test]
fn skip_without_lookahead_consumes_like_str_input() {
let mut buffered = BufferedInput::new("ab".chars());
buffered.skip();
buffered.lookahead(1);
let mut str_input = StrInput::new("ab");
str_input.skip();
str_input.lookahead(1);
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn skip_n_saturates_at_eof_like_str_input() {
let mut buffered = BufferedInput::new("abc".chars());
buffered.lookahead(1);
buffered.skip_n(8);
buffered.lookahead(1);
let mut str_input = StrInput::new("abc");
str_input.lookahead(1);
str_input.skip_n(8);
str_input.lookahead(1);
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn buflen_matches_str_input_lookahead_window_after_consumption() {
let mut buffered = BufferedInput::new("ab".chars());
buffered.lookahead(2);
buffered.skip();
buffered.skip();
let mut str_input = StrInput::new("ab");
str_input.lookahead(2);
str_input.skip();
str_input.skip();
assert_eq!(buffered.buflen(), str_input.buflen());
assert_eq!(buffered.buf_is_empty(), str_input.buf_is_empty());
assert_eq!(buffered.peek(), str_input.peek());
}
#[test]
fn streaming_input_never_borrows_slices() {

@@ -185,0 +317,0 @@ let input = BufferedInput::new("abc".chars());

@@ -98,3 +98,3 @@ // Copyright 2015, Yuheng Chen.

Event, EventReceiver, Parser, ParserTrait, SpannedEventReceiver, StructureStyle, Tag,
TryEventReceiver, TryLoadError, TrySpannedEventReceiver,
TryEventReceiver, TryLoadError, TrySpannedEventReceiver, YamlVersion,
};

@@ -101,0 +101,0 @@ pub use crate::scanner::{

@@ -164,2 +164,3 @@ use crate::{

current: Option<(Event<'input>, Span)>,
current_error: Option<ScanError>,
stream_end_emitted: bool,

@@ -181,2 +182,3 @@ #[allow(clippy::type_complexity)]

current: None,
current_error: None,
stream_end_emitted: false,

@@ -244,2 +246,9 @@ include_resolver: None,

fn prepare_for_push(&mut self) {
if matches!(self.current.as_ref(), Some((Event::StreamEnd, _))) {
self.current = None;
}
self.stream_end_emitted = false;
}
/// Push a string parser onto the stack.

@@ -250,2 +259,3 @@ ///

pub fn push_str_parser(&mut self, mut parser: Parser<'input, StrInput<'input>>, name: String) {
self.prepare_for_push();
if let Some(parent) = self.parsers.last() {

@@ -266,2 +276,3 @@ parser.set_anchor_offset(parent.get_anchor_offset());

) {
self.prepare_for_push();
if let Some(parent) = self.parsers.last() {

@@ -278,2 +289,3 @@ parser.set_anchor_offset(parent.get_anchor_offset());

pub fn push_custom_parser(&mut self, mut parser: Parser<'input, T>, name: String) {
self.prepare_for_push();
if let Some(parent) = self.parsers.last() {

@@ -290,2 +302,3 @@ parser.set_anchor_offset(parent.get_anchor_offset());

pub fn push_replay_parser(&mut self, mut parser: ReplayParser<'input>, name: String) {
self.prepare_for_push();
if let Some(parent) = self.parsers.last() {

@@ -309,2 +322,3 @@ let inherited = parent.get_anchor_offset();

) {
self.prepare_for_push();
if let Some(parent) = self.parsers.last() {

@@ -344,2 +358,7 @@ parser.set_anchor_offset(parent.get_anchor_offset());

fn pop_parser_and_propagate_anchor_offset(&mut self) {
let popped = self.parsers.pop().unwrap();
self.propagate_anchor_offset_from_popped(&popped);
}
fn next_event_impl(&mut self) -> Result<(Event<'input>, Span), ScanError> {

@@ -367,4 +386,3 @@ loop {

}
let popped = self.parsers.pop().unwrap();
self.propagate_anchor_offset_from_popped(&popped);
self.pop_parser_and_propagate_anchor_offset();
}

@@ -379,8 +397,6 @@ None => {

}
let popped = self.parsers.pop().unwrap();
self.propagate_anchor_offset_from_popped(&popped);
self.pop_parser_and_propagate_anchor_offset();
}
Some(Err(e)) => {
let popped = self.parsers.pop().unwrap();
self.propagate_anchor_offset_from_popped(&popped);
self.pop_parser_and_propagate_anchor_offset();
return e.into_result();

@@ -403,6 +419,6 @@ }

Some(Ok((Event::StreamEnd, _))) | None => {
let popped = self.parsers.pop().unwrap();
self.propagate_anchor_offset_from_popped(&popped);
self.pop_parser_and_propagate_anchor_offset();
}
_ => {
Some(Ok(_)) => {
self.pop_parser_and_propagate_anchor_offset();
return Err(ScanError::new_str(

@@ -413,2 +429,6 @@ span.start,

}
Some(Err(e)) => {
self.pop_parser_and_propagate_anchor_offset();
return Err(e);
}
}

@@ -418,3 +438,3 @@ }

if self.parsers.len() > 1
&& matches!(event.0, Event::StreamStart | Event::DocumentStart(_))
&& matches!(event.0, Event::StreamStart | Event::DocumentStart(..))
{

@@ -448,2 +468,4 @@ continue;

Some(Ok(x))
} else if let Some(error) = &self.current_error {
Some(Err(error.clone()))
} else {

@@ -458,3 +480,6 @@ if self.stream_end_emitted {

}
Err(e) => Some(e.into_result()),
Err(e) => {
self.current_error = Some(e.clone());
Some(Err(e))
}
}

@@ -465,2 +490,7 @@ }

fn next_event(&mut self) -> Option<ParseResult<'input>> {
if let Some(error) = self.current_error.take() {
self.stream_end_emitted = true;
return Some(Err(error));
}
if let Some(token) = self.current.take() {

@@ -482,3 +512,6 @@ if let Event::StreamEnd = token.0 {

}
Err(e) => Some(e.into_result()),
Err(e) => {
self.stream_end_emitted = true;
Some(Err(e))
}
}

@@ -485,0 +518,0 @@ }

#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::float_cmp)]
use granit_parser::{Event, Parser, Placement, ScalarStyle, ScanError, StructureStyle};
use granit_parser::{
Event, Parser, Placement, ScalarStyle, ScanError, StructureStyle, YamlVersion,
};

@@ -134,3 +136,3 @@ /// Run the parser through the string.

Event::StreamStart,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
Event::Scalar("~".into(), ScalarStyle::Plain, 0, None),

@@ -149,3 +151,3 @@ Event::DocumentEnd,

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),

@@ -175,3 +177,3 @@ Event::Scalar("a".into(), ScalarStyle::Plain, 0, None),

Event::Comment(" This is a comment".into(), Placement::Above),
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),

@@ -202,3 +204,3 @@ Event::Scalar("a".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::SequenceStart(StructureStyle::Block, 0, None),

@@ -228,9 +230,9 @@ Event::Scalar("plain".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::Scalar("a scalar".into(), ScalarStyle::Plain, 0, None),
Event::DocumentEnd,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
Event::Scalar("a scalar".into(), ScalarStyle::Plain, 0, None),
Event::DocumentEnd,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
Event::Scalar("a scalar".into(), ScalarStyle::Plain, 0, None),

@@ -250,4 +252,4 @@ Event::DocumentEnd,

Event::StreamStart,
Event::DocumentStart(false),
Event::Scalar("".into(), ScalarStyle::Plain, 1, None),
Event::DocumentStart(false, None),
Event::Scalar("~".into(), ScalarStyle::Plain, 1, None),
Event::DocumentEnd,

@@ -260,2 +262,41 @@ Event::StreamEnd,

#[test]
fn test_missing_node_with_anchor_is_null_scalar_but_tag_keeps_empty_content() {
let scalar_events = |input: &str| -> Vec<(String, ScalarStyle, usize, Option<String>)> {
run_parser(input)
.unwrap()
.into_iter()
.filter_map(|event| match event {
Event::Scalar(value, style, anchor, tag) => Some((
value.into_owned(),
style,
anchor,
tag.map(|tag| tag.original()),
)),
_ => None,
})
.collect()
};
assert_eq!(
scalar_events("a: &x\n"),
vec![
("a".to_string(), ScalarStyle::Plain, 0, None),
("~".to_string(), ScalarStyle::Plain, 1, None),
]
);
assert_eq!(
scalar_events("a: !!str\n"),
vec![
("a".to_string(), ScalarStyle::Plain, 0, None),
(
String::new(),
ScalarStyle::Plain,
0,
Some("!!str".to_string()),
),
]
);
}
#[test]
fn test_bad_hyphen() {

@@ -295,3 +336,3 @@ // See: https://github.com/chyh1990/yaml-rust/issues/23

Event::Comment(" This is a comment".into(), Placement::Above),
Event::DocumentStart(true),
Event::DocumentStart(true, Some(YamlVersion::new(1, 2))),
Event::Comment("-------".into(), Placement::Right),

@@ -304,3 +345,32 @@ Event::Scalar("foobar".into(), ScalarStyle::Plain, 0, None),

}
#[test]
fn test_directive_followed_by_comment_then_content_errors() {
for yaml in ["%YAML 1.2\n# c\nfoo\n--- bar\n", "%YAML 1.2\n# c\n"] {
let error = run_parser(yaml)
.expect_err("directives must still be followed by an explicit document start");
assert_eq!(error.info(), "did not find expected <document start>");
}
}
#[test]
fn test_empty_block_scalar_value_does_not_depend_on_eof() {
fn value_of(yaml: &str) -> String {
run_parser(yaml)
.unwrap()
.into_iter()
.find_map(|event| match event {
Event::Scalar(value, ScalarStyle::Literal, _, _) => Some(value.into_owned()),
_ => None,
})
.unwrap()
}
assert_eq!(value_of("a: |\nb: c\n"), value_of("a: |\n"));
assert_eq!(value_of("a: |+\nb: c\n"), value_of("a: |+\n"));
assert_eq!(value_of("a: |+\n\n"), "\n");
}
#[test]
fn test_large_block_scalar_indent() {

@@ -322,3 +392,3 @@ // https://github.com/Ethiraric/yaml-rust2/issues/29

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),

@@ -341,3 +411,3 @@ Event::Scalar("a".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::Scalar("----".into(), ScalarStyle::Plain, 0, None),

@@ -353,3 +423,3 @@ Event::DocumentEnd,

Event::StreamStart,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
Event::Comment("comment".into(), Placement::Right),

@@ -366,3 +436,3 @@ Event::Scalar("~".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::Scalar("----".into(), ScalarStyle::Plain, 0, None),

@@ -369,0 +439,0 @@ Event::Comment("comment".into(), Placement::Right),

@@ -1225,3 +1225,3 @@ use std::{borrow::Cow, cell::Cell, rc::Rc};

Event::StreamStart => Some("StreamStart".into()),
Event::DocumentStart(_) => Some("DocumentStart".into()),
Event::DocumentStart(..) => Some("DocumentStart".into()),
Event::SequenceStart(..) => Some("SequenceStart".into()),

@@ -1228,0 +1228,0 @@ Event::SequenceEnd => Some("SequenceEnd".into()),

@@ -39,3 +39,3 @@ use granit_parser::{Event, Parser, ScalarStyle, ScanError, StructureStyle};

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),

@@ -42,0 +42,0 @@ Event::Scalar("~".into(), ScalarStyle::Plain, 0, None),

@@ -98,3 +98,3 @@ use granit_parser::{Event, Parser, ScanError, Span};

.iter()
.find(|(ev, _)| matches!(ev, Event::DocumentStart(true)))
.find(|(ev, _)| matches!(ev, Event::DocumentStart(true, None)))
.expect("DocumentStart(true) event should exist");

@@ -122,4 +122,5 @@

// In YAML, a `---` marker can implicitly terminate the previous document.
// The `DocumentEnd` span should therefore be located at the `---` marker and must be
// emitted before the subsequent `DocumentStart(true)`.
// The synthetic `DocumentEnd` should be emitted before the subsequent
// `DocumentStart(true)`, but its span belongs to the end of the previous document, not to the
// following `---` marker.
let s = "foo\n---\nbar";

@@ -145,17 +146,17 @@ // 0123 456 789...

assert!(
matches!(next_ev, Event::DocumentStart(true)),
matches!(next_ev, Event::DocumentStart(true, None)),
"DocumentStart(true) should immediately follow DocumentEnd"
);
// DocumentEnd should be located at the `---` marker.
// DocumentEnd should be a zero-width span at the end of the previous document.
let doc_end_span = events[doc_end_idx].1;
assert_eq!(
doc_end_span.start.index(),
4,
"DocumentEnd should start at the '---' marker"
3,
"DocumentEnd should start at the end of the previous document"
);
assert_eq!(
doc_end_span.end.index(),
7,
"DocumentEnd should end right after the '---' marker"
3,
"DocumentEnd should end at the end of the previous document"
);

@@ -162,0 +163,0 @@

@@ -1,2 +0,2 @@

use granit_parser::{Event, Parser, ScalarStyle};
use granit_parser::{Event, Parser, ScalarStyle, StructureStyle};

@@ -22,2 +22,6 @@ fn scalar_value<'a>(ev: &'a Event<'_>) -> Option<&'a str> {

fn first_error_info(yaml: &str) -> Option<String> {
Parser::new_from_str(yaml).find_map(|event| event.err().map(|err| err.info().to_owned()))
}
#[test]

@@ -113,1 +117,49 @@ fn indentation_is_reported_for_block_mapping_keys_only() {

}
#[test]
fn root_block_sequence_can_have_anchor_on_previous_line() {
let yaml = "&anchor\n- a\n- b\n";
let events = Parser::new_from_str(yaml)
.map(|event| event.expect("valid yaml").0)
.collect::<Vec<_>>();
assert!(events
.iter()
.any(|event| matches!(event, Event::SequenceStart(StructureStyle::Block, 1, None))));
}
#[test]
fn indented_mapping_value_sequence_can_have_anchor_and_comment_on_previous_lines() {
let yaml = "seq:\n &anchor\n # c\n - a\n - b\n";
let events = Parser::new_from_str(yaml)
.map(|event| event.expect("valid yaml").0)
.collect::<Vec<_>>();
assert!(events
.iter()
.any(|event| matches!(event, Event::SequenceStart(StructureStyle::Block, 1, None))));
}
#[test]
fn unindented_mapping_value_sequence_after_anchor_is_rejected() {
assert_eq!(
first_error_info("seq:\n&anchor\n- a\n- b\n").as_deref(),
Some("simple key expected ':'")
);
}
#[test]
fn unindented_mapping_value_sequence_after_anchor_comment_is_rejected() {
assert_eq!(
first_error_info("seq:\n&anchor\n# c\n- a\n- b\n").as_deref(),
Some("simple key expected ':'")
);
}
#[test]
fn unindented_mapping_value_sequence_after_tag_comment_is_rejected() {
assert_eq!(
first_error_info("seq:\n!tag\n# c\n- a\n- b\n").as_deref(),
Some("simple key expected ':'")
);
}

@@ -109,3 +109,3 @@ use granit_parser::{

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Block),

@@ -125,3 +125,3 @@ map(Block),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -144,3 +144,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -162,3 +162,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -187,3 +187,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Block),

@@ -210,3 +210,3 @@ seq(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -228,3 +228,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -256,3 +256,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -287,3 +287,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -308,3 +308,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -328,3 +328,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -347,3 +347,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -367,3 +367,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Flow),

@@ -389,3 +389,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Flow),

@@ -407,3 +407,3 @@ Event::Scalar("foo".into(), ScalarStyle::DoubleQuoted, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Block),

@@ -431,3 +431,3 @@ map(Flow),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Block),

@@ -452,3 +452,3 @@ Event::Scalar("k".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
seq(Block),

@@ -513,3 +513,3 @@ Event::Scalar("a\n".into(), ScalarStyle::Literal, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Block),

@@ -546,3 +546,3 @@ Event::Scalar("foo".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(true),
Event::DocumentStart(true, None),
map(Block),

@@ -588,3 +588,3 @@ Event::Scalar("array".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Block),

@@ -623,3 +623,3 @@ Event::Scalar("comment".into(), ScalarStyle::Plain, 0, None),

(Event::StreamStart, Span::new(Marker::new(0, 1, 0).with_byte_offset(Some(0)), Marker::new(0, 1, 0).with_byte_offset(Some(0)))),
(Event::DocumentStart(true), Span::new(Marker::new(0, 1, 0).with_byte_offset(Some(0)), Marker::new(3, 1, 3).with_byte_offset(Some(3)))),
(Event::DocumentStart(true, None), Span::new(Marker::new(0, 1, 0).with_byte_offset(Some(0)), Marker::new(3, 1, 3).with_byte_offset(Some(3)))),
(map(Block), Span::new(Marker::new(8, 2, 4).with_byte_offset(Some(8)), Marker::new(8, 2, 4).with_byte_offset(Some(8)))),

@@ -674,3 +674,3 @@ (Event::Scalar("hash_block_null_value".into(), ScalarStyle::Plain, 0, None), Span::new(Marker::new(8, 2, 4).with_byte_offset(Some(8)), Marker::new(29, 2, 25).with_byte_offset(Some(29))).with_indent(Some(4))),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
map(Block),

@@ -713,3 +713,3 @@ Event::Scalar("hello".into(), ScalarStyle::Plain, 0, None),

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
seq(Block),

@@ -716,0 +716,0 @@ Event::Scalar("value".into(), ScalarStyle::Plain, 1, None),

@@ -20,3 +20,3 @@ use granit_parser::{Event, Parser, ScalarStyle, StructureStyle};

assert!(matches!(events[0], Event::StreamStart));
assert!(matches!(events[1], Event::DocumentStart(false)));
assert!(matches!(events[1], Event::DocumentStart(false, None)));
assert!(matches!(

@@ -58,3 +58,3 @@ events[2],

assert!(matches!(events[0], Event::StreamStart));
assert!(matches!(events[1], Event::DocumentStart(false)));
assert!(matches!(events[1], Event::DocumentStart(false, None)));
assert!(matches!(

@@ -96,3 +96,3 @@ events[2],

assert!(matches!(events[0], Event::StreamStart));
assert!(matches!(events[1], Event::DocumentStart(false)));
assert!(matches!(events[1], Event::DocumentStart(false, None)));
assert!(matches!(

@@ -99,0 +99,0 @@ events[2],

@@ -36,3 +36,3 @@ use granit_parser::{Event, Parser, ScalarStyle, ScanError, StructureStyle};

Event::StreamStart,
Event::DocumentStart(false),
Event::DocumentStart(false, None),
Event::MappingStart(StructureStyle::Block, 0, None),

@@ -39,0 +39,0 @@ Event::Scalar("k".into(), ScalarStyle::Plain, 0, None),

@@ -65,2 +65,14 @@ #![allow(clippy::bool_assert_comparison)]

fn first_event_slice(yaml: &str, matches_event: impl Fn(&Event<'_>) -> bool) -> Option<String> {
Parser::new_from_str(yaml)
.filter_map(Result::ok)
.find_map(|(event, span)| {
matches_event(&event).then(|| span.slice(yaml).map(str::to_owned))?
})
}
fn event_spans(yaml: &str) -> Vec<(Event<'_>, granit_parser::Span)> {
Parser::new_from_str(yaml).map(Result::unwrap).collect()
}
#[test]

@@ -87,2 +99,30 @@ fn span_helpers_report_length_empty_and_byte_range() {

#[test]
fn flow_collection_event_spans_cover_only_the_indicators() {
assert_eq!(
first_event_slice("[ # c\n a]\n", |event| matches!(
event,
Event::SequenceStart(..)
))
.as_deref(),
Some("[")
);
assert_eq!(
first_event_slice("[a] # c\n", |event| matches!(event, Event::SequenceEnd)).as_deref(),
Some("]")
);
assert_eq!(
first_event_slice("{ # c\n a: b}\n", |event| matches!(
event,
Event::MappingStart(..)
))
.as_deref(),
Some("{")
);
assert_eq!(
first_event_slice("{a: b} # c\n", |event| matches!(event, Event::MappingEnd)).as_deref(),
Some("}")
);
}
#[test]
fn tagged_block_collection_reports_tag_start_on_tag_line() {

@@ -219,2 +259,63 @@ let yaml = "key: !!omap # tag is here\n a: 1\n";

#[test]
fn tagged_empty_scalar_span_stays_at_tag_end() {
let yaml = "a: !!str\nb: c\n";
let events = event_spans(yaml);
let (_event, span) = events
.iter()
.find(|(event, _span)| {
matches!(
event,
Event::Scalar(value, _, _, Some(tag))
if value.is_empty() && tag.suffix == "str"
)
})
.expect("expected tagged empty scalar");
let tag_end = yaml.find('\n').unwrap();
let next_key = yaml.find("b:").unwrap();
assert_eq!(span.start.index(), tag_end);
assert_eq!(span.end.index(), tag_end);
assert_ne!(span.start.index(), next_key);
assert_eq!(span.slice(yaml), Some(""));
}
#[test]
fn implicit_document_end_span_stays_at_previous_document_end() {
let yaml = "foo\n--- bar\n";
let events = event_spans(yaml);
let (_event, span) = events
.iter()
.find(|(event, _span)| matches!(event, Event::DocumentEnd))
.expect("expected implicit document end");
let foo_end = yaml.find('\n').unwrap();
let next_marker = yaml.find("---").unwrap();
assert_eq!(span.start.index(), foo_end);
assert_eq!(span.end.index(), foo_end);
assert_ne!(span.start.index(), next_marker);
assert_eq!(span.slice(yaml), Some(""));
}
#[test]
fn block_missing_value_span_is_empty_at_next_token_start() {
let yaml = "? foo\nbar: baz\n";
let events = event_spans(yaml);
let empty_value_spans: Vec<_> = events
.iter()
.filter_map(|(event, span)| {
matches!(event, Event::Scalar(value, ..) if value == "~").then_some(*span)
})
.collect();
let next_key = yaml.find("bar").unwrap();
assert_eq!(empty_value_spans.len(), 1);
assert_eq!(empty_value_spans[0].start.index(), next_key);
assert_eq!(empty_value_spans[0].end.index(), next_key);
assert_eq!(empty_value_spans[0].slice(yaml), Some(""));
}
#[test]
fn span_slice_returns_none_for_buffered_input_spans_without_byte_offsets() {

@@ -221,0 +322,0 @@ let source = "foo: bar";

@@ -29,3 +29,3 @@ #![allow(dead_code)]

let tev = match ev {
Event::DocumentStart(_) => TestEvent::OnDocumentStart,
Event::DocumentStart(..) => TestEvent::OnDocumentStart,
Event::DocumentEnd => TestEvent::OnDocumentEnd,

@@ -32,0 +32,0 @@ Event::SequenceStart(..) => TestEvent::OnSequenceStart,

@@ -63,3 +63,3 @@ extern crate alloc;

Event::StreamEnd => "StreamEnd".to_string(),
Event::DocumentStart(_) => "DocStart".to_string(),
Event::DocumentStart(..) => "DocStart".to_string(),
Event::DocumentEnd => "DocEnd".to_string(),

@@ -145,2 +145,24 @@ Event::Comment(text, _) => alloc::format!("Comment({})", text.as_ref()),

#[test]
fn nested_parser_scan_error_after_document_end_is_propagated_verbatim() {
let mut stack: MyStack = ParserStack::new();
stack.push_str_parser(Parser::new_from_str("parent: value"), "parent".to_string());
stack.push_str_parser(
Parser::new_from_str("child: value\n...\n[\n"),
"child".to_string(),
);
loop {
match stack.next_event() {
Some(Ok(_)) => {}
Some(Err(err)) => {
assert_eq!(err.info(), "unclosed bracket '['");
assert_eq!(stack.stack(), vec!["parent".to_string()]);
break;
}
None => panic!("expected nested scan error"),
}
}
}
#[test]
fn test_two_parsers_first_has_multiple_docs_fine() {

@@ -518,3 +540,3 @@ let mut stack: MyStack = ParserStack::new();

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(Event::MappingStart(StructureStyle::Block, 0, None), span),

@@ -567,3 +589,3 @@ (

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(Event::MappingStart(StructureStyle::Block, 0, None), span),

@@ -609,3 +631,3 @@ (

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(Event::SequenceStart(StructureStyle::Block, 4, None), span),

@@ -652,6 +674,6 @@ (Event::SequenceEnd, span),

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("first", 0), span),
(Event::DocumentEnd, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("second", 0), span),

@@ -677,6 +699,6 @@ (Event::DocumentEnd, span),

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("first", 0), span),
(Event::DocumentEnd, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("second", 0), span),

@@ -702,3 +724,3 @@ (Event::DocumentEnd, span),

(Event::StreamStart, span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("first", 0), span),

@@ -731,3 +753,3 @@ (Event::DocumentEnd, span),

(Event::Comment(" replay".into(), Placement::Free), span),
(Event::DocumentStart(false), span),
(Event::DocumentStart(false, None), span),
(plain_scalar("value", 0), span),

@@ -826,2 +848,51 @@ (Event::DocumentEnd, span),

#[test]
fn parser_stack_push_str_after_exhaustion_reactivates_stack() {
let mut stack: MyStack = ParserStack::new();
while stack.next_event().is_some() {}
assert!(stack.peek().is_none());
stack.push_str_parser(Parser::new_from_str("b: 2"), "second".to_string());
let events = collect_events(&mut stack).unwrap();
assert_eq!(
format_events(&events),
vec![
"StreamStart",
"DocStart",
"MapStart",
"Scalar(b)",
"Scalar(2)",
"MapEnd",
"DocEnd",
"StreamEnd"
]
);
}
#[test]
fn parser_stack_push_after_peeked_empty_stream_end_reactivates_stack() {
let mut stack: MyStack = ParserStack::new();
assert!(matches!(stack.peek().unwrap().unwrap().0, Event::StreamEnd));
stack.push_str_parser(Parser::new_from_str("b: 2"), "second".to_string());
let events = collect_events(&mut stack).unwrap();
assert_eq!(
format_events(&events),
vec![
"StreamStart",
"DocStart",
"MapStart",
"Scalar(b)",
"Scalar(2)",
"MapEnd",
"DocEnd",
"StreamEnd"
]
);
}
#[test]
fn parser_stack_resolve_without_resolver_reports_error() {

@@ -873,2 +944,70 @@ let mut stack: MyStack = ParserStack::new();

#[test]
fn parser_stack_push_include_after_exhaustion_reactivates_stack() {
let mut stack: MyStack = ParserStack::new();
stack.set_resolver(|name| match name {
"child" => Ok("b: 2".to_string()),
_ => Err(ScanError::new(
Marker::new(0, 1, 0),
"Not found".to_string(),
)),
});
while stack.next_event().is_some() {}
assert!(stack.peek().is_none());
stack.push_include("child").unwrap();
let events = collect_events(&mut stack).unwrap();
assert_eq!(
format_events(&events),
vec![
"StreamStart",
"DocStart",
"MapStart",
"Scalar(b)",
"Scalar(2)",
"MapEnd",
"DocEnd",
"StreamEnd"
]
);
}
#[test]
fn multi_document_include_does_not_splice_into_parent() {
let mut stack: MyStack = ParserStack::new();
stack.push_str_parser(
Parser::new_from_str("root:\n inc: !include two.yaml\n after: tail\n"),
"root".to_string(),
);
stack.set_resolver(|_| Ok("x: 1\n---\ny: 2\n".to_string()));
let mut saw_error = false;
let mut spliced = false;
while let Some(result) = stack.next_event() {
match result {
Ok((Event::Scalar(value, _, _, Some(_)), _)) if value.as_ref() == "two.yaml" => {
stack.push_include(value.as_ref()).unwrap();
}
Ok((Event::Scalar(value, ..), _)) if value.as_ref() == "y" => {
spliced = true;
}
Err(err) => {
assert_eq!(err.info(), "multiple documents not supported here");
assert_eq!(stack.stack(), vec!["root".to_string()]);
saw_error = true;
}
_ => {}
}
}
assert!(saw_error);
assert!(
!spliced,
"second include document leaked into parent stream"
);
}
#[test]
fn iter_parser_inherits_anchor_offset_and_reports_stack() {

@@ -1022,2 +1161,49 @@ let mut stack: ParserStack<'static, alloc::vec::IntoIter<char>, StrInput<'static>> =

#[test]
fn parser_stack_error_survives_peek_until_next_event() {
let mut stack: MyStack = ParserStack::new();
stack.push_str_parser(Parser::new_from_str("a: [1, 2"), "bad".to_string());
loop {
match stack.peek() {
Some(Ok(_)) => {
stack.next_event().unwrap().unwrap();
}
Some(Err(err)) => {
assert_eq!(err.info(), "unclosed bracket '['");
break;
}
None => panic!("expected parse error before the stream ended"),
}
}
let second_peek_error = stack.peek().unwrap().unwrap_err();
assert_eq!(second_peek_error.info(), "unclosed bracket '['");
let next_error = stack.next_event().unwrap().unwrap_err();
assert_eq!(next_error.info(), "unclosed bracket '['");
assert!(stack.next_event().is_none());
assert!(stack.peek().is_none());
}
#[test]
fn parser_stack_next_event_error_is_terminal() {
let mut stack: MyStack = ParserStack::new();
stack.push_str_parser(Parser::new_from_str("a: [1, 2"), "bad".to_string());
loop {
match stack.next_event() {
Some(Ok(_)) => {}
Some(Err(err)) => {
assert_eq!(err.info(), "unclosed bracket '['");
break;
}
None => panic!("expected parse error before the stream ended"),
}
}
assert!(stack.next_event().is_none());
assert!(stack.peek().is_none());
}
#[test]
fn nested_replay_stream_end_is_popped_and_parent_continues() {

@@ -1024,0 +1210,0 @@ let span = test_span();

@@ -383,3 +383,3 @@ use std::{

Event::DocumentStart(_) => "+DOC".into(),
Event::DocumentStart(..) => "+DOC".into(),
Event::DocumentEnd => "-DOC".into(),

@@ -525,3 +525,9 @@

s if s.starts_with("+MAP {}") => s.replacen("+MAP {}", "+MAP", 1),
"=VAL :" => "=VAL :~".into(), // FIXME: known bug
// The parser represents untagged missing/null scalars as a plain "~" event.
// Normalize the YAML test-suite's canonical empty-content form to that local
// convention, including anchored nodes.
"=VAL :" => "=VAL :~".into(),
s if s.starts_with("=VAL &") && !s.contains('<') && s.ends_with(" :") => {
format!("{s}~")
}
s => s.into(),

@@ -528,0 +534,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display