Skip to content

Commit 80a98ec

Browse files
committed
feat(error): offset-based span tracking
New methods `error()` and `error_at()` attach the current offset so callers get location info in error messages.
1 parent be57c48 commit 80a98ec

3 files changed

Lines changed: 66 additions & 25 deletions

File tree

‎src/patch/error.rs‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
//! Error types for patch parsing.
22
33
use std::fmt;
4+
use std::ops::Range;
45

56
/// An error returned when parsing a `Patch` using [`Patch::from_str`] fails.
67
///
78
/// [`Patch::from_str`]: struct.Patch.html#method.from_str
89
#[derive(Debug, Clone, PartialEq, Eq)]
910
pub struct ParsePatchError {
1011
pub(crate) kind: ParsePatchErrorKind,
12+
span: Option<Range<usize>>,
13+
}
14+
15+
impl ParsePatchError {
16+
/// Creates a new error with the given kind and span.
17+
pub(crate) fn new(kind: ParsePatchErrorKind, span: Range<usize>) -> Self {
18+
Self {
19+
kind,
20+
span: Some(span),
21+
}
22+
}
23+
24+
/// Returns the byte range in the input where the error occurred.
25+
pub fn span(&self) -> Option<Range<usize>> {
26+
self.span.clone()
27+
}
1128
}
1229

1330
impl fmt::Display for ParsePatchError {
1431
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15-
write!(f, "error parsing patch: {}", self.kind)
32+
if let Some(span) = &self.span {
33+
write!(
34+
f,
35+
"error parsing patch at byte {}: {}",
36+
span.start, self.kind
37+
)
38+
} else {
39+
write!(f, "error parsing patch: {}", self.kind)
40+
}
1641
}
1742
}
1843

1944
impl std::error::Error for ParsePatchError {}
2045

2146
impl From<ParsePatchErrorKind> for ParsePatchError {
2247
fn from(kind: ParsePatchErrorKind) -> Self {
23-
Self { kind }
48+
Self { kind, span: None }
2449
}
2550
}
2651

‎src/patch/parse.rs‎

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,28 @@ impl<'a, T: Text + ?Sized> Parser<'a, T> {
2929
self.lines.peek()
3030
}
3131

32+
fn offset(&self) -> usize {
33+
self.offset
34+
}
35+
3236
fn next(&mut self) -> Result<&'a T> {
3337
let line = self
3438
.lines
3539
.next()
36-
.ok_or(ParsePatchErrorKind::UnexpectedEof)?;
40+
.ok_or_else(|| self.error(ParsePatchErrorKind::UnexpectedEof))?;
3741
self.offset += line.len();
3842
Ok(line)
3943
}
4044

45+
/// Creates an error with the current offset as span.
46+
fn error(&self, kind: ParsePatchErrorKind) -> ParsePatchError {
47+
ParsePatchError::new(kind, self.offset..self.offset)
48+
}
49+
50+
/// Creates an error with a specific offset as span.
51+
fn error_at(&self, kind: ParsePatchErrorKind, offset: usize) -> ParsePatchError {
52+
ParsePatchError::new(kind, offset..offset)
53+
}
4154
}
4255

