From 271376023d0849f5b8223ef911d0b285f2f69253 Mon Sep 17 00:00:00 2001 From: jskoiz <20649937+jskoiz@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:25:32 -1000 Subject: [PATCH] Bound multiline flow span metadata --- src/parse.rs | 156 ++++++++++++++++++++++++++++++++++++------- tests/diagnostics.rs | 25 +++++++ 2 files changed, 158 insertions(+), 23 deletions(-) diff --git a/src/parse.rs b/src/parse.rs index 9cbdbc8..6f63914 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -5410,16 +5410,45 @@ impl SourceMark { } } +const MAX_FLOW_SOURCE_SEGMENTS: usize = 4096; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct FlowSourceSegment { + start: usize, + end: usize, + start_mark: SourceMark, + end_mark: SourceMark, +} + +impl FlowSourceSegment { + fn mark_at(&self, position: usize) -> SourceMark { + debug_assert!(position >= self.start && position <= self.end); + let relative = position.saturating_sub(self.start); + if relative >= self.end - self.start { + return self.end_mark; + } + SourceMark::new( + self.start_mark.offset + relative, + self.start_mark.line, + self.start_mark.column + relative, + ) + } +} + struct FlowBuffer { text: String, - marks: Vec, + segments: Vec, + tail: Option, + overflow: Option, } impl FlowBuffer { fn single(text: &str, line: &Line, local_start: usize) -> Self { let mut buffer = Self { text: String::new(), - marks: Vec::with_capacity(text.len() + 1), + segments: Vec::new(), + tail: None, + overflow: None, }; buffer.push_source_text(text, line, local_start); buffer @@ -5429,38 +5458,96 @@ impl FlowBuffer { if text.is_empty() { return; } - let start = SourceMark::for_line_content(line, local_start); - if self.marks.is_empty() { - self.marks.push(start); - } + let start = self + .tail + .unwrap_or_else(|| SourceMark::for_line_content(line, local_start)); + let end = SourceMark::new( + start.offset + text.len(), + start.line, + start.column + text.len(), + ); + self.push_segment(text.len(), start, end); self.text.push_str(text); - for offset in 1..=text.len() { - self.marks.push(SourceMark::new( - start.offset + offset, - line.no(), - line.indent() + local_start + offset + 1, - )); - } + self.tail = Some(end); } fn push_virtual_separator(&mut self, next: SourceMark) { if self.text.is_empty() { - self.marks.push(next); + self.tail = Some(next); return; } + let start = self.tail.expect("flow source text has a trailing mark"); + self.push_segment(1, start, next); self.text.push('\n'); - self.marks.push(next); + self.tail = Some(next); } - fn span(&self, start: usize, end: usize) -> Span { - // `start`/`end` are byte positions into `self.text`; the `marks` vector - // always carries one extra trailing mark, but guard with checked access - // and clamp to the last valid mark so malformed offsets cannot panic. - let Some(last) = self.marks.last().copied() else { - return Span::point(0, 1, 1); + fn push_segment(&mut self, len: usize, start_mark: SourceMark, end_mark: SourceMark) { + let segment = FlowSourceSegment { + start: self.text.len(), + end: self.text.len() + len, + start_mark, + end_mark, }; - let start = self.marks.get(start).copied().unwrap_or(last); - let end = self.marks.get(end).copied().unwrap_or(last); + if self.overflow.is_some() { + return; + } + if self.segments.len() < MAX_FLOW_SOURCE_SEGMENTS { + self.segments.push(segment); + } else { + // Keep the exact prefix and a bounded fallback boundary. The + // parser still retains the normalized flow text, while spans in a + // metadata overflow region resolve to this safe source location + // until the exact final tail mark is requested. + self.overflow = Some(segment); + } + } + + fn mark_at(&self, position: usize) -> SourceMark { + let position = position.min(self.text.len()); + if position == self.text.len() { + return self + .tail + .or_else(|| self.segments.last().map(|segment| segment.end_mark)) + .unwrap_or_else(|| SourceMark::new(0, 1, 1)); + } + if let Some(overflow) = self.overflow + && position >= overflow.start + { + return overflow.start_mark; + } + + let mut low = 0usize; + let mut high = self.segments.len(); + while low < high { + let middle = low + (high - low) / 2; + if self.segments[middle].start <= position { + low = middle + 1; + } else { + high = middle; + } + } + let Some(segment) = self + .segments + .get(low.saturating_sub(1)) + .filter(|segment| position <= segment.end) + else { + return self + .tail + .or_else(|| self.segments.last().map(|segment| segment.end_mark)) + .unwrap_or_else(|| SourceMark::new(0, 1, 1)); + }; + segment.mark_at(position) + } + + #[cfg(test)] + fn metadata_len(&self) -> usize { + self.segments.len() + usize::from(self.overflow.is_some()) + } + + fn span(&self, start: usize, end: usize) -> Span { + let start = self.mark_at(start); + let end = self.mark_at(end); Span::new(start.offset, end.offset, start.line, start.column) } } @@ -6604,4 +6691,27 @@ mod tests { parser.parser.lines.max_retained_len() ); } + + #[test] + fn flow_source_metadata_is_bounded_for_multiline_input() { + let line_count = MAX_FLOW_SOURCE_SEGMENTS * 2; + let mut input = String::from("[\n"); + for _ in 0..line_count { + input.push_str("a\n"); + } + + let first_line = preprocess_line(&input, 1, 0, 1).expect("first flow line"); + let mut buffer = FlowBuffer::single("[", &first_line, 0); + let mut line_start = 2usize; + for line_no in 2..=(line_count + 1) { + let line = preprocess_line(&input, line_no, line_start, 1).expect("flow line"); + buffer.push_virtual_separator(SourceMark::for_line_content(&line, 0)); + buffer.push_source_text(line.content(&input), &line, 0); + line_start += 2; + } + + assert!(buffer.overflow.is_some(), "test must exercise metadata cap"); + assert!(buffer.metadata_len() <= MAX_FLOW_SOURCE_SEGMENTS + 1); + assert!(buffer.segments.capacity() <= MAX_FLOW_SOURCE_SEGMENTS); + } } diff --git a/tests/diagnostics.rs b/tests/diagnostics.rs index 70d1ec2..e0602b7 100644 --- a/tests/diagnostics.rs +++ b/tests/diagnostics.rs @@ -397,6 +397,31 @@ fn parser_spans_q9wf_flow_key_and_block_value_mapping() { ); } +#[test] +fn multiline_flow_scalar_spans_keep_physical_source_locations() { + let input = "root: [\n first,\n second\n]\n"; + let events = saneyaml::parse_events(input).expect("multiline flow parses"); + let second = events + .iter() + .find_map(|event| match event { + saneyaml::Event::Scalar { value, span, .. } if value == "second" => Some(span), + _ => None, + }) + .expect("second scalar event"); + + assert_exact_span( + second, + input, + &ExpectedSpan { + line: 3, + column: 3, + source: "second", + }, + "multiline flow scalar", + "parse_events", + ); +} + #[test] fn parser_spans_remaining_tree_deferral_properties() { let pw8x = include_str!("fixtures/yaml-test-suite/data/PW8X/in.yaml");