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