4356
pub fn parse(input: &str) -> Result<Patch<'_, str>> {
@@ -80,12 +93,12 @@ fn patch_header<'a, T: Text + ToOwned + ?Sized>(
8093
while let Some(line) = parser.peek() {
8194
if line.starts_with("--- ") {
8295
if filename1.is_some() {
83-
return Err(ParsePatchErrorKind::MultipleOriginalHeaders.into());
96+
return Err(parser.error(ParsePatchErrorKind::MultipleOriginalHeaders));
8497
}
8598
filename1 = Some(parse_filename("--- ", parser.next()?)?);
8699
} else if line.starts_with("+++ ") {
87100
if filename2.is_some() {
88-
return Err(ParsePatchErrorKind::MultipleModifiedHeaders.into());
101+
return Err(parser.error(ParsePatchErrorKind::MultipleModifiedHeaders));
89102
}
90103
filename2 = Some(parse_filename("+++ ", parser.next()?)?);
91104
} else {
@@ -149,20 +162,23 @@ fn hunks<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Vec<Hunk<'a
149162

150163
// check and verify that the Hunks are in sorted order and don't overlap
151164
if !verify_hunks_in_order(&hunks) {
152-
return Err(ParsePatchErrorKind::HunksOutOfOrder.into());
165+
return Err(parser.error(ParsePatchErrorKind::HunksOutOfOrder));
153166
}
154167

155168
Ok(hunks)
156169
}
157170

158171
fn hunk<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Hunk<'a, T>> {
159-
let (range1, range2, function_context) = hunk_header(parser.next()?)?;
172+
let hunk_start = parser.offset();
173+
let header_line = parser.next()?;
174+
let (range1, range2, function_context) =
175+
hunk_header(header_line).map_err(|e| parser.error_at(e.kind, hunk_start))?;
160176
let lines = hunk_lines(parser)?;
161177

162178
// check counts of lines to see if they match the ranges in the hunk header
163179
let (len1, len2) = super::hunk_lines_count(&lines);
164180
if len1 != range1.len || len2 != range2.len {
165-
return Err(ParsePatchErrorKind::HunkMismatch.into());
181+
return Err(parser.error_at(ParsePatchErrorKind::HunkMismatch, hunk_start));
166182
}
167183

168184
Ok(Hunk::new(range1, range2, function_context, lines))
@@ -217,25 +233,25 @@ fn hunk_lines<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Vec<Li
217233
let line = if line.starts_with("@") {
218234
break;
219235
} else if no_newline_context {
220-
return Err(ParsePatchErrorKind::ExpectedEndOfHunk.into());
236+
return Err(parser.error(ParsePatchErrorKind::ExpectedEndOfHunk));
221237
} else if let Some(line) = line.strip_prefix(" ") {
222238
Line::Context(line)
223239
} else if line.starts_with("\n") {
224240
Line::Context(*line)
225241
} else if let Some(line) = line.strip_prefix("-") {
226242
if no_newline_delete {
227-
return Err(ParsePatchErrorKind::TooManyDeletedLines.into());
243+
return Err(parser.error(ParsePatchErrorKind::TooManyDeletedLines));
228244
}
229245
Line::Delete(line)
230246
} else if let Some(line) = line.strip_prefix("+") {
231247
if no_newline_insert {
232-
return Err(ParsePatchErrorKind::TooManyInsertedLines.into());
248+
return Err(parser.error(ParsePatchErrorKind::TooManyInsertedLines));
233249
}
234250
Line::Insert(line)
235251
} else if line.starts_with(NO_NEWLINE_AT_EOF) {
236252
let last_line = lines
237253
.pop()
238-
.ok_or(ParsePatchErrorKind::UnexpectedNoNewlineMarker)?;
254+
.ok_or_else(|| parser.error(ParsePatchErrorKind::UnexpectedNoNewlineMarker))?;
239255
match last_line {
240256
Line::Context(line) => {
241257
no_newline_context = true;
@@ -251,7 +267,7 @@ fn hunk_lines<'a, T: Text + ?Sized>(parser: &mut Parser<'a, T>) -> Result<Vec<Li
251267
}
252268
}
253269
} else {
254-
return Err(ParsePatchErrorKind::UnexpectedHunkLine.into());
270+
return Err(parser.error(ParsePatchErrorKind::UnexpectedHunkLine));
255271
};
256272

257273
lines.push(line);

‎src/patch/tests.rs‎

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ fn test_escaped_filenames() {
2121
+Oathbringer
2222
";
2323
assert_eq!(
24-
parse(s).unwrap_err(),
25-
ParsePatchErrorKind::InvalidCharInUnquotedFilename.into(),
24+
parse(s).unwrap_err().kind,
25+
ParsePatchErrorKind::InvalidCharInUnquotedFilename,
2626
);
2727
parse_bytes(s.as_ref()).unwrap_err();
2828

@@ -34,8 +34,8 @@ fn test_escaped_filenames() {
3434
+Oathbringer
3535
";
3636
assert_eq!(
37-
parse(s).unwrap_err(),
38-
ParsePatchErrorKind::InvalidUnescapedChar.into(),
37+
parse(s).unwrap_err().kind,
38+
ParsePatchErrorKind::InvalidUnescapedChar,
3939
);
4040
parse_bytes(s.as_ref()).unwrap_err();
4141

@@ -193,8 +193,8 @@ fn escaped_filename_octal() {
193193
+content
194194
"#;
195195
assert_eq!(
196-
parse(s).unwrap_err(),
197-
ParsePatchErrorKind::InvalidEscapedChar.into(),
196+
parse(s).unwrap_err().kind,
197+
ParsePatchErrorKind::InvalidEscapedChar,
198198
);
199199

200200
// Non-octal digit in second position → error
@@ -205,8 +205,8 @@ fn escaped_filename_octal() {
205205
+content
206206
"#;
207207
assert_eq!(
208-
parse(s).unwrap_err(),
209-
ParsePatchErrorKind::InvalidEscapedChar.into(),
208+
parse(s).unwrap_err().kind,
209+
ParsePatchErrorKind::InvalidEscapedChar,
210210
);
211211
}
212212

@@ -267,8 +267,8 @@ fn test_missing_filename_header() {
267267
+Oathbringer
268268
";
269269
assert_eq!(
270-
parse(s).unwrap_err(),
271-
ParsePatchErrorKind::MultipleOriginalHeaders.into(),
270+
parse(s).unwrap_err().kind,
271+
ParsePatchErrorKind::MultipleOriginalHeaders,
272272
);
273273
}
274274

@@ -396,7 +396,7 @@ mod error_display {
396396
let err = Patch::from_str(content).unwrap_err();
397397
assert_data_eq!(
398398
err.to_string(),
399-
str!["error parsing patch: unable to parse hunk header"]
399+
str!["error parsing patch at byte 28: unable to parse hunk header"]
400400
);
401401
}
402402

@@ -412,7 +412,7 @@ mod error_display {
412412
let err = Patch::from_str(content).unwrap_err();
413413
assert_data_eq!(
414414
err.to_string(),
415-
str!["error parsing patch: hunk header does not match hunk"]
415+
str!["error parsing patch at byte 28: hunk header does not match hunk"]
416416
);
417417
}
418418

0 commit comments

Comments
 (0)