Skip to main content

ion_rs/
location.rs

1use crate::Span;
2use std::cell::RefCell;
3use std::rc::Rc;
4
5/// Represents the source location (row, column) of this element in the original Ion text.
6///
7/// The source location metadata is primarily intended for error reporting and debugging purposes,
8/// helping applications provide meaningful feedback to users about the source of issues.
9#[derive(Debug, Clone, Copy, Eq, PartialEq, Default)]
10pub struct SourceLocation {
11    /// A 1-based row and column pair.
12    /// INVARIANT: both components must be `0` or both must be non-zero.
13    location: (usize, usize),
14}
15
16impl SourceLocation {
17    /// Constructs a new SourceLocation. If either of `row` or `column` is `0`, returns an instance
18    /// with no row/column value (i.e. both row and column are zero). This maintains the invariant
19    /// that the location field must be `(0, 0)` or must have two non-zero values.
20    pub(crate) fn new(row: usize, column: usize) -> SourceLocation {
21        if row == 0 || column == 0 {
22            return Self::empty();
23        }
24        Self {
25            location: (row, column),
26        }
27    }
28
29    /// Constructs a new `SourceLocation` instance that has no row/column available.
30    pub(crate) fn empty() -> SourceLocation {
31        Self { location: (0, 0) }
32    }
33
34    /// If this `SourceLocation` instance has row-column information, returns a tuple containing
35    /// the 1-based row and column numbers. Otherwise, returns [`None`].
36    pub fn row_column(&self) -> Option<(usize, usize)> {
37        match self.location {
38            (0, 0) => None,
39            other => Some(other),
40        }
41    }
42
43    /// If this `SourceLocation` instance has row-column information, returns the 1-based row number.
44    /// Otherwise, returns [`None`].
45    pub fn row(&self) -> Option<usize> {
46        match self.location {
47            (0, 0) => None,
48            (row, _) => Some(row),
49        }
50    }
51
52    /// If this `SourceLocation` instance has row-column information, returns the 1-based column number.
53    /// Otherwise, returns [`None`].
54    pub fn column(&self) -> Option<usize> {
55        match self.location {
56            (0, 0) => None,
57            (_, col) => Some(col),
58        }
59    }
60}
61
62#[cfg(test)]
63mod source_location_tests {
64    use crate::location::SourceLocation;
65    #[test]
66    fn empty_source_location() {
67        let location = SourceLocation::empty();
68        assert_eq!(None, location.row_column());
69        assert_eq!(None, location.row());
70        assert_eq!(None, location.column());
71    }
72
73    #[test]
74    fn non_empty_source_location() {
75        let location = SourceLocation::new(2, 3);
76        assert_eq!(Some((2, 3)), location.row_column());
77        assert_eq!(Some(2), location.row());
78        assert_eq!(Some(3), location.column());
79    }
80}
81
82/// Encapsulates location tracking state and functionality.
83///
84/// This struct is cheap to clone because all of its state is behind a reference-counted pointer.
85#[derive(Debug, Clone)]
86pub(crate) struct SourceLocationState {
87    /// A non-empty vec containing the offset of the start of each row.
88    /// The first row always starts at offset 0.
89    row_start_offsets: Rc<RefCell<Vec<usize>>>,
90}
91impl SourceLocationState {
92    pub fn new() -> Self {
93        Self {
94            row_start_offsets: Rc::from(RefCell::new(vec![0])),
95        }
96    }
97
98    /// Updates the location tracking state from the given source data.
99    pub fn update_from_source<T: AsRef<[u8]>>(&mut self, stream_offset: usize, data: T) {
100        let data = data.as_ref();
101        if !data.is_empty() {
102            let newlines = memchr::memchr_iter(b'\n', data);
103            self.row_start_offsets
104                .borrow_mut()
105                .extend(newlines.map(|it| it + stream_offset + 1));
106        }
107    }
108
109    pub fn calculate_location_for_span(&self, span: Span<'_>) -> SourceLocation {
110        let range = span.range();
111        let (row, row_start_offset) = self
112            .row_start_offsets
113            .borrow()
114            .iter()
115            .copied()
116            .enumerate()
117            .rfind(|&(_, newline_offset)| newline_offset <= range.start)
118            // Always safe to unwrap because of the invariant that `newlines` is never empty.
119            .unwrap();
120        let column = range.start - row_start_offset;
121        // Both of these are 0-based counts, and must be incremented to be 1-based row/column
122        SourceLocation::new(row + 1, column + 1)
123    }
124}