Skip to main content

plc_source/
source_location.rs

1use std::{
2    fmt::{Debug, Display, Formatter},
3    ops::Range,
4    path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{SourceCode, SourceContainer};
10
11#[derive(Clone, Default)]
12pub struct SourceLocationFactory {
13    file: Option<&'static str>,
14    newlines: NewLines,
15}
16
17impl SourceLocationFactory {
18    /// constructs a SourceRangeFactory used for internally generated code (e.g. builtins)
19    pub fn internal(src: &str) -> Self {
20        SourceLocationFactory { file: None, newlines: NewLines::build(src) }
21    }
22
23    /// constructs a SourceRangeFactory used to construct SourceRanes that point into the given source
24    pub fn for_source(source_code: &SourceCode) -> Self {
25        SourceLocationFactory {
26            file: Some(source_code.get_location_str()),
27            newlines: NewLines::build(&source_code.source),
28        }
29    }
30
31    /// creates a new SourceRange using the factory's file_name
32    pub fn create_range(&self, range: core::ops::Range<usize>) -> SourceLocation {
33        let start = TextLocation::from_offset(range.start, &self.newlines);
34        let end = TextLocation::from_offset(range.end, &self.newlines);
35        SourceLocation { span: CodeSpan::Range(start..end), file: self.file.into() }
36    }
37
38    pub fn create_block_location(&self, local_id: usize) -> SourceLocation {
39        SourceLocation { span: CodeSpan::Block { local_id }, file: self.file.into() }
40    }
41
42    pub fn create_file_only_location(&self) -> SourceLocation {
43        SourceLocation { span: CodeSpan::None, file: self.file.into() }
44    }
45
46    pub fn create_range_to_end_of_line(&self, line: usize, column: usize) -> SourceLocation {
47        let start = TextLocation::new(line, column, 0);
48        let end = TextLocation::new(line, self.newlines.get_end_of_line(line), 0);
49        SourceLocation { span: CodeSpan::Range(start..end), file: self.file.into() }
50    }
51}
52
53/// The location of a certain element in a text file
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct TextLocation {
57    /// Line in the source code where this location points to
58    line: usize,
59    /// Column in the sourcecode where this location points to
60    column: usize,
61    /// Raw offset to this location from the start of the file
62    offset: usize,
63}
64
65impl TextLocation {
66    pub fn new(line: usize, column: usize, offset: usize) -> Self {
67        TextLocation { line, column, offset }
68    }
69
70    pub fn from_offset(offset: usize, newlines: &NewLines) -> Self {
71        let line = newlines.get_line_nr(offset);
72        let column = newlines.get_column(line, offset);
73        TextLocation { line, column, offset }
74    }
75}
76
77/// Represents the location of a code element in a source code
78#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub enum CodeSpan {
80    /// The location of a block in a diagram
81    Block { local_id: usize },
82    /// An element spanning multiple IDs
83    Combined(Vec<CodeSpan>),
84    /// A location inside a text
85    Range(Range<TextLocation>),
86    /// The location does not point anywhere in the file
87    None,
88}
89
90impl std::fmt::Debug for CodeSpan {
91    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::Block { local_id } => f.debug_struct("Block").field("local_id", local_id).finish(),
94            Self::Combined(arg0) => f.debug_tuple("Combined").field(arg0).finish(),
95            Self::None => write!(f, "None"),
96
97            // The default debug implementation for Ranges is very verbose, which makes reading the output
98            // of `--ast` or snapshots "harder" than it should be. Thus we do it in a more concise way here
99            Self::Range(range) => {
100                write!(
101                    f,
102                    "Range({}:{} - {}:{})",
103                    range.start.line, range.start.column, range.end.line, range.end.column
104                )
105            }
106        }
107    }
108}
109
110impl Display for CodeSpan {
111    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112        match self {
113            CodeSpan::Block { .. } => write!(f, "Block {}", self.get_line()),
114            CodeSpan::Combined(spans) => {
115                write!(f, "{}", spans.iter().map(|it| it.to_string()).collect::<String>())
116            }
117            CodeSpan::Range(range) => write!(
118                f,
119                "{}:{}:{{{}:{}-{}:{}}}: ",
120                range.start.line,
121                range.start.column,
122                range.start.line,
123                range.start.column,
124                range.end.line,
125                range.end.column,
126            ),
127            CodeSpan::None => Ok(()),
128        }
129    }
130}
131
132impl CodeSpan {
133    /// Creates a codespan from line, column and range info
134    pub fn from_text_info(start: TextLocation, end: TextLocation) -> Self {
135        CodeSpan::Range(start..end)
136    }
137
138    /// Gets the line representation for a source location
139    /// If the location does not represent a line, the closest equivalent is returned
140    // That is 0 for None and the ID for id/inner spans
141    pub fn get_line(&self) -> usize {
142        match self {
143            Self::Range(range) => range.start.line,
144            Self::Block { local_id } => *local_id,
145            _ => 0,
146        }
147    }
148
149    /// Gets the colmumn representation for a source location
150    /// If the location does not represent a line, 0 is returned
151    pub fn get_line_end(&self) -> usize {
152        match self {
153            Self::Range(range) => range.end.line,
154            _ => 0,
155        }
156    }
157
158    pub fn get_line_plus_one(&self) -> usize {
159        match self {
160            Self::Range(range) => range.start.line + 1,
161            Self::Block { local_id } => *local_id,
162            _ => 0,
163        }
164    }
165
166    /// Gets the colmumn representation for a source location
167    /// If the location does not represent a line, 0 is returned
168    pub fn get_column(&self) -> usize {
169        match self {
170            Self::Range(range) => range.start.column,
171            _ => 0,
172        }
173    }
174
175    /// Gets the colmumn representation for a source location
176    /// If the location does not represent a line, 0 is returned
177    pub fn get_column_end(&self) -> usize {
178        match self {
179            Self::Range(range) => range.end.column,
180            _ => 0,
181        }
182    }
183
184    pub fn to_range(&self) -> Option<Range<usize>> {
185        match self {
186            CodeSpan::Range(range) => Some(range.start.offset..range.end.offset),
187            _ => None,
188        }
189    }
190}
191
192#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug, Serialize, Deserialize)]
193pub enum FileMarker {
194    File(&'static str),
195    #[default]
196    Undefined,
197    Internal(&'static str),
198}
199
200impl From<&'static str> for FileMarker {
201    fn from(value: &'static str) -> Self {
202        Self::File(value)
203    }
204}
205
206impl From<Option<&'static str>> for FileMarker {
207    fn from(value: Option<&'static str>) -> Self {
208        value.map(FileMarker::from).unwrap_or_default()
209    }
210}
211
212impl From<&FileMarker> for PathBuf {
213    fn from(val: &FileMarker) -> Self {
214        match val {
215            // Windows has problems with `<` and `>` in paths, hence we replace these symbols with `__`
216            FileMarker::File(f) | FileMarker::Internal(f) => f.replace(['<', '>'], "__").into(),
217            FileMarker::Undefined => Path::new("").to_path_buf(),
218        }
219    }
220}
221
222impl FileMarker {
223    pub fn get_name(&self) -> Option<&'static str> {
224        match self {
225            FileMarker::File(f) | FileMarker::Internal(f) => Some(f),
226            FileMarker::Undefined => None,
227        }
228    }
229
230    pub fn is_undefined(&self) -> bool {
231        matches!(self, Self::Undefined)
232    }
233
234    pub fn is_internal(&self) -> bool {
235        matches!(self, Self::Internal(_))
236    }
237}
238
239#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
240#[serde(bound(deserialize = "'de: 'static"))]
241pub struct SourceLocation {
242    span: CodeSpan,
243    /// the name of the file if available. if there is no file available
244    /// the source is probably internally generated by the compiler. (e.g.
245    /// a automatically generated data_type)
246    file: FileMarker,
247}
248
249impl From<&SourceLocation> for SourceLocation {
250    fn from(value: &SourceLocation) -> Self {
251        value.clone()
252    }
253}
254
255impl Debug for SourceLocation {
256    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
257        let mut f = f.debug_struct("SourceLocation");
258        f.field("span", &self.span);
259        if !(self.file.is_undefined() || self.file.is_internal()) {
260            f.field("file", &self.file.get_name());
261        }
262        f.finish()
263    }
264}
265
266impl Display for SourceLocation {
267    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
268        if !(self.file.is_internal() || self.file.is_undefined()) {
269            write!(f, "{}", self.get_file_name().unwrap())?;
270        }
271        write!(f, ":{}", self.span)
272    }
273}
274
275impl Default for SourceLocation {
276    fn default() -> Self {
277        Self::internal()
278    }
279}
280
281impl SourceLocation {
282    pub fn into_internal(self) -> Self {
283        let SourceLocation { span, file } = self;
284        SourceLocation { span, file: FileMarker::Internal(file.get_name().unwrap_or_default()) }
285    }
286    /// Constructs an undefined SourceRange with a 0..0 range and no filename
287    pub fn undefined() -> SourceLocation {
288        SourceLocation { span: CodeSpan::None, file: FileMarker::default() }
289    }
290
291    /// Gets the line representation for a source location
292    /// If the location does not represent a line, the closest equivalent is returned
293    /// That is 0 for None and the ID for id/inner spans
294    pub fn get_line(&self) -> usize {
295        self.span.get_line()
296    }
297
298    /// Gets the line representation for a source location
299    /// If the location does not represent a line, the closest equivalent is returned
300    /// That is 0 for None and the ID for id/inner spans
301    pub fn get_line_end(&self) -> usize {
302        self.span.get_line_end()
303    }
304
305    /// Same as [`get_line`] but adds one to the line number if its of type [`CodeSpan::Range`].
306    pub fn get_line_plus_one(&self) -> usize {
307        self.span.get_line_plus_one()
308    }
309
310    /// Gets the colmumn representation for a source location
311    /// If the location does not represent a line, 0 is returned
312    pub fn get_column(&self) -> usize {
313        self.span.get_column()
314    }
315
316    /// Gets the colmumn representation for a source location
317    /// If the location does not represent a line, 0 is returned
318    pub fn get_column_end(&self) -> usize {
319        self.span.get_column_end()
320    }
321
322    /// returns a new SourceRange that spans `this` and the `other` range.
323    /// In other words this results in `self.start .. other.end`
324    pub fn span(&self, other: &SourceLocation) -> SourceLocation {
325        let span = match (&self.span, &other.span) {
326            //ID -> ID = Combine
327            (CodeSpan::Block { local_id }, CodeSpan::Block { local_id: other }) if local_id == other => {
328                CodeSpan::Block { local_id: *local_id }
329            }
330            (CodeSpan::Block { .. }, CodeSpan::Block { .. }) => {
331                CodeSpan::Combined(vec![self.span.clone(), other.span.clone()])
332            }
333            //Range -> Range = Range
334            (CodeSpan::Range(start), CodeSpan::Range(end)) => CodeSpan::Range(start.start..end.end),
335            //ID -> Range = keep the block identity
336            (CodeSpan::Block { local_id }, CodeSpan::Range(_))
337            | (CodeSpan::Range(_), CodeSpan::Block { local_id }) => CodeSpan::Block { local_id: *local_id },
338            //None any -> None (unsupported)
339            (CodeSpan::None, _) | (_, CodeSpan::None) => CodeSpan::None,
340            (CodeSpan::Combined(inner), CodeSpan::Combined(other)) => {
341                let mut inner = inner.clone();
342                inner.extend_from_slice(other);
343                CodeSpan::Combined(inner)
344            }
345            (CodeSpan::Combined(data), other) | (other, CodeSpan::Combined(data)) => {
346                let mut data = data.clone();
347                data.push(other.clone());
348                CodeSpan::Combined(data)
349            }
350        };
351        SourceLocation { span, file: self.file }
352    }
353
354    /// converts this SourceRange into a Range
355    pub fn to_range(&self) -> Option<Range<usize>> {
356        self.span.to_range()
357    }
358
359    pub fn get_file_name(&self) -> Option<&'static str> {
360        self.file.get_name()
361    }
362
363    /// returns true if this SourceRange points to an undefined location.
364    /// see `SourceRange::undefined()`
365    pub fn is_undefined(&self) -> bool {
366        self.file.is_undefined() && self.span == CodeSpan::None
367    }
368
369    pub fn get_span(&self) -> &CodeSpan {
370        &self.span
371    }
372
373    pub fn replace_with(&mut self, new_location: SourceLocation) {
374        self.span = new_location.span;
375        self.file = new_location.file;
376    }
377    /// creates a SymbolLocation with undefined source_range used for
378    /// symbols that are created by the compiler on-the-fly.
379    pub fn internal() -> Self {
380        SourceLocation { span: CodeSpan::None, file: FileMarker::Internal("<internal>") }
381    }
382
383    /// creates an internal SymbolLocation but specifies the CompilationUnit/File it was encountered in.
384    /// This can be necessary when checking if dependencies are located within the same unit as the internally generated
385    /// element
386    pub fn internal_in_unit(unit: Option<&'static str>) -> Self {
387        let Some(unit) = unit else { return Self::internal() };
388        SourceLocation { span: CodeSpan::None, file: FileMarker::File(unit) }
389    }
390
391    pub fn is_internal(&self) -> bool {
392        matches!(self.file, FileMarker::Internal(_)) | matches!(self.span, CodeSpan::None)
393    }
394
395    pub fn is_builtin_internal(&self) -> bool {
396        self.file.is_internal() && self.span == CodeSpan::None
397    }
398
399    pub fn is_in_unit(&self, unit: impl AsRef<str>) -> bool {
400        if let Some(filename) = self.get_file_name() {
401            filename == unit.as_ref()
402        } else {
403            // Fallback, if no file is defined all files are local
404            true
405        }
406    }
407}
408
409/**
410 * A datastructure that stores the location of newline characters of a string.
411 * It also offers some useful methods to determine the line-number of an offset-location.
412 */
413#[derive(Clone, Default, Debug, PartialEq, Eq)]
414pub struct NewLines {
415    line_breaks: Vec<usize>,
416}
417
418impl NewLines {
419    pub fn build(str: &str) -> NewLines {
420        let mut line_breaks = Vec::new();
421        let mut total_offset: usize = 0;
422        if !str.is_empty() {
423            // Instead of using ´lines()´ we split at \n to preserve the offsets if a \r exists
424            for l in str.split('\n') {
425                total_offset += l.len() + 1;
426                line_breaks.push(total_offset);
427            }
428        }
429        NewLines { line_breaks }
430    }
431
432    ///
433    /// returns the 0 based line-nr of the given offset-location
434    ///
435    pub fn get_line_nr(&self, offset: usize) -> usize {
436        match self.line_breaks.binary_search(&offset) {
437            //In case we hit an exact match, we just found the first character of a new line, we must add one to the result
438            Ok(line) => line + 1,
439            Err(line) => line,
440        }
441    }
442
443    ///
444    /// returns the 0 based column of the given offset-location
445    ///
446    pub fn get_column(&self, line: usize, offset: usize) -> usize {
447        if line > 0 {
448            self.line_breaks.get(line - 1).map(|l| offset - *l).unwrap_or(0)
449        } else {
450            offset
451        }
452    }
453
454    ///
455    /// returns the 0 based column of end-of-line character for the given line
456    pub fn get_end_of_line(&self, line: usize) -> usize {
457        self.line_breaks.get(line).copied().unwrap_or_default()
458    }
459}
460
461#[cfg(test)]
462mod tests {
463    use crate::source_location::{NewLines, TextLocation};
464    use insta::assert_debug_snapshot;
465
466    use super::{CodeSpan, SourceLocation};
467
468    #[test]
469    fn new_lines_test_empty_string() {
470        let text = "";
471        let nl = NewLines::build(text);
472
473        assert_eq!(nl.get_line_nr(0), 0);
474        assert_eq!(nl.get_line_nr(1), 0);
475        assert_eq!(nl.get_line_nr(2), 0);
476        assert_eq!(nl.get_line_nr(3), 0);
477    }
478
479    #[test]
480    fn new_lines_test_three_lines_with_crlf() {
481        let text = "A\r\nB\r\nC";
482        let nl = NewLines::build(text);
483        assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
484        assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 1);
485        assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 2);
486    }
487
488    #[test]
489    fn new_lines_test_three_lines_with_lf() {
490        let text = "A\nB\nC";
491        let nl = NewLines::build(text);
492        assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
493        assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 1);
494        assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 2);
495    }
496
497    #[test]
498    fn new_lines_test_three_long_lines_with_lf() {
499        let text = "xxxx A xxxx
500
501    xxxx B xxxx
502
503    xxxx C xxxxx";
504        let nl = NewLines::build(text);
505        assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
506        assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 2);
507        assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 4);
508    }
509
510    #[test]
511    fn new_lines_and_columns_test() {
512        let text = "xxxx A xxxx
513
514    xxxx B xxxx
515
516    xxxx C xxxxx";
517        let nl = NewLines::build(text);
518        assert_eq!(nl.get_column(0, text.find('A').unwrap()), 5);
519        assert_eq!(nl.get_column(2, text.find('B').unwrap()), 9);
520        assert_eq!(nl.get_column(4, text.find('C').unwrap()), 9);
521    }
522
523    #[test]
524    fn span_two_blocks() {
525        let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
526        let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 2 } };
527        assert_debug_snapshot!(loc1.span(&loc2));
528    }
529
530    #[test]
531    fn span_two_blocks_with_same_id() {
532        let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
533        let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
534        assert_debug_snapshot!(loc1.span(&loc2));
535    }
536
537    #[test]
538    fn span_id_and_range() {
539        let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
540        let loc2 = SourceLocation {
541            file: None.into(),
542            span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(2, 0, 4)),
543        };
544        assert_debug_snapshot!(loc1.span(&loc2));
545    }
546
547    #[test]
548    fn span_two_ranges() {
549        let loc1 = SourceLocation {
550            file: None.into(),
551            span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(2, 0, 4)),
552        };
553        let loc2 = SourceLocation {
554            file: None.into(),
555            span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(5, 30, 10)),
556        };
557        assert_debug_snapshot!(loc1.span(&loc2));
558    }
559
560    #[test]
561    fn span_combined() {
562        let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
563        let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 2 } };
564        let loc3 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 3 } };
565        let loc4 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 4 } };
566        assert_debug_snapshot!(loc1.span(&loc2).span(&loc3.span(&loc4)));
567    }
568
569    #[test]
570    fn span_none() {
571        let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
572        let loc2 = SourceLocation { file: None.into(), span: CodeSpan::None };
573        assert_debug_snapshot!(loc1.span(&loc2));
574    }
575}