sourcefile/
lib.rs

1//! A library providing `SourceFiles`, a concatenated list of files with information for resolving
2//! points and spans.
3
4use std::path::Path;
5use std::{fmt, fs, io};
6
7/// A concatenated string of files, with sourcemap information.
8#[derive(Debug, Default, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct SourceFile {
10    /// The full contents of all the files
11    pub contents: String,
12    /// The names of the files (same length as `files`).
13    file_names: Vec<String>,
14    /// The number of lines in each file.
15    file_lines: Vec<usize>,
16    /// The length of each line in all source files
17    line_lengths: Vec<usize>,
18}
19
20impl SourceFile {
21    /// Create a new empty sourcefile. Equivalent to `Default::default`.
22    pub fn new() -> Self {
23        Default::default()
24    }
25
26    /// Concatenate a file to the end of `contents`, and record info needed to resolve spans.
27    ///
28    /// If the last line doesn't end with a newline character, it will still be a 'line' for the
29    /// purposes of this calculation.
30    pub fn add_file(&mut self, filename: impl AsRef<Path>) -> io::Result<()> {
31        let filename = filename.as_ref();
32        let file = fs::read_to_string(filename)?;
33
34        // We should skip this file if it is completely empty.
35        self.add_file_raw(filename.display(), file);
36        Ok(())
37    }
38
39    pub fn add_file_raw(&mut self, name: impl fmt::Display, contents: impl Into<String>) {
40        let contents = contents.into();
41        // We should skip this file if it is completely empty (There are no offsets that index into this file).
42        if contents.is_empty() {
43            return;
44        }
45
46        let mut num_lines = 0;
47        // We can't use str::lines because we won't know if 1 or 2 chars were lost (if there was a \r).
48        let mut lines = contents.split('\n').peekable();
49        while let Some(line) = lines.next() {
50            if lines.peek().is_some() {
51                // middle line
52                num_lines += 1;
53                self.line_lengths.push(line.len() + 1);
54            } else if line.is_empty() {
55                // last line is empty, skip it
56            } else {
57                // last line not empty, but no \n at the end.
58                num_lines += 1;
59                self.line_lengths.push(line.len());
60            }
61        }
62
63        // Record the name
64        self.file_names.push(name.to_string());
65        // Record the number of lines
66        self.file_lines.push(num_lines);
67        self.contents += &contents;
68    }
69
70    /// Get the file, line, and col position of a byte offset.
71    ///
72    /// # Panics
73    ///
74    /// This function will panic if `offset` is not on a character boundary.
75    pub fn resolve_offset<'a>(&'a self, offset: usize) -> Option<Position<'a>> {
76        // If there isn't a single line, always return None.
77        let mut line_acc = *self.line_lengths.get(0)?;
78        let mut line_idx = 0;
79        while line_acc <= offset {
80            line_idx += 1;
81            // If we have exhaused all the lines, return None
82            line_acc += *self.line_lengths.get(line_idx)?;
83        }
84        // Go back to the start of the line (for working out the column).
85        line_acc -= self.line_lengths[line_idx];
86
87        // Can't panic - if we have a line we have a file
88        let mut file_acc = self.file_lines[0];
89        let mut file_idx = 0;
90        while file_acc <= line_idx {
91            file_idx += 1;
92            file_acc += self.file_lines[file_idx];
93        }
94        // Go back to the start of the file (for working out the line).
95        file_acc -= self.file_lines[file_idx];
96
97        Some(Position::new(
98            &self.file_names[file_idx],
99            line_idx - file_acc,
100            offset - line_acc,
101        ))
102    }
103
104    /// Get the file, line, and col position of each end of a span.
105    // TODO this could be more efficient by using the fact that end is after (and probably near to)
106    // start.
107    pub fn resolve_offset_span<'a>(&'a self, start: usize, end: usize) -> Option<Span<'a>> {
108        if end < start {
109            return None;
110        }
111        Some(Span {
112            start: self.resolve_offset(start)?,
113            end: self.resolve_offset(end)?,
114        })
115    }
116}
117
118/// A position in a source file.
119#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
120pub struct Position<'a> {
121    /// Name of the file the position is in.
122    pub filename: &'a str,
123    /// 0-indexed line number of position.
124    pub line: usize,
125    /// 0-indexed column number of position.
126    pub col: usize,
127}
128
129impl<'a> Position<'a> {
130    /// Constructor for tests.
131    fn new(filename: &'a str, line: usize, col: usize) -> Position<'a> {
132        Position {
133            filename: filename.as_ref(),
134            line,
135            col,
136        }
137    }
138}
139
140/// A span in a source file
141#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
142pub struct Span<'a> {
143    pub start: Position<'a>,
144    pub end: Position<'a>,
145}
146
147#[cfg(test)]
148mod tests {
149    extern crate tempfile;
150
151    use self::tempfile::NamedTempFile;
152    use super::{Position, SourceFile, Span};
153    use std::io::Write;
154
155    #[test]
156    fn empty() {
157        let sourcefile = SourceFile::default();
158        assert!(sourcefile.resolve_offset(0).is_none());
159    }
160
161    #[test]
162    fn smoke() {
163        test_files(
164            &[
165                "A file with\ntwo lines.\n",
166                "Another file with\ntwo more lines.\n",
167            ],
168            &[
169                (0, (0, 0, 0)),   // start
170                (5, (0, 0, 5)),   // last char first line first file
171                (11, (0, 0, 11)), // first char second line first file
172                (12, (0, 1, 0)),  // ..
173                (13, (0, 1, 1)),
174                (13, (0, 1, 1)),
175                (22, (0, 1, 10)),
176                (23, (1, 0, 0)),
177                (24, (1, 0, 1)),
178                (40, (1, 0, 17)),
179                (41, (1, 1, 0)),
180                (42, (1, 1, 1)),
181                (56, (1, 1, 15)),
182                //(57, (1, 1, 16)), // should panic
183            ],
184            &[((0, 5), (0, 0, 0), (0, 0, 5))],
185        )
186    }
187
188    fn test_files<'a>(
189        files: &[impl AsRef<str>],
190        offset_tests: &[(usize, (usize, usize, usize))],
191        offset_span_tests: &[((usize, usize), (usize, usize, usize), (usize, usize, usize))],
192    ) {
193        let mut sourcefile = SourceFile::default();
194        let mut file_handles = Vec::new(); // don't clean me up please
195        for contents in files {
196            let mut file = NamedTempFile::new().unwrap();
197            write!(file, "{}", contents.as_ref()).unwrap();
198            sourcefile.add_file(file.path()).unwrap();
199            file_handles.push(file);
200        }
201
202        for &(offset, (file_idx, line, col)) in offset_tests {
203            let filename = format!("{}", file_handles[file_idx].path().display());
204            let pos = sourcefile.resolve_offset(offset);
205            assert_eq!(pos.unwrap(), Position::new(&filename, line, col));
206        }
207
208        for &(
209            (start, end),
210            (file_idx_start, line_start, col_start),
211            (file_idx_end, line_end, col_end),
212        ) in offset_span_tests
213        {
214            let start_filename = format!("{}", file_handles[file_idx_start].path().display());
215            let end_filename = format!("{}", file_handles[file_idx_end].path().display());
216            assert_eq!(
217                sourcefile.resolve_offset_span(start, end).unwrap(),
218                Span {
219                    start: Position::new(&start_filename, line_start, col_start),
220                    end: Position::new(&end_filename, line_end, col_end),
221                }
222            );
223        }
224    }
225
226    #[test]
227    fn test_raw() {
228        let mut sourcefile = SourceFile::new();
229        sourcefile.add_file_raw("test", " ");
230        assert_eq!(*sourcefile.line_lengths.last().unwrap(), 1);
231    }
232}