Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-db/src/ra_fixture.rs')
| -rw-r--r-- | crates/ide-db/src/ra_fixture.rs | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/crates/ide-db/src/ra_fixture.rs b/crates/ide-db/src/ra_fixture.rs new file mode 100644 index 0000000000..1f056a835b --- /dev/null +++ b/crates/ide-db/src/ra_fixture.rs @@ -0,0 +1,532 @@ +//! Working with the fixtures in r-a tests, and providing IDE services for them. + +use std::hash::{BuildHasher, Hash}; + +use hir::{CfgExpr, FilePositionWrapper, FileRangeWrapper, Semantics}; +use smallvec::SmallVec; +use span::{TextRange, TextSize}; +use syntax::{ + AstToken, SmolStr, + ast::{self, IsString}, +}; + +use crate::{ + MiniCore, RootDatabase, SymbolKind, active_parameter::ActiveParameter, + documentation::Documentation, range_mapper::RangeMapper, search::ReferenceCategory, +}; + +pub use span::FileId; + +impl RootDatabase { + fn from_ra_fixture( + text: &str, + minicore: MiniCore<'_>, + ) -> Result<(RootDatabase, Vec<(FileId, usize)>, Vec<FileId>), ()> { + // We don't want a mistake in the fixture to crash r-a, so we wrap this in `catch_unwind()`. + std::panic::catch_unwind(|| { + let mut db = RootDatabase::default(); + let fixture = test_fixture::ChangeFixture::parse_with_proc_macros( + &db, + text, + minicore.0, + Vec::new(), + ); + db.apply_change(fixture.change); + let files = fixture + .files + .into_iter() + .zip(fixture.file_lines) + .map(|(file_id, range)| (file_id.file_id(&db), range)) + .collect(); + (db, files, fixture.sysroot_files) + }) + .map_err(|error| { + tracing::error!( + "cannot crate the crate graph: {}\nCrate graph:\n{}\n", + if let Some(&s) = error.downcast_ref::<&'static str>() { + s + } else if let Some(s) = error.downcast_ref::<String>() { + s.as_str() + } else { + "Box<dyn Any>" + }, + text, + ); + }) + } +} + +pub struct RaFixtureAnalysis { + pub db: RootDatabase, + tmp_file_ids: Vec<(FileId, usize)>, + line_offsets: Vec<TextSize>, + virtual_file_id_to_line: Vec<usize>, + mapper: RangeMapper, + literal: ast::String, + // `minicore` etc.. + sysroot_files: Vec<FileId>, + combined_len: TextSize, +} + +impl RaFixtureAnalysis { + pub fn analyze_ra_fixture( + sema: &Semantics<'_, RootDatabase>, + literal: ast::String, + expanded: &ast::String, + minicore: MiniCore<'_>, + on_cursor: &mut dyn FnMut(TextRange), + ) -> Option<RaFixtureAnalysis> { + if !literal.is_raw() { + return None; + } + + let active_parameter = ActiveParameter::at_token(sema, expanded.syntax().clone())?; + let has_rust_fixture_attr = active_parameter.attrs().is_some_and(|attrs| { + attrs.filter_map(|attr| attr.as_simple_path()).any(|path| { + path.segments() + .zip(["rust_analyzer", "rust_fixture"]) + .all(|(seg, name)| seg.name_ref().map_or(false, |nr| nr.text() == name)) + }) + }); + if !has_rust_fixture_attr { + return None; + } + let value = literal.value().ok()?; + + let mut mapper = RangeMapper::default(); + + // This is used for the `Injector`, to resolve precise location in the string literal, + // which will then be used to resolve precise location in the enclosing file. + let mut offset_with_indent = TextSize::new(0); + // This is used to resolve the location relative to the virtual file into a location + // relative to the indentation-trimmed file which will then (by the `Injector`) used + // to resolve to a location in the actual file. + // Besides indentation, we also skip `$0` cursors for this, since they are not included + // in the virtual files. + let mut offset_without_indent = TextSize::new(0); + + let mut text = &*value; + if let Some(t) = text.strip_prefix('\n') { + offset_with_indent += TextSize::of("\n"); + text = t; + } + // This stores the offsets of each line, **after we remove indentation**. + let mut line_offsets = Vec::new(); + for mut line in text.split_inclusive('\n') { + line_offsets.push(offset_without_indent); + + if line.starts_with("@@") { + // Introducing `//` into a fixture inside fixture causes all sorts of problems, + // so for testing purposes we escape it as `@@` and replace it here. + mapper.add("//", TextRange::at(offset_with_indent, TextSize::of("@@"))); + line = &line["@@".len()..]; + offset_with_indent += TextSize::of("@@"); + offset_without_indent += TextSize::of("@@"); + } + + // Remove indentation to simplify the mapping with fixture (which de-indents). + // Removing indentation shouldn't affect highlighting. + let mut unindented_line = line.trim_start(); + if unindented_line.is_empty() { + // The whole line was whitespaces, but we need the newline. + unindented_line = "\n"; + } + offset_with_indent += TextSize::of(line) - TextSize::of(unindented_line); + + let marker = "$0"; + match unindented_line.find(marker) { + Some(marker_pos) => { + let (before_marker, after_marker) = unindented_line.split_at(marker_pos); + let after_marker = &after_marker[marker.len()..]; + + mapper.add( + before_marker, + TextRange::at(offset_with_indent, TextSize::of(before_marker)), + ); + offset_with_indent += TextSize::of(before_marker); + offset_without_indent += TextSize::of(before_marker); + + if let Some(marker_range) = literal + .map_range_up(TextRange::at(offset_with_indent, TextSize::of(marker))) + { + on_cursor(marker_range); + } + offset_with_indent += TextSize::of(marker); + + mapper.add( + after_marker, + TextRange::at(offset_with_indent, TextSize::of(after_marker)), + ); + offset_with_indent += TextSize::of(after_marker); + offset_without_indent += TextSize::of(after_marker); + } + None => { + mapper.add( + unindented_line, + TextRange::at(offset_with_indent, TextSize::of(unindented_line)), + ); + offset_with_indent += TextSize::of(unindented_line); + offset_without_indent += TextSize::of(unindented_line); + } + } + } + + let combined = mapper.take_text(); + let combined_len = TextSize::of(&combined); + let (analysis, tmp_file_ids, sysroot_files) = + RootDatabase::from_ra_fixture(&combined, minicore).ok()?; + + // We use a `Vec` because we know the `FileId`s will always be close. + let mut virtual_file_id_to_line = Vec::new(); + for &(file_id, line) in &tmp_file_ids { + virtual_file_id_to_line.resize(file_id.index() as usize + 1, usize::MAX); + virtual_file_id_to_line[file_id.index() as usize] = line; + } + + Some(RaFixtureAnalysis { + db: analysis, + tmp_file_ids, + line_offsets, + virtual_file_id_to_line, + mapper, + literal, + sysroot_files, + combined_len, + }) + } + + pub fn files(&self) -> impl Iterator<Item = FileId> { + self.tmp_file_ids.iter().map(|(file, _)| *file) + } + + /// This returns `None` for minicore or other sysroot files. + fn virtual_file_id_to_line(&self, file_id: FileId) -> Option<usize> { + if self.is_sysroot_file(file_id) { + None + } else { + Some(self.virtual_file_id_to_line[file_id.index() as usize]) + } + } + + pub fn map_offset_down(&self, offset: TextSize) -> Option<(FileId, TextSize)> { + let inside_literal_range = self.literal.map_offset_down(offset)?; + let combined_offset = self.mapper.map_offset_down(inside_literal_range)?; + // There is usually a small number of files, so a linear search is smaller and faster. + let (_, &(file_id, file_line)) = + self.tmp_file_ids.iter().enumerate().find(|&(idx, &(_, file_line))| { + let file_start = self.line_offsets[file_line]; + let file_end = self + .tmp_file_ids + .get(idx + 1) + .map(|&(_, next_file_line)| self.line_offsets[next_file_line]) + .unwrap_or_else(|| self.combined_len); + TextRange::new(file_start, file_end).contains(combined_offset) + })?; + let file_line_offset = self.line_offsets[file_line]; + let file_offset = combined_offset - file_line_offset; + Some((file_id, file_offset)) + } + + pub fn map_range_down(&self, range: TextRange) -> Option<(FileId, TextRange)> { + let (start_file_id, start_offset) = self.map_offset_down(range.start())?; + let (end_file_id, end_offset) = self.map_offset_down(range.end())?; + if start_file_id != end_file_id { + None + } else { + Some((start_file_id, TextRange::new(start_offset, end_offset))) + } + } + + pub fn map_range_up( + &self, + virtual_file: FileId, + range: TextRange, + ) -> impl Iterator<Item = TextRange> { + // This could be `None` if the file is empty. + self.virtual_file_id_to_line(virtual_file) + .and_then(|line| self.line_offsets.get(line)) + .into_iter() + .flat_map(move |&tmp_file_offset| { + // Resolve the offset relative to the virtual file to an offset relative to the combined indentation-trimmed file + let range = range + tmp_file_offset; + // Then resolve that to an offset relative to the real file. + self.mapper.map_range_up(range) + }) + // And finally resolve the offset relative to the literal to relative to the file. + .filter_map(|range| self.literal.map_range_up(range)) + } + + pub fn map_offset_up(&self, virtual_file: FileId, offset: TextSize) -> Option<TextSize> { + self.map_range_up(virtual_file, TextRange::empty(offset)).next().map(|range| range.start()) + } + + pub fn is_sysroot_file(&self, file_id: FileId) -> bool { + self.sysroot_files.contains(&file_id) + } +} + +pub trait UpmapFromRaFixture: Sized { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()>; +} + +trait IsEmpty { + fn is_empty(&self) -> bool; +} + +impl<T> IsEmpty for Vec<T> { + fn is_empty(&self) -> bool { + self.is_empty() + } +} + +impl<T, const N: usize> IsEmpty for SmallVec<[T; N]> { + fn is_empty(&self) -> bool { + self.is_empty() + } +} + +#[allow(clippy::disallowed_types)] +impl<K, V, S> IsEmpty for std::collections::HashMap<K, V, S> { + fn is_empty(&self) -> bool { + self.is_empty() + } +} + +fn upmap_collection<T, Collection>( + collection: Collection, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, +) -> Result<Collection, ()> +where + T: UpmapFromRaFixture, + Collection: IntoIterator<Item = T> + FromIterator<T> + IsEmpty, +{ + if collection.is_empty() { + // The collection was already empty, don't mark it as failing just because of that. + return Ok(collection); + } + let result = collection + .into_iter() + .filter_map(|item| item.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok()) + .collect::<Collection>(); + if result.is_empty() { + // The collection was emptied by the upmapping - all items errored, therefore mark it as erroring as well. + Err(()) + } else { + Ok(result) + } +} + +impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Option<T> { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + Ok(match self { + Some(it) => Some(it.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?), + None => None, + }) + } +} + +impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Vec<T> { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + upmap_collection(self, analysis, virtual_file_id, real_file_id) + } +} + +impl<T: UpmapFromRaFixture, const N: usize> UpmapFromRaFixture for SmallVec<[T; N]> { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + upmap_collection(self, analysis, virtual_file_id, real_file_id) + } +} + +#[allow(clippy::disallowed_types)] +impl<K: UpmapFromRaFixture + Hash + Eq, V: UpmapFromRaFixture, S: BuildHasher + Default> + UpmapFromRaFixture for std::collections::HashMap<K, V, S> +{ + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + upmap_collection(self, analysis, virtual_file_id, real_file_id) + } +} + +// A map of `FileId`s is treated as associating the ranges in the values with the keys. +#[allow(clippy::disallowed_types)] +impl<V: UpmapFromRaFixture, S: BuildHasher + Default> UpmapFromRaFixture + for std::collections::HashMap<FileId, V, S> +{ + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + _virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + if self.is_empty() { + return Ok(self); + } + let result = self + .into_iter() + .filter_map(|(virtual_file_id, value)| { + Some(( + real_file_id, + value.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok()?, + )) + }) + .collect::<std::collections::HashMap<_, _, _>>(); + if result.is_empty() { Err(()) } else { Ok(result) } + } +} + +macro_rules! impl_tuple { + () => {}; // Base case. + ( $first:ident, $( $rest:ident, )* ) => { + impl< + $first: UpmapFromRaFixture, + $( $rest: UpmapFromRaFixture, )* + > UpmapFromRaFixture for ( $first, $( $rest, )* ) { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + #[allow(non_snake_case)] + let ( $first, $($rest,)* ) = self; + Ok(( + $first.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?, + $( $rest.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?, )* + )) + } + } + + impl_tuple!( $($rest,)* ); + }; +} +impl_tuple!(A, B, C, D, E,); + +impl UpmapFromRaFixture for TextSize { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + _real_file_id: FileId, + ) -> Result<Self, ()> { + analysis.map_offset_up(virtual_file_id, self).ok_or(()) + } +} + +impl UpmapFromRaFixture for TextRange { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + virtual_file_id: FileId, + _real_file_id: FileId, + ) -> Result<Self, ()> { + analysis.map_range_up(virtual_file_id, self).next().ok_or(()) + } +} + +// Deliberately do not implement that, as it's easy to get things misbehave and be treated with the wrong FileId: +// +// impl UpmapFromRaFixture for FileId { +// fn upmap_from_ra_fixture( +// self, +// _analysis: &RaFixtureAnalysis, +// _virtual_file_id: FileId, +// real_file_id: FileId, +// ) -> Result<Self, ()> { +// Ok(real_file_id) +// } +// } + +impl UpmapFromRaFixture for FilePositionWrapper<FileId> { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + _virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + Ok(FilePositionWrapper { + file_id: real_file_id, + offset: self.offset.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?, + }) + } +} + +impl UpmapFromRaFixture for FileRangeWrapper<FileId> { + fn upmap_from_ra_fixture( + self, + analysis: &RaFixtureAnalysis, + _virtual_file_id: FileId, + real_file_id: FileId, + ) -> Result<Self, ()> { + Ok(FileRangeWrapper { + file_id: real_file_id, + range: self.range.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?, + }) + } +} + +#[macro_export] +macro_rules! impl_empty_upmap_from_ra_fixture { + ( $( $ty:ty ),* $(,)? ) => { + $( + impl $crate::ra_fixture::UpmapFromRaFixture for $ty { + fn upmap_from_ra_fixture( + self, + _analysis: &$crate::ra_fixture::RaFixtureAnalysis, + _virtual_file_id: $crate::ra_fixture::FileId, + _real_file_id: $crate::ra_fixture::FileId, + ) -> Result<Self, ()> { + Ok(self) + } + } + )* + }; +} + +impl_empty_upmap_from_ra_fixture!( + bool, + i8, + i16, + i32, + i64, + i128, + u8, + u16, + u32, + u64, + u128, + f32, + f64, + &str, + String, + SmolStr, + Documentation, + SymbolKind, + CfgExpr, + ReferenceCategory, +); |