shrimple_parser/
loc.rs

1extern crate alloc;
2
3use {
4    crate::{nonzero, utils::PathLike},
5    alloc::borrow::Cow,
6    core::{
7        char::REPLACEMENT_CHARACTER,
8        fmt::{Display, Formatter, Write},
9        num::NonZero,
10    },
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14/// Location of the error. Useful for error reporting, and used by [`crate::FullParsingError`]
15pub struct Location {
16    /// Source code line of the location.
17    pub line: NonZero<u32>,
18    /// Source code column of the location.
19    pub col: u32,
20}
21
22/// The error returned when converting a [`proc_macro2::LineColumn`] to [`Location`].
23#[cfg(feature = "proc-macro2")]
24#[derive(Debug, Clone, Copy)]
25pub enum LineColumnToLocationError {
26    /// Line 0 was encountered, which is invalid, source lines are 1-indexed.
27    LineZero,
28    /// Line number overflowed a u32.
29    LineNumberTooBig,
30    /// Column number overflowed a u32.
31    ColumnNumberTooBig,
32}
33
34#[cfg(feature = "proc-macro2")]
35impl Display for LineColumnToLocationError {
36    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
37        f.write_str(match self {
38            Self::LineZero => "`LineColumn` with line #0 found",
39            Self::LineNumberTooBig => "`LineColumn`s line number overflowed a u32",
40            Self::ColumnNumberTooBig => "`LineColumn`s column number overflowed a u32",
41        })
42    }
43}
44
45#[cfg(feature = "proc-macro2")]
46impl TryFrom<proc_macro2::LineColumn> for Location {
47    type Error = LineColumnToLocationError;
48
49    fn try_from(value: proc_macro2::LineColumn) -> Result<Self, Self::Error> {
50        let line = u32::try_from(value.line).map_err(|_| LineColumnToLocationError::LineNumberTooBig)?;
51        let line = NonZero::new(line).ok_or(LineColumnToLocationError::LineZero)?;
52        let col = u32::try_from(value.column).map_err(|_| LineColumnToLocationError::ColumnNumberTooBig)?;
53
54        Ok(Self { line, col })
55    }
56}
57
58impl Default for Location {
59    fn default() -> Self {
60        Self {
61            line: nonzero!(1),
62            col: 0,
63        }
64    }
65}
66
67impl Display for Location {
68    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
69        write!(f, "{}:{}", self.line, self.col)
70    }
71}
72
73impl Location {
74    /// Returns the [`Location`] that's calculated with `self` as the base & `rhs` as the offset
75    /// from the base.
76    ///
77    /// # Panics
78    /// Panics if the line or column overflow a `u32`
79    #[must_use]
80    pub const fn offset(self, rhs: Self) -> Self {
81        if rhs.line.get() == 1 {
82            Self {
83                line: self.line,
84                col: self.col - rhs.col,
85            }
86        } else {
87            Self {
88                line: NonZero::new(self.line.get() - rhs.line.get() + 1).expect("no overflow"),
89                col: rhs.col,
90            }
91        }
92    }
93
94    /// Turn a [`Location`] into a [`FullLocation`] by providing the file path.
95    pub fn with_path<'path>(self, path: impl PathLike<'path>) -> FullLocation<'path> {
96        FullLocation {
97            path: path.into_path_bytes(),
98            loc: self,
99        }
100    }
101
102    /// Locates address `ptr` in `src` and returns its source code location, or None if `ptr` is
103    /// outside of the memory range of `src`.
104    pub fn find(ptr: *const u8, src: &str) -> Option<Self> {
105        let progress =
106            usize::checked_sub(ptr as _, src.as_ptr() as _).filter(|x| *x <= src.len())?;
107
108        Some(
109            src.bytes()
110                .take(progress)
111                .fold(Self::default(), |loc, b| match b {
112                    b'\n' => Self {
113                        line: loc.line.saturating_add(1),
114                        col: 0,
115                    },
116                    _ => Self {
117                        col: loc.col.saturating_add(1),
118                        ..loc
119                    },
120                }),
121        )
122    }
123
124    /// Same as [`find`](Self::find), except for the `None` case:
125    /// - If `ptr` is before `src`, the returned location points to the beginning of `src`.
126    /// - If `ptr` is after `src`, the returned location points to the end of `src`.
127    ///
128    /// This function is used by [`crate::ParsingError::with_src_loc`]
129    pub fn find_saturating(ptr: *const u8, src: &str) -> Self {
130        let progress = usize::saturating_sub(ptr as _, src.as_ptr() as _);
131
132        let res = src
133            .bytes()
134            .take(progress)
135            .fold(Self::default(), |loc, b| match b {
136                b'\n' => Self {
137                    line: loc.line.saturating_add(1),
138                    col: 0,
139                },
140                _ => Self {
141                    col: loc.col.saturating_add(1),
142                    ..loc
143                },
144            });
145        res
146    }
147
148    /// Same as [`find`](Self::find), but searches in multiple "files".
149    ///
150    /// A file, per definition of this function, is a key `K` that identifies it,
151    /// and a memory range that is its content.
152    /// The function returns the key of the file where `ptr` is contained, or `None` if no files
153    /// matched.
154    /// ```rust
155    /// # fn main() {
156    /// use std::collections::HashMap;
157    /// use shrimple_parser::{Location, nonzero, tuple::copied};
158    ///
159    /// let file2 = "          \n\nfn main() { panic!() }";
160    /// let sources = HashMap::from([
161    ///     ("file1.rs", r#"fn main() { println!("Hiiiii!!!!! :3") }"#),
162    ///     ("file2.rs", file2),
163    /// ]);
164    /// let no_ws = file2.trim();
165    /// assert_eq!(
166    ///     Location::find_in_multiple(no_ws.as_ptr(), sources.iter().map(copied)),
167    ///     Some(("file2.rs", Location { line: nonzero!(3), col: 0 })),
168    /// )
169    /// # }
170    /// ```
171    /// Also see [`tuple::copied`], [`nonzero`]
172    pub fn find_in_multiple<K>(
173        ptr: *const u8,
174        files: impl IntoIterator<Item = (K, impl AsRef<str>)>,
175    ) -> Option<(K, Self)> {
176        files
177            .into_iter()
178            .find_map(|(k, src)| Some((k, Self::find(ptr, src.as_ref())?)))
179    }
180}
181
182/// Like [`Location`], but also stores the path to the file.
183#[derive(Debug, Clone, PartialEq, Eq, Hash)]
184pub struct FullLocation<'path> {
185    /// The path to the file associated with the location.
186    pub path: Cow<'path, [u8]>,
187    /// The line & column numbers of the location.
188    pub loc: Location,
189}
190
191impl Display for FullLocation<'_> {
192    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
193        for chunk in self.path.utf8_chunks() {
194            f.write_str(chunk.valid())?;
195            if !chunk.invalid().is_empty() {
196                f.write_char(REPLACEMENT_CHARACTER)?;
197            }
198        }
199        write!(f, ":{}", self.loc)
200    }
201}
202
203impl FullLocation<'_> {
204    /// Unbind the location from the lifetimes by allocating the path if it hasn't been already.
205    pub fn own(self) -> FullLocation<'static> {
206        FullLocation {
207            path: self.path.into_owned().into(),
208            loc: self.loc,
209        }
210    }
211}