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
58#[cfg(feature = "proc-macro2")]
59impl From<Location> for proc_macro2::LineColumn {
60    fn from(value: Location) -> Self {
61        Self { line: value.line.get() as usize, column: value.col as usize }
62    }
63}
64
65impl Default for Location {
66    fn default() -> Self {
67        Self {
68            line: nonzero!(1),
69            col: 0,
70        }
71    }
72}
73
74impl Display for Location {
75    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
76        write!(f, "{}:{}", self.line, self.col)
77    }
78}
79
80impl Location {
81    /// Returns the [`Location`] that's calculated with `self` as the base & `rhs` as the offset
82    /// from the base.
83    ///
84    /// # Panics
85    /// Panics if the line or column overflow a `u32`
86    #[must_use]
87    pub const fn offset(self, rhs: Self) -> Self {
88        if rhs.line.get() == 1 {
89            Self {
90                line: self.line,
91                col: self.col + rhs.col,
92            }
93        } else {
94            Self {
95                line: NonZero::new(self.line.get() + rhs.line.get() - 1).expect("no overflow"),
96                col: rhs.col,
97            }
98        }
99    }
100
101    /// Turn a [`Location`] into a [`FullLocation`] by providing the file path.
102    pub fn with_path<'path>(self, path: impl PathLike<'path>) -> FullLocation<'path> {
103        FullLocation {
104            path: path.into_path_bytes(),
105            loc: self,
106        }
107    }
108
109    /// Locates address `ptr` in `src` and returns its source code location, or None if `ptr` is
110    /// outside of the memory range of `src`.
111    pub fn find(ptr: *const u8, src: &str) -> Option<Self> {
112        let progress =
113            usize::checked_sub(ptr as _, src.as_ptr() as _).filter(|x| *x <= src.len())?;
114
115        Some(
116            src.bytes()
117                .take(progress)
118                .fold(Self::default(), |loc, b| match b {
119                    b'\n' => Self {
120                        line: loc.line.saturating_add(1),
121                        col: 0,
122                    },
123                    _ => Self {
124                        col: loc.col.saturating_add(1),
125                        ..loc
126                    },
127                }),
128        )
129    }
130
131    /// Same as [`find`](Self::find), except for the `None` case:
132    /// - If `ptr` is before `src`, the returned location points to the beginning of `src`.
133    /// - If `ptr` is after `src`, the returned location points to the end of `src`.
134    ///
135    /// This function is used by [`crate::ParsingError::with_src_loc`]
136    pub fn find_saturating(ptr: *const u8, src: &str) -> Self {
137        let progress = usize::saturating_sub(ptr as _, src.as_ptr() as _);
138
139        let res = src
140            .bytes()
141            .take(progress)
142            .fold(Self::default(), |loc, b| match b {
143                b'\n' => Self {
144                    line: loc.line.saturating_add(1),
145                    col: 0,
146                },
147                _ => Self {
148                    col: loc.col.saturating_add(1),
149                    ..loc
150                },
151            });
152        res
153    }
154
155    /// Same as [`find`](Self::find), but searches in multiple "files".
156    ///
157    /// A file, per definition of this function, is a key `K` that identifies it,
158    /// and a memory range that is its content.
159    /// The function returns the key of the file where `ptr` is contained, or `None` if no files
160    /// matched.
161    /// ```rust
162    /// # fn main() {
163    /// use std::collections::HashMap;
164    /// use shrimple_parser::{Location, nonzero, tuple::copied};
165    ///
166    /// let file2 = "          \n\nfn main() { panic!() }";
167    /// let sources = HashMap::from([
168    ///     ("file1.rs", r#"fn main() { println!("Hiiiii!!!!! :3") }"#),
169    ///     ("file2.rs", file2),
170    /// ]);
171    /// let no_ws = file2.trim();
172    /// assert_eq!(
173    ///     Location::find_in_multiple(no_ws.as_ptr(), sources.iter().map(copied)),
174    ///     Some(("file2.rs", Location { line: nonzero!(3), col: 0 })),
175    /// )
176    /// # }
177    /// ```
178    /// Also see [`tuple::copied`], [`nonzero`]
179    pub fn find_in_multiple<K>(
180        ptr: *const u8,
181        files: impl IntoIterator<Item = (K, impl AsRef<str>)>,
182    ) -> Option<(K, Self)> {
183        files
184            .into_iter()
185            .find_map(|(k, src)| Some((k, Self::find(ptr, src.as_ref())?)))
186    }
187}
188
189/// Like [`Location`], but also stores the path to the file.
190#[derive(Debug, Clone, PartialEq, Eq, Hash)]
191pub struct FullLocation<'path> {
192    /// The path to the file associated with the location.
193    pub path: Cow<'path, [u8]>,
194    /// The line & column numbers of the location.
195    pub loc: Location,
196}
197
198impl Display for FullLocation<'_> {
199    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
200        for chunk in self.path.utf8_chunks() {
201            f.write_str(chunk.valid())?;
202            if !chunk.invalid().is_empty() {
203                f.write_char(REPLACEMENT_CHARACTER)?;
204            }
205        }
206        write!(f, ":{}", self.loc)
207    }
208}
209
210impl FullLocation<'_> {
211    /// Unbind the location from the lifetimes by allocating the path if it hasn't been already.
212    pub fn own(self) -> FullLocation<'static> {
213        FullLocation {
214            path: self.path.into_owned().into(),
215            loc: self.loc,
216        }
217    }
218}