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