Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 133 additions & 23 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SourceMark>,
segments: Vec<FlowSourceSegment>,
tail: Option<SourceMark>,
overflow: Option<FlowSourceSegment>,
}

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
Expand All @@ -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)
}
}
Expand Down Expand Up @@ -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);
}
}
25 changes: 25 additions & 0 deletions tests/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading