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.rs532
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,
+);