docs and line/offset numbers in paths

This commit is contained in:
Jana Dönszelmann 2026-04-02 09:41:35 +02:00
parent af09bcd403
commit 2d9a029130
No known key found for this signature in database
9 changed files with 366 additions and 116 deletions

View file

@ -97,8 +97,25 @@ impl Separator {
}
}
impl<'a> FileLocation<'a> {
fn parse<E: ParserError<&'a str>>() -> impl Parser<&'a str, Self, E> {
use winnow::{ascii::dec_uint, combinator::*, prelude::*};
let colon_number = || {
(":".value(()), dec_uint::<_, u64, _>.take())
.map(|(_, number): (_, &str)| Cow::Borrowed(number))
};
let line_offset = (colon_number(), opt(colon_number()));
trace(
"file location",
line_offset.map(|(line, offset)| FileLocation { line, offset }),
)
}
}
impl<'a> FileName<'a> {
fn parse(segment: PathSegment<'a>) -> Self {
fn parse(segment: PathSegment<'a>, location: Option<FileLocation<'a>>) -> Self {
fn rsplit<'a>(
input: Cow<'a, str>,
delimiter: char,
@ -113,34 +130,11 @@ impl<'a> FileName<'a> {
}
}
let (rest, location) = if let Some((before, offset)) = rsplit(segment.segment.clone(), ':')
{
if let Some((before, line)) = rsplit(before.clone(), ':') {
(
before,
Some(FileLocation {
line,
offset: Some(offset),
}),
)
} else {
(
before,
Some(FileLocation {
line: offset,
offset: None,
}),
)
}
} else {
(segment.segment, None)
};
let (new_segment, ext_excluding_dot) =
if let Some((segment, ext_excluding_dot)) = rsplit(rest.clone(), '.') {
if let Some((segment, ext_excluding_dot)) = rsplit(segment.segment.clone(), '.') {
(segment, Some(ext_excluding_dot))
} else {
(rest, None)
(segment.segment, None)
};
Self {
@ -202,12 +196,13 @@ impl<'a> Path<'a> {
opt_sep_and_next,
repeat_till(0.., sep_and_next, peek(terminator()))
.map(|(segments, _): (Vec<PathSegment>, _)| segments),
opt(FileLocation::parse()),
);
trace(
"path",
drive_and_segments
.map(|(drive, segment, segments)| {
.map(|(drive, segment, segments, location)| {
let (segments, last) = {
let mut segments = segments;
segments.insert(0, segment);
@ -215,7 +210,7 @@ impl<'a> Path<'a> {
(segments, last)
};
let filename = FileName::parse(last);
let filename = FileName::parse(last, location);
Self {
drive_excluding_colon: drive,
@ -225,7 +220,6 @@ impl<'a> Path<'a> {
})
.verify(|i| {
!i.segments.is_empty()
|| i.drive_excluding_colon.is_some()
|| i.filename.ext_excluding_dot.is_some()
|| !matches!(i.filename.leading_separator, PathSep::None)
}),
@ -266,8 +260,8 @@ impl<'a> Token<'a> {
"true".value(Self::True),
"false".value(Self::False),
"None".value(Self::None),
Path::parse().map(Self::Path),
Number::parse().map(Self::Number),
Path::parse().map(Self::Path),
AnyString::parse().map(Self::String),
delimited,
Atom::parse(alt((Separator::parse().value(""), ")", "]", "}"))).map(Self::Atom),
@ -318,12 +312,10 @@ impl<'a> Delimited<'a> {
trace(
"delimited",
(
opt(Atom::parse(alt((
"(",
"[",
"{",
Separator::parse().value(""),
)))),
opt((
Atom::parse(alt(("(", "[", "{", Separator::parse().value("")))),
Space::parse(),
)),
alt((
literal('(').map(|_| literal(')').value(Delimiter::Paren)),
literal('[').map(|_| literal(']').value(Delimiter::Bracket)),
@ -392,6 +384,12 @@ impl<'a> Segment<'a> {
}
}
/// Parses an input string (a log line) into an ast.
///
/// This *should* never error.
/// Many tests ensure that arbitrary input can be parsed.
/// Even if non-structured or completely random.
/// The parser will gracefully accept such strings anyway, and just categorize them suboptimally.
pub fn parse_input<'a>(i: &'a str) -> Result<Segments<'a>, String> {
use winnow::combinator::eof;
Segments::parse(eof::<&str, winnow::error::EmptyError>)
@ -405,7 +403,7 @@ mod tests {
use insta::assert_debug_snapshot;
use winnow::Parser;
use crate::format_debug_output::{
use crate::{
ast::{Path, Segments},
parse_input,
};