sup_xml_core/error.rs
1#![forbid(unsafe_code)] // see CONTRIBUTING.md § "Unsafe policy"
2
3use std::fmt;
4
5/// Which subsystem raised the error.
6///
7/// Mirrors `xmlErrorDomain` from libxml2 (`include/libxml/xmlerror.h`) so that
8/// errors can be categorised without inspecting the message string, AND so
9/// that `domain as i32` produces the exact numeric value a C caller sees
10/// through `xmlError::domain` after going through the [`crates/compat`] FFI
11/// shim. See `thoughts/c_abi_implementation_plan.md` § "Unified Rust error
12/// type, free conversion to libxml2 layout."
13///
14/// Variant discriminants are pinned — **do not renumber** — they are part
15/// of the C ABI surface.
16#[repr(i32)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ErrorDomain {
19 /// No domain — used by libxml2 as the default value for fields it hasn't
20 /// initialised. We rarely emit this from Rust; included for round-trip.
21 None = 0, // XML_FROM_NONE
22 /// XML parser (well-formedness violations, unexpected tokens, etc.).
23 Parser = 1, // XML_FROM_PARSER
24 /// Document tree manipulation.
25 Tree = 2, // XML_FROM_TREE
26 /// Namespace resolution (undeclared prefix, duplicate declarations, etc.).
27 Namespace = 3, // XML_FROM_NAMESPACE
28 /// DTD processing and entity expansion.
29 Dtd = 4, // XML_FROM_DTD
30 /// HTML parsing errors (malformed tag soup, recovered errors from the
31 /// HTML5 tree-construction algorithm).
32 Html = 5, // XML_FROM_HTML
33 /// I/O errors (file not found, read failure, etc.).
34 Io = 8, // XML_FROM_IO
35 /// XPath expression parsing or evaluation.
36 XPath = 12, // XML_FROM_XPATH
37 /// XSLT processing — stylesheet compile, transform errors.
38 Xslt = 22, // XML_FROM_XSLT
39 /// W3C XML Schema (XSD) validation. lxml's `error.domain_name` →
40 /// `"SCHEMASV"`.
41 SchemasValidate = 17, // XML_FROM_SCHEMASV
42 /// RELAX NG validation. lxml's `error.domain_name` → `"RELAXNGV"`.
43 RelaxNGValidate = 19, // XML_FROM_RELAXNGV
44 /// Schematron validation. lxml's `error.domain_name` → `"SCHEMATRONV"`.
45 SchematronValidate = 28, // XML_FROM_SCHEMATRONV
46 /// Schema or DTD validation.
47 Validation = 23, // XML_FROM_VALID
48 /// Character encoding errors (invalid UTF-8, unsupported encoding).
49 /// libxml2 calls this domain "I18N" but it's the encoding/charset
50 /// error bucket.
51 Encoding = 27, // XML_FROM_I18N
52}
53
54/// Severity of the error.
55///
56/// `Warning < Error < Fatal`. Fatal errors abort processing; warnings and
57/// errors may still produce a partial document.
58///
59/// Discriminants match libxml2's `xmlErrorLevel`.
60#[repr(i32)]
61#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
62pub enum ErrorLevel {
63 /// `XML_ERR_NONE` (0) is a no-error sentinel libxml2 sometimes emits;
64 /// callers should treat it as "not actually an error." We don't
65 /// construct it from Rust.
66 None = 0,
67 Warning = 1, // XML_ERR_WARNING
68 Error = 2, // XML_ERR_ERROR
69 Fatal = 3, // XML_ERR_FATAL
70}
71
72/// One of libxml2's well-known numeric error codes.
73///
74/// libxml2's `xmlParserErrors` enum has ~800 variants; this type covers
75/// the ~40 we actually emit from the parser, validator, encoder, etc.
76/// Everything else lands at [`ErrorCode::InternalError`] (`= 1`), which
77/// is what libxml2 itself uses for unmapped cases.
78///
79/// Discriminants are pinned to match libxml2's `include/libxml/xmlerror.h`
80/// exactly — **do not renumber.** A C caller doing
81/// `if (err->code == XML_ERR_INVALID_CHAR) { ... }` sees `9` here too,
82/// because `ErrorCode::InvalidChar as i32 == 9`.
83///
84/// # When to add a new variant
85///
86/// When you have a new error-construction site that maps to a specific
87/// libxml2 code that callers genuinely check for. When in doubt, default
88/// to [`ErrorCode::InternalError`]. Adding a variant is additive (callers
89/// reading numeric codes are unaffected) but renumbering an existing
90/// variant is a breaking ABI change.
91#[repr(i32)]
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum ErrorCode {
95 // ── general (xmlParserErrors) ────────────────────────────────────────
96 /// No error — round-trip sentinel. We don't emit this from Rust.
97 Ok = 0, // XML_ERR_OK
98 /// Default for any error we can't classify more specifically.
99 /// libxml2 itself uses this for unhandled cases, so callers
100 /// branching on specific codes will already have a "default" arm
101 /// that handles us.
102 InternalError = 1, // XML_ERR_INTERNAL_ERROR
103 /// Out-of-memory at parse / build time. Not currently emitted (we
104 /// panic on OOM today), reserved for when we add fallible alloc.
105 NoMemory = 2, // XML_ERR_NO_MEMORY
106 /// Document is empty (no root element, no prolog).
107 DocumentEmpty = 4, // XML_ERR_DOCUMENT_EMPTY
108
109 // ── well-formedness violations (the ones consumers check) ───────────
110 /// `&#x...;` malformed — empty hex, non-hex digit, etc.
111 InvalidHexCharRef = 6, // XML_ERR_INVALID_HEX_CHARREF
112 /// `&#...;` malformed — empty decimal, non-digit, etc.
113 InvalidDecCharRef = 7, // XML_ERR_INVALID_DEC_CHARREF
114 /// Character outside XML 1.0 § 2.2 char range.
115 InvalidChar = 9, // XML_ERR_INVALID_CHAR
116 /// Entity reference to an undeclared entity name.
117 UndeclaredEntity = 26, // XML_ERR_UNDECLARED_ENTITY
118 /// Unknown encoding label on `<?xml encoding="..."?>` or BOM mismatch.
119 UnknownEncoding = 31, // XML_ERR_UNKNOWN_ENCODING
120 /// Encoding declared but not supported by the build.
121 UnsupportedEncoding = 32, // XML_ERR_UNSUPPORTED_ENCODING
122
123 // ── attribute / element structure ───────────────────────────────────
124 /// Two attributes with the same expanded name on one element.
125 AttributeRedefined = 42, // XML_ERR_ATTRIBUTE_REDEFINED
126 /// Comment didn't terminate before EOF.
127 CommentNotFinished = 45, // XML_ERR_COMMENT_NOT_FINISHED
128 /// Bad `<?xml ... ?>` declaration syntax.
129 XmlDeclNotStarted = 56, // XML_ERR_XMLDECL_NOT_STARTED
130 XmlDeclNotFinished = 57, // XML_ERR_XMLDECL_NOT_FINISHED
131 /// `]]>` in text content, where it's reserved for CDATA close.
132 MisplacedCdataEnd = 62, // XML_ERR_MISPLACED_CDATA_END
133 /// CDATA section didn't terminate before EOF.
134 CdataNotFinished = 63, // XML_ERR_CDATA_NOT_FINISHED
135 /// Expected an XML name (start tag, attribute name, entity name, etc.).
136 NameRequired = 68, // XML_ERR_NAME_REQUIRED
137 /// Expected `=` after attribute name.
138 EqualRequired = 75, // XML_ERR_EQUAL_REQUIRED
139 /// `</X>` end-tag name doesn't match the open `<Y>`.
140 TagNameMismatch = 76, // XML_ERR_TAG_NAME_MISMATCH
141 /// Start tag never closed before EOF.
142 TagNotFinished = 77, // XML_ERR_TAG_NOT_FINISHED
143 /// Document is not well-balanced (open tags don't match close tags
144 /// at EOF). General catch-all when we can't say which tag.
145 NotWellBalanced = 85, // XML_ERR_NOT_WELL_BALANCED
146 /// Content after the root element's close.
147 ExtraContent = 86, // XML_ERR_EXTRA_CONTENT
148
149 // ── namespace ────────────────────────────────────────────────────────
150 /// Prefix used but never declared via `xmlns:prefix=...`.
151 NsErrUndefinedNamespace = 201, // XML_NS_ERR_UNDEFINED_NAMESPACE
152 /// Malformed QName (e.g. multiple colons).
153 NsErrQname = 202, // XML_NS_ERR_QNAME
154
155 // ── I/O (xmlErrorDomain::Io) ────────────────────────────────────────
156 /// A document input (the main file or an external entity) could not
157 /// be opened or read. libxml2's `__xmlLoaderErr` reports exactly
158 /// this code with domain [`ErrorDomain::Io`]; consumers that key on
159 /// the domain (e.g. lxml's `_raiseParseError`) then raise an I/O
160 /// error rather than a syntax error.
161 IoLoadError = 1549, // XML_IO_LOAD_ERROR
162
163 // ── XSD schema validation (xmlSchemaValidError) ─────────────────────
164 // Codes consumers (e.g. lxml's `error_log.filter_types`) match on to
165 // classify a validation failure.
166 /// Attribute/element value invalid against its datatype.
167 SchemavCvcDatatypeValid121 = 1824, // XML_SCHEMAV_CVC_DATATYPE_VALID_1_2_1
168 /// A value rejected by a facet (pattern, length, range, …).
169 SchemavCvcFacetValid = 1829, // XML_SCHEMAV_CVC_FACET_VALID
170 /// An attribute not permitted by the element's complex type.
171 SchemavCvcComplexType322 = 1867, // XML_SCHEMAV_CVC_COMPLEX_TYPE_3_2_2
172 /// A required attribute is missing.
173 SchemavCvcComplexType4 = 1868, // XML_SCHEMAV_CVC_COMPLEX_TYPE_4
174 /// Element content doesn't match the declared content model
175 /// (unexpected child element, or a required one absent).
176 SchemavElementContent = 1871, // XML_SCHEMAV_ELEMENT_CONTENT
177
178 // ── DTD validation (xmlParserErrors) ────────────────────────────────
179 /// An element declared `EMPTY` has child content.
180 DtdNotEmpty = 528, // XML_DTD_NOT_EMPTY
181
182 // ── RELAX NG validation (xmlRelaxNGValidErr) ────────────────────────
183 /// An element appeared where the pattern did not expect it.
184 RelaxngErrElemwrong = 38, // XML_RELAXNG_ERR_ELEMWRONG
185
186 // ── encoding (xmlErrorDomain::I18n) ─────────────────────────────────
187 /// Encoding handler couldn't decode the input bytes.
188 EncodingConvFailed = 6003, // XML_I18N_CONV_FAILED
189}
190
191/// A structured XML processing error.
192///
193/// SupXML returns `XmlError` through `Result<_, XmlError>` instead of
194/// relying on a global error variable like libxml2 does. The `domain`,
195/// `level`, and `code` fields let callers react without parsing the
196/// human-readable `message`.
197///
198/// `code` is an [`ErrorCode`] enum whose discriminants match libxml2's
199/// `xmlParserErrors` numeric values. Callers can match on the enum
200/// (idiomatic Rust); the [`crates/compat`] cdylib converts to libxml2's
201/// `xmlError::code: i32` via `err.code as i32` — zero cost. See
202/// `thoughts/c_abi_implementation_plan.md` for the design.
203#[derive(Debug, Clone)]
204pub struct XmlError {
205 /// Which subsystem produced the error.
206 pub domain: ErrorDomain,
207 /// Severity.
208 pub level: ErrorLevel,
209 /// Specific error category (libxml2-compatible numeric code on
210 /// the wire side). When in doubt, [`ErrorCode::InternalError`].
211 pub code: ErrorCode,
212 /// Human-readable description of the problem.
213 pub message: String,
214 /// Source file name, if available (e.g. for file-based parsing).
215 pub file: Option<String>,
216 /// 1-based line number where the error occurred, if known.
217 pub line: Option<u32>,
218 /// 1-based column number where the error occurred, if known.
219 pub column: Option<u32>,
220 /// 0-based byte offset into the parser's input buffer where the
221 /// error occurred, if known.
222 ///
223 /// Reported alongside [`line`](Self::line) / [`column`](Self::column)
224 /// because the three answer different questions: line/col is what
225 /// a human reads, byte offset is what tools (editors, LSP servers,
226 /// `dd if=… bs=1 skip=…`) act on without re-walking the input.
227 /// Byte offset also survives line-ending normalization (XML 1.0
228 /// § 2.11) and is the only useful coordinate for binary
229 /// pipelines — gzipped XML, network captures, mmap'd files.
230 ///
231 /// `u64` (not `usize`) so that the ABI surface in `crates/compat`
232 /// is stable across 32- and 64-bit targets and survives documents
233 /// larger than 4 GB on the streaming reader.
234 ///
235 /// # Coordinate system
236 ///
237 /// The offset is measured in the parser's **internal UTF-8
238 /// buffer**, which is the same as the caller's input byte slice
239 /// in the common case (input was already UTF-8). If
240 /// [`ParseOptions::auto_transcode`](crate::options::ParseOptions)
241 /// converted UTF-16 or another encoding to UTF-8 first, the
242 /// offset is relative to the post-transcode buffer and does
243 /// **not** point at the user's original bytes; the user-facing
244 /// offset would require a transcoder back-map we don't have
245 /// today. Callers operating on already-UTF-8 input — which is
246 /// the overwhelming majority of XML on the wire — can use this
247 /// directly.
248 pub byte_offset: Option<u64>,
249 /// XPath/XQuery/XSLT error code as a local name in the standard
250 /// `err:` namespace (`http://www.w3.org/2005/xqt-errors`) — e.g.
251 /// `"FOAR0001"` for division by zero, `"FORG0001"` for an invalid
252 /// cast. Distinct from [`code`](Self::code), which is the
253 /// libxml2-numeric category; this is the spec-defined dynamic
254 /// error a stylesheet's `xsl:catch` / `try/catch` matches on and
255 /// exposes through `$err:code`. `None` when the error has no
256 /// specific spec code (it then projects as the generic
257 /// `err:FOER0000`).
258 pub xpath_code: Option<String>,
259}
260
261impl XmlError {
262 /// Construct an error with the catch-all
263 /// [`ErrorCode::InternalError`] code. Add a more specific code
264 /// via [`with_code`](Self::with_code) when there's a libxml2
265 /// numeric value that fits the case.
266 pub fn new(domain: ErrorDomain, level: ErrorLevel, message: impl Into<String>) -> Self {
267 Self {
268 domain,
269 level,
270 code: ErrorCode::InternalError,
271 message: message.into(),
272 file: None,
273 line: None,
274 column: None,
275 byte_offset: None,
276 xpath_code: None,
277 }
278 }
279
280 /// Attach a specific [`ErrorCode`] (libxml2-numeric). Builder-style;
281 /// returns `self` for chaining with [`at`](Self::at).
282 pub fn with_code(mut self, code: ErrorCode) -> Self {
283 self.code = code;
284 self
285 }
286
287 /// Attach the spec-defined XPath/XSLT error code (an `err:` local
288 /// name such as `"FOAR0001"`). Builder-style; see
289 /// [`xpath_code`](Self::xpath_code).
290 pub fn with_xpath_code(mut self, code: impl Into<String>) -> Self {
291 self.xpath_code = Some(code.into());
292 self
293 }
294
295 /// Attach `code` only if no spec code is already present. Used at
296 /// outer choke points (e.g. `document()` retrieval) that want to
297 /// label an otherwise-uncoded error without overwriting a more
298 /// specific code an inner layer already set.
299 pub fn or_xpath_code(mut self, code: impl Into<String>) -> Self {
300 if self.xpath_code.is_none() {
301 self.xpath_code = Some(code.into());
302 }
303 self
304 }
305
306 /// Attach source position. All three coordinates are taken
307 /// together because the scanner derives them from a single byte
308 /// offset and any error that knows one knows all three;
309 /// `byte_offset` is documented on [`Self::byte_offset`].
310 pub fn at(
311 mut self,
312 file: impl Into<String>,
313 line: u32,
314 column: u32,
315 byte_offset: u64,
316 ) -> Self {
317 self.file = Some(file.into());
318 self.line = Some(line);
319 self.column = Some(column);
320 self.byte_offset = Some(byte_offset);
321 self
322 }
323}
324
325impl fmt::Display for XmlError {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 // `file:line:col:` is the conventional editor-clickable
328 // prefix. Byte offset goes in a trailing `@N` because it
329 // breaks that convention if injected in the middle.
330 match (&self.file, self.line, self.column) {
331 (Some(file), Some(line), Some(col)) => write!(f, "{file}:{line}:{col}: ")?,
332 (Some(file), Some(line), None) => write!(f, "{file}:{line}: ")?,
333 _ => {}
334 }
335 write!(f, "[{:?}/{:?}] {}", self.domain, self.level, self.message)?;
336 if let Some(ofs) = self.byte_offset {
337 write!(f, " @ byte {ofs}")?;
338 }
339 Ok(())
340 }
341}
342
343impl std::error::Error for XmlError {}
344
345/// Convenience alias used throughout SupXML — equivalent to
346/// `std::result::Result<T, XmlError>`.
347pub type Result<T> = std::result::Result<T, XmlError>;
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn error_display() {
355 let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "unexpected EOF")
356 .at("doc.xml", 42, 7, 1024);
357 let s = e.to_string();
358 assert!(s.contains("doc.xml"));
359 assert!(s.contains("42"));
360 assert!(s.contains("7"));
361 assert!(s.contains("unexpected EOF"));
362 assert!(s.contains("byte 1024"));
363 }
364
365 #[test]
366 fn error_level_ordering() {
367 assert!(ErrorLevel::Warning < ErrorLevel::Error);
368 assert!(ErrorLevel::Error < ErrorLevel::Fatal);
369 }
370
371 /// Verifies that `ErrorCode` discriminants match libxml2's
372 /// `xmlParserErrors` numeric values exactly — the contract that
373 /// makes `err.code as i32` a zero-cost FFI conversion. Drift here
374 /// breaks libsupxml2's caller-facing codes silently.
375 ///
376 /// Spot-check the codes most consumers care about; the full set
377 /// is documented inline in the enum.
378 /// Pinned discriminants — one row per variant so a renumbering bug
379 /// (the kind that caused [`NsErrUndefinedNamespace`] to ship at 502
380 /// instead of 201, silently breaking C consumers comparing
381 /// `err->code == XML_NS_ERR_UNDEFINED_NAMESPACE`) lights up here
382 /// instead of leaking through the FFI surface. Numbers checked
383 /// against `xmlParserErrors` in
384 /// `/opt/homebrew/Cellar/libxml2/<version>/include/libxml2/libxml/xmlerror.h`.
385 #[test]
386 fn error_code_libxml2_values_match() {
387 assert_eq!(ErrorCode::Ok as i32, 0);
388 assert_eq!(ErrorCode::InternalError as i32, 1);
389 assert_eq!(ErrorCode::NoMemory as i32, 2);
390 assert_eq!(ErrorCode::DocumentEmpty as i32, 4);
391 assert_eq!(ErrorCode::InvalidHexCharRef as i32, 6);
392 assert_eq!(ErrorCode::InvalidDecCharRef as i32, 7);
393 assert_eq!(ErrorCode::InvalidChar as i32, 9);
394 assert_eq!(ErrorCode::UndeclaredEntity as i32, 26);
395 assert_eq!(ErrorCode::UnknownEncoding as i32, 31);
396 assert_eq!(ErrorCode::UnsupportedEncoding as i32, 32);
397 assert_eq!(ErrorCode::AttributeRedefined as i32, 42);
398 assert_eq!(ErrorCode::CommentNotFinished as i32, 45);
399 assert_eq!(ErrorCode::XmlDeclNotStarted as i32, 56);
400 assert_eq!(ErrorCode::XmlDeclNotFinished as i32, 57);
401 assert_eq!(ErrorCode::MisplacedCdataEnd as i32, 62);
402 assert_eq!(ErrorCode::CdataNotFinished as i32, 63);
403 assert_eq!(ErrorCode::NameRequired as i32, 68);
404 assert_eq!(ErrorCode::EqualRequired as i32, 75);
405 assert_eq!(ErrorCode::TagNameMismatch as i32, 76);
406 assert_eq!(ErrorCode::TagNotFinished as i32, 77);
407 assert_eq!(ErrorCode::NotWellBalanced as i32, 85);
408 assert_eq!(ErrorCode::ExtraContent as i32, 86);
409 assert_eq!(ErrorCode::NsErrUndefinedNamespace as i32, 201);
410 assert_eq!(ErrorCode::NsErrQname as i32, 202);
411 assert_eq!(ErrorCode::IoLoadError as i32, 1549);
412 assert_eq!(ErrorCode::EncodingConvFailed as i32, 6003);
413 }
414
415 /// Pins every [`ErrorDomain`] discriminant against libxml2's
416 /// `xmlErrorDomain` (xmlerror.h). Same drift-protection role as
417 /// [`error_code_libxml2_values_match`].
418 ///
419 /// We model **11 of 31** libxml2 domains. The 20 absent values
420 /// stay free at their libxml2 numbers so adding them later
421 /// requires no renumbering:
422 ///
423 /// | # | libxml2 name | rationale to add |
424 /// |---:|------------------------|------------------|
425 /// | 6 | XML_FROM_MEMORY | alloc failures |
426 /// | 7 | XML_FROM_OUTPUT | serializer I/O |
427 /// | 9 | XML_FROM_FTP | (deprecated) |
428 /// | 10 | XML_FROM_HTTP | network fetch |
429 /// | 11 | XML_FROM_XINCLUDE | XInclude |
430 /// | 13 | XML_FROM_XPOINTER | XPointer |
431 /// | 14 | XML_FROM_REGEXP | XSD §F regex |
432 /// | 15 | XML_FROM_DATATYPE | XSD types |
433 /// | 16 | XML_FROM_SCHEMASP | schema parse |
434 /// | 17 | XML_FROM_SCHEMASV | schema validate |
435 /// | 18 | XML_FROM_RELAXNGP | RelaxNG parse |
436 /// | 19 | XML_FROM_RELAXNGV | RelaxNG validate |
437 /// | 20 | XML_FROM_CATALOG | XML Catalogs |
438 /// | 21 | XML_FROM_C14N | canonicalisation |
439 /// | 24 | XML_FROM_CHECK | tree integrity |
440 /// | 25 | XML_FROM_WRITER | xmlTextWriter |
441 /// | 26 | XML_FROM_MODULE | xmlModule |
442 /// | 28 | XML_FROM_SCHEMATRONV | Schematron |
443 /// | 29 | XML_FROM_BUFFER | xmlBuffer |
444 /// | 30 | XML_FROM_URI | URI parse |
445 ///
446 /// Numbers verified against
447 /// `/opt/homebrew/Cellar/libxml2/<version>/include/libxml2/libxml/xmlerror.h`.
448 #[test]
449 fn error_domain_libxml2_values_match() {
450 assert_eq!(ErrorDomain::None as i32, 0);
451 assert_eq!(ErrorDomain::Parser as i32, 1);
452 assert_eq!(ErrorDomain::Tree as i32, 2);
453 assert_eq!(ErrorDomain::Namespace as i32, 3);
454 assert_eq!(ErrorDomain::Dtd as i32, 4);
455 assert_eq!(ErrorDomain::Html as i32, 5);
456 assert_eq!(ErrorDomain::Io as i32, 8);
457 assert_eq!(ErrorDomain::XPath as i32, 12);
458 assert_eq!(ErrorDomain::Xslt as i32, 22);
459 assert_eq!(ErrorDomain::Validation as i32, 23);
460 assert_eq!(ErrorDomain::Encoding as i32, 27);
461 }
462
463 #[test]
464 fn xml_error_carries_default_code() {
465 let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "boom");
466 assert_eq!(e.code, ErrorCode::InternalError);
467 }
468
469 #[test]
470 fn xml_error_with_code_chains() {
471 let e = XmlError::new(ErrorDomain::Parser, ErrorLevel::Fatal, "bad char")
472 .with_code(ErrorCode::InvalidChar)
473 .at("doc.xml", 1, 2, 5);
474 assert_eq!(e.code, ErrorCode::InvalidChar);
475 assert_eq!(e.code as i32, 9);
476 assert_eq!(e.line, Some(1));
477 assert_eq!(e.column, Some(2));
478 assert_eq!(e.byte_offset, Some(5));
479 }
480}