lang_util/
located.rs

1//! Located type definition
2
3use std::{
4    fmt,
5    path::{Path, PathBuf},
6};
7
8use text_size::{TextRange, TextSize};
9
10use super::FileId;
11
12/// Represents a file location override
13#[derive(Debug, Clone, PartialEq, Eq, derive_more::From)]
14pub enum FileOverride {
15    /// No override
16    None,
17    /// Override with a raw file number
18    Number(u32),
19    /// Override with a path
20    Path(String),
21}
22
23impl FileOverride {
24    /// Return true if this file override is empty
25    pub fn is_none(&self) -> bool {
26        matches!(self, Self::None)
27    }
28}
29
30impl Default for FileOverride {
31    fn default() -> Self {
32        Self::None
33    }
34}
35
36impl fmt::Display for FileOverride {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            FileOverride::None => Ok(()),
40            FileOverride::Number(number) => write!(f, "{}", number),
41            FileOverride::Path(path) => write!(f, "{}", path),
42        }
43    }
44}
45
46/// Trait for objects that can resolve offsets to line and column numbers
47pub trait Resolver {
48    /// Resolve the raw offset into a (line, column) tuple
49    ///
50    /// # Parameters
51    ///
52    /// * `offset`: raw offset into the source string
53    fn resolve(&self, offset: TextSize) -> (u32, u32);
54}
55
56impl<'s> Resolver for &'s str {
57    fn resolve(&self, offset: TextSize) -> (u32, u32) {
58        let offset: usize = offset.into();
59        let offset = if offset >= self.len() {
60            self.len().max(1) - 1
61        } else {
62            offset
63        };
64
65        // Find line start offset
66        let line_start = line_span::find_line_start(self, offset);
67
68        // Count newlines
69        let line_index = self
70            .bytes()
71            .take(line_start)
72            .filter(|c| *c == b'\n')
73            .count();
74
75        // Find column
76        let pos_index = offset - line_start;
77
78        (line_index as _, pos_index as _)
79    }
80}
81
82/// Trait for objects that can return the current file number
83pub trait HasFileNumber {
84    /// Return the current file identifier
85    fn current_file(&self) -> FileId;
86}
87
88/// Trait for resolving file identifiers to file names
89pub trait FileIdResolver {
90    /// Return the path corresponding to the FileId, if known
91    fn resolve(&self, file_id: FileId) -> Option<&Path>;
92}
93
94/// Builder for a [Located] struct
95#[derive(Default)]
96pub struct LocatedBuilder {
97    /// Position at which the error occurred
98    pos: TextRange,
99    /// File identifier for the error
100    current_file: Option<FileId>,
101    /// Path corresponding to the file identifier
102    path: Option<PathBuf>,
103    /// Overriden file location
104    file_override: FileOverride,
105    /// Resolved line number
106    line_number: u32,
107    /// Resolved column number
108    column: u32,
109}
110
111impl LocatedBuilder {
112    /// Create a new builder for [Located] with default values
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// Set the raw position
118    pub fn pos(self, pos: impl Into<TextRange>) -> Self {
119        Self {
120            pos: pos.into(),
121            ..self
122        }
123    }
124
125    /// Set the file identifier
126    pub fn current_file(self, file: impl Into<FileId>) -> Self {
127        Self {
128            current_file: Some(file.into()),
129            ..self
130        }
131    }
132
133    /// Set the source path
134    pub fn path(self, path: impl Into<PathBuf>) -> Self {
135        Self {
136            path: Some(path.into()),
137            ..self
138        }
139    }
140
141    /// Set the source file override
142    pub fn file_override(self, file_override: impl Into<FileOverride>) -> Self {
143        Self {
144            file_override: file_override.into(),
145            ..self
146        }
147    }
148
149    /// Set the resolved line number
150    pub fn line_number(self, line_number: u32) -> Self {
151        Self {
152            line_number,
153            ..self
154        }
155    }
156
157    /// Set the resolved column number
158    pub fn column(self, column: u32) -> Self {
159        Self { column, ..self }
160    }
161
162    /// Resolve the raw offset (see [LocatedBuilder::pos]) to line and column information
163    pub fn resolve(self, resolver: &impl Resolver) -> Self {
164        let (line, col) = resolver.resolve(self.pos.start());
165        Self {
166            line_number: line,
167            column: col,
168            ..self
169        }
170    }
171
172    /// Resolve the raw offset (see [LocatedBuilder::pos]) to line and column information, and set
173    /// the current file information
174    pub fn resolve_file(self, resolver: &(impl Resolver + HasFileNumber)) -> Self {
175        self.resolve(resolver).current_file(resolver.current_file())
176    }
177
178    /// Resolve the given file id into a path name, and store it in this builder
179    pub fn resolve_path(self, resolver: &impl FileIdResolver) -> Self {
180        Self {
181            path: self
182                .current_file
183                .and_then(|current_file| resolver.resolve(current_file).map(Path::to_owned)),
184            ..self
185        }
186    }
187
188    /// Build the final [Located] object from the given inner object
189    pub fn finish<E>(self, inner: E) -> Located<E> {
190        Located {
191            inner,
192            pos: self.pos,
193            current_file: self.current_file,
194            path: self.path,
195            file_override: self.file_override,
196            line_number: self.line_number,
197            column: self.column,
198        }
199    }
200}
201
202/// Wraps an object with location data
203#[derive(Debug)]
204pub struct Located<E> {
205    /// Inner error, without location information
206    inner: E,
207    /// Position at which the error occurred
208    pos: TextRange,
209    /// File identifier for the error
210    current_file: Option<FileId>,
211    /// Path corresponding to the file identifier
212    path: Option<PathBuf>,
213    /// Overriden file location
214    file_override: FileOverride,
215    /// Resolved line number
216    line_number: u32,
217    /// Resolved column number
218    column: u32,
219}
220
221impl<E> Located<E> {
222    /// Create a builder for this located type
223    pub fn builder() -> LocatedBuilder {
224        LocatedBuilder::default()
225    }
226
227    /// Transform the inner value wrapped by this instance
228    ///
229    /// # Parameters
230    ///
231    /// * `f`: function to apply to the inner value
232    pub fn map<F>(self, f: impl FnOnce(E) -> F) -> Located<F> {
233        Located {
234            inner: f(self.inner),
235            pos: self.pos,
236            current_file: self.current_file,
237            path: self.path,
238            file_override: self.file_override,
239            line_number: self.line_number,
240            column: self.column,
241        }
242    }
243
244    /// Return a reference to the inner value
245    pub fn inner(&self) -> &E {
246        &self.inner
247    }
248
249    /// Return the inner value
250    pub fn into_inner(self) -> E {
251        self.inner
252    }
253
254    /// Get the current file identifier
255    pub fn current_file(&self) -> Option<FileId> {
256        self.current_file
257    }
258
259    /// Set the current file identifier
260    pub fn set_current_file(&mut self, current_file: FileId) {
261        self.current_file = Some(current_file);
262    }
263
264    /// Get the raw position into the source
265    pub fn pos(&self) -> TextRange {
266        self.pos
267    }
268
269    /// Get the line number for this location
270    pub fn line(&self) -> u32 {
271        self.line_number
272    }
273
274    /// Get the column number for this location
275    pub fn col(&self) -> u32 {
276        self.column
277    }
278}
279
280impl<E: Clone> Clone for Located<E> {
281    fn clone(&self) -> Self {
282        Self {
283            inner: self.inner.clone(),
284            pos: self.pos,
285            current_file: self.current_file,
286            path: self.path.clone(),
287            file_override: self.file_override.clone(),
288            line_number: self.line_number,
289            column: self.column,
290        }
291    }
292}
293
294impl<E: PartialEq> PartialEq for Located<E> {
295    fn eq(&self, other: &Self) -> bool {
296        self.inner == other.inner
297            && self.pos == other.pos
298            && self.current_file == other.current_file
299            && self.path == other.path
300            && self.file_override == other.file_override
301            && self.line_number == other.line_number
302            && self.column == other.column
303    }
304}
305
306impl<E: Eq> Eq for Located<E> {}
307
308impl<E: std::error::Error> std::error::Error for Located<E> {}
309
310impl<E: std::fmt::Display> std::fmt::Display for Located<E> {
311    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312        if self.file_override.is_none() {
313            if let Some(path) = self.path.as_ref() {
314                write!(f, "{}:", path.display())?;
315            } else if let Some(current_file) = self.current_file {
316                write!(f, "{}:", current_file)?;
317            }
318        } else {
319            write!(f, "{}:", self.file_override)?;
320        }
321
322        write!(
323            f,
324            "{}:{}: {}",
325            self.line_number + 1,
326            self.column + 1,
327            self.inner
328        )
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::Resolver;
335    use std::convert::TryInto;
336
337    #[test]
338    fn resolved_position() {
339        let s = r#"
340Hello,
341World"#;
342
343        let offset = s.find('r').unwrap().try_into().unwrap();
344        let resolved = s.resolve(offset);
345
346        assert_eq!(resolved.0, 2);
347        assert_eq!(resolved.1, 2);
348    }
349
350    #[test]
351    fn resolved_position_last_char() {
352        let s = r#"
353Hello,
354World"#;
355
356        let offset = s.find('d').unwrap().try_into().unwrap();
357        let resolved = s.resolve(offset);
358
359        assert_eq!(resolved.0, 2);
360        assert_eq!(resolved.1, 4);
361    }
362
363    #[test]
364    fn resolved_position_out_of_bounds() {
365        let offset = 1.into();
366        assert_eq!("".resolve(offset).0, 0);
367    }
368}