1use std::{
2 cell::RefCell, collections::hash_map::Entry, default::Default, ffi::OsStr,
3 path::PathBuf, sync::Arc,
4};
5
6use anyhow::{Context, Result, bail, ensure};
7use rustc_data_structures::fx::FxHashMap as HashMap;
8use rustc_hir::{
9 BodyId,
10 intravisit::{self, Visitor},
11};
12use rustc_index::IndexVec;
13use rustc_middle::ty::TyCtxt;
14use rustc_span::{
15 FileName, RemapPathScopeComponents, SourceFile, Span, source_map::SourceMap,
16};
17#[cfg(feature = "serde")]
18use serde::Serialize;
19#[cfg(feature = "ts-rs")]
20use ts_rs::TS;
21
22use super::filename::{Filename, FilenameIndex};
23use crate::cache::Cache;
24
25struct CharByteMapping {
26 byte_to_char: HashMap<BytePos, CharPos>,
27 char_to_byte: HashMap<CharPos, BytePos>,
28}
29
30impl CharByteMapping {
31 pub fn build(file: &SourceFile) -> Self {
32 let mut byte_to_char = HashMap::default();
33 let mut char_to_byte = HashMap::default();
34
35 macro_rules! check_insert {
36 ($bpos:expr, $cpos:expr) => {
37 let _r = byte_to_char.insert($bpos, $cpos);
38 debug_assert!(
39 _r.is_none(),
40 "byte_to_char: bytepos={:?}, charpos={:?}",
41 $bpos,
42 $cpos
43 );
44 let _r = char_to_byte.insert($cpos, $bpos);
45 debug_assert!(
46 _r.is_none(),
47 "char_to_byte: bytepos={:?}, charpos={:?}",
48 $bpos,
49 $cpos
50 );
51 };
52 }
53
54 for line in 0 .. file.count_lines() {
55 let line_str = file.get_line(line).unwrap();
56 let line_bounds = file.line_bounds(line);
57 let line_start = line_bounds.start.0 as usize;
58 let mut last_column = 0;
59 let mut last_offset = 0;
60 for (column, (byte_offset, c)) in line_str.char_indices().enumerate() {
61 let bpos = BytePos(line_start + byte_offset);
62 let cpos = CharPos { line, column };
63 check_insert!(bpos, cpos);
64 last_column = column + 1;
65 last_offset = byte_offset + c.len_utf8();
66 }
67
68 let bpos = BytePos(line_start + last_offset);
69 let cpos = CharPos {
70 line,
71 column: last_column,
72 };
73 check_insert!(bpos, cpos);
74 }
75
76 CharByteMapping {
77 byte_to_char,
78 char_to_byte,
79 }
80 }
81
82 #[allow(unused)]
83 pub fn byte_to_char(&self, pos: BytePos) -> CharPos {
84 *self
85 .byte_to_char
86 .get(&pos)
87 .unwrap_or_else(|| panic!("Could not find char pos for {pos:?}"))
88 }
89
90 pub fn char_to_byte(&self, pos: CharPos) -> BytePos {
91 *self
92 .char_to_byte
93 .get(&pos)
94 .unwrap_or_else(|| panic!("Could not find byte pos for {pos:?}"))
95 }
96}
97
98#[derive(Default)]
99pub struct RangeContext {
100 filenames: IndexVec<FilenameIndex, Filename>,
101 path_mapping: HashMap<FilenameIndex, Arc<SourceFile>>,
102 char_byte_mapping: Cache<FilenameIndex, CharByteMapping>,
103}
104
105thread_local! {
106 static CONTEXT: RefCell<RangeContext> = RefCell::new(RangeContext::default());
107}
108
109impl Filename {
110 fn intern_with_ctx(self, ctx: &mut RangeContext) -> FilenameIndex {
111 let existing = ctx.filenames.iter_enumerated().find(|(_, f)| &self == *f);
112 match existing {
113 Some((index, _)) => index,
114 None => ctx.filenames.push(self),
115 }
116 }
117
118 pub fn intern<T: ?Sized + AsRef<OsStr>>(t: &T) -> FilenameIndex {
119 let filename = Filename(PathBuf::from(t));
120 CONTEXT.with(|ctx| filename.intern_with_ctx(&mut ctx.borrow_mut()))
121 }
122}
123
124impl FilenameIndex {
125 pub fn find_source_file(self, source_map: &SourceMap) -> Result<Arc<SourceFile>> {
126 CONTEXT.with(|ctx| {
127 let ctx = &mut *ctx.borrow_mut();
128 match ctx.path_mapping.entry(self) {
129 Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
130 Entry::Vacant(entry) => {
131 let files = source_map.files();
132 ensure!(
133 ctx.filenames.get(self).is_some(),
134 "Missing file index!"
135 );
136 let filename = &ctx.filenames[self];
137 let filename = filename.0
138 .canonicalize()
139 .unwrap_or_else(|_| filename.0.clone());
140 let rustc_filename = files
141 .iter()
142 .map(|file| &file.name)
143 .find(|name| match &name {
144 FileName::Real(real_file_name) => {
147 let other = real_file_name.path(RemapPathScopeComponents::DOCUMENTATION).to_path_buf();
148 let canonical = other.canonicalize();
149 let other = canonical.as_ref().unwrap_or(&other);
150 filename.ends_with(other)
151 }
152 _ => false,
153 })
154 .with_context(|| {
155 format!(
156 "Could not find SourceFile for path: {}. Available SourceFiles were: [{}]",
157 filename.display(),
158 files
159 .iter()
160 .filter_map(|file| match &file.name {
161 FileName::Real(other) =>
162 Some(other.path(RemapPathScopeComponents::DOCUMENTATION).display().to_string()),
163 _ => None,
164 })
165 .collect::<Vec<_>>()
166 .join(", ")
167 )
168 })?;
169 let file = source_map.get_source_file(rustc_filename).unwrap();
170 entry.insert(Arc::clone(&file));
171 Ok(file)
172 }
173 }
174 })
175 }
176}
177
178#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
179#[cfg_attr(feature = "serde", derive(Serialize))]
180#[cfg_attr(feature = "ts-rs", derive(TS))]
181pub struct BytePos(pub usize);
182
183#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
193#[cfg_attr(feature = "serde", derive(Serialize))]
194#[cfg_attr(feature = "ts-rs", derive(TS))]
195pub struct CharPos {
196 pub line: usize,
197 pub column: usize,
198}
199
200#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
201#[cfg_attr(feature = "serde", derive(Serialize))]
202#[cfg_attr(feature = "ts-rs", derive(TS))]
203pub struct ByteRange {
204 pub start: BytePos,
205 pub end: BytePos,
206 pub filename: FilenameIndex,
207}
208
209#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
216#[cfg_attr(feature = "serde", derive(Serialize))]
217#[cfg_attr(feature = "ts-rs", derive(TS))]
218pub struct CharRange {
219 pub start: CharPos,
220 pub end: CharPos,
221 pub filename: FilenameIndex,
222}
223
224impl ByteRange {
225 pub fn as_char_range(&self, source_map: &SourceMap) -> CharRange {
226 let file = self.filename.find_source_file(source_map).unwrap();
227
228 CONTEXT.with(|ctx| {
229 let ctx = ctx.borrow();
230 let mapping: &CharByteMapping = ctx
231 .char_byte_mapping
232 .get(&self.filename, |_| CharByteMapping::build(&file));
233
234 let char_start = mapping.byte_to_char(self.start);
235 let char_end = mapping.byte_to_char(self.end);
236
237 CharRange {
238 start: char_start,
239 end: char_end,
240 filename: self.filename,
241 }
242 })
243 }
244
245 pub fn from_char_range(
246 char_start: CharPos,
247 char_end: CharPos,
248 filename: FilenameIndex,
249 source_map: &SourceMap,
250 ) -> Result<ByteRange> {
251 let file = filename.find_source_file(source_map)?;
252
253 CONTEXT.with(|ctx| {
254 let ctx = ctx.borrow();
255 let mapping = ctx
256 .char_byte_mapping
257 .get(&filename, |_| CharByteMapping::build(&file));
258 let byte_start = mapping.char_to_byte(char_start);
259 let byte_end = mapping.char_to_byte(char_end);
260 Ok(ByteRange {
261 start: byte_start,
262 end: byte_end,
263 filename,
264 })
265 })
266 }
267
268 pub fn from_span(span: Span, source_map: &SourceMap) -> Result<Self> {
269 CONTEXT.with(|ctx| {
270 let mut ctx = ctx.borrow_mut();
271
272 log::trace!("Converting to range: {span:?}");
273 let file = source_map.lookup_source_file(span.lo());
274 let filename = match &file.name {
275 FileName::Real(real_file_name) => {
276 let filename = real_file_name.path(RemapPathScopeComponents::DOCUMENTATION);
277 Filename(filename.to_path_buf()).intern_with_ctx(&mut ctx)
278 }
279 filename => bail!("Range::from_span doesn't support {filename:?}"),
280 };
281
282 ensure!(
283 source_map.ensure_source_file_source_present(&file),
284 "Could not load source for file: {:?}",
285 file.name
286 );
287 let external = file.external_src.borrow();
288 let _src = file
289 .src
290 .as_deref()
291 .map(String::as_str)
292 .unwrap_or_else(|| external.get_source().as_ref().unwrap());
293
294 let byte_start = BytePos(span.lo().0 as usize);
295 let byte_end = BytePos(span.hi().0 as usize);
296
297 Ok(ByteRange {
298 start: byte_start,
299 end: byte_end,
300 filename,
301 })
302 })
303 }
304
305 pub fn substr(&self, s: &str) -> String {
306 s[self.start.0 .. self.end.0].to_string()
307 }
308}
309
310impl CharRange {
311 pub fn from_span(span: Span, source_map: &SourceMap) -> Result<Self> {
312 let byte_range = ByteRange::from_span(span, source_map)?;
313 Ok(byte_range.as_char_range(source_map))
314 }
315}
316
317pub trait ToSpan {
319 fn to_span(&self, tcx: TyCtxt) -> Result<Span>;
320}
321
322impl ToSpan for ByteRange {
323 fn to_span(&self, _tcx: TyCtxt) -> Result<Span> {
324 Ok(Span::with_root_ctxt(
325 rustc_span::BytePos(u32::try_from(self.start.0).unwrap()),
326 rustc_span::BytePos(u32::try_from(self.end.0).unwrap()),
327 ))
328 }
329}
330
331impl ToSpan for CharRange {
332 fn to_span(&self, tcx: TyCtxt) -> Result<Span> {
333 let range = ByteRange::from_char_range(
334 self.start,
335 self.end,
336 self.filename,
337 tcx.sess.source_map(),
338 )?;
339 range.to_span(tcx)
340 }
341}
342
343fn qpath_to_span(tcx: TyCtxt, qpath: String) -> Result<Span> {
344 struct Finder<'tcx> {
345 tcx: TyCtxt<'tcx>,
346 qpath: String,
347 span: Option<Span>,
348 }
349
350 impl<'tcx> Visitor<'tcx> for Finder<'tcx> {
351 fn visit_nested_body(&mut self, id: BodyId) {
352 intravisit::walk_body(self, self.tcx.hir_body(id));
353
354 let local_def_id = self.tcx.hir_body_owner_def_id(id);
355 let function_path = self
356 .tcx
357 .def_path(local_def_id.to_def_id())
358 .to_string_no_crate_verbose();
359 if function_path[2 ..] == self.qpath {
360 self.span = Some(self.tcx.hir_span(id.hir_id));
361 }
362 }
363 }
364
365 let mut finder = Finder {
366 tcx,
367 qpath,
368 span: None,
369 };
370 tcx.hir_visit_all_item_likes_in_crate(&mut finder);
371 finder
372 .span
373 .with_context(|| format!("No function with qpath {}", finder.qpath))
374}
375
376pub enum FunctionIdentifier {
378 Qpath(String),
380
381 Range(CharRange),
383}
384
385impl ToSpan for FunctionIdentifier {
386 fn to_span(&self, tcx: TyCtxt) -> Result<Span> {
387 match self {
388 FunctionIdentifier::Qpath(qpath) => qpath_to_span(tcx, qpath.clone()),
389 FunctionIdentifier::Range(range) => range.to_span(tcx),
390 }
391 }
392}
393
394#[cfg(test)]
395mod test {
396 use super::*;
397 use crate::test_utils::{self, CompileResult};
398
399 #[test]
400 fn test_range() {
401 let emoji = "🦀";
402 let input = &format!(
403 r#"fn main() {{
404 let x = "{emoji}";
405}}
406
407// mysterious bytes
408"#
409 );
410
411 test_utils::CompileBuilder::new(input).compile(|CompileResult { tcx }| {
412 let source_map = tcx.sess.source_map();
413 let filename = Filename::intern("dummy.rs");
414 filename.find_source_file(source_map).unwrap();
415
416 let id = FunctionIdentifier::Qpath(String::from("main"));
417 id.to_span(tcx).unwrap();
418
419 let id = FunctionIdentifier::Qpath(String::from("foobar"));
420 id.to_span(tcx).unwrap_err();
421
422 let id = FunctionIdentifier::Range(CharRange {
423 start: CharPos { line: 0, column: 0 },
424 end: CharPos { line: 0, column: 1 },
425 filename,
426 });
427 id.to_span(tcx).unwrap();
428
429 let emoji_index = input.find(emoji).unwrap();
430 let byte_range = ByteRange {
431 start: BytePos(emoji_index),
432 end: BytePos(emoji_index + emoji.len()),
433 filename,
434 };
435 let char_range = byte_range.as_char_range(source_map);
436 let emoji_line = 1;
437 let emoji_column = 11;
438 assert_eq!(char_range, CharRange {
439 start: CharPos {
440 line: emoji_line,
441 column: emoji_column
442 },
443 end: CharPos {
444 line: emoji_line,
445 column: emoji_column + 1
446 },
447 filename
448 });
449 });
450 }
451}