Skip to main content

shrimple_parser/
loc.rs

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