ide_db/
ra_fixture.rs

1//! Working with the fixtures in r-a tests, and providing IDE services for them.
2
3use std::hash::{BuildHasher, Hash};
4
5use hir::{CfgExpr, FilePositionWrapper, FileRangeWrapper, Semantics, Symbol};
6use smallvec::SmallVec;
7use span::{TextRange, TextSize};
8use syntax::{
9    AstToken, SmolStr,
10    ast::{self, IsString},
11};
12
13use crate::{
14    MiniCore, RootDatabase, SymbolKind, active_parameter::ActiveParameter,
15    documentation::Documentation, range_mapper::RangeMapper, search::ReferenceCategory,
16};
17
18pub use span::FileId;
19
20impl RootDatabase {
21    fn from_ra_fixture(
22        text: &str,
23        minicore: MiniCore<'_>,
24    ) -> Result<(RootDatabase, Vec<(FileId, usize)>, Vec<FileId>), ()> {
25        // We don't want a mistake in the fixture to crash r-a, so we wrap this in `catch_unwind()`.
26        std::panic::catch_unwind(|| {
27            let mut db = RootDatabase::default();
28            let fixture =
29                test_fixture::ChangeFixture::parse_with_proc_macros(text, minicore.0, Vec::new());
30            db.apply_change(fixture.change);
31            let files = fixture
32                .files
33                .into_iter()
34                .zip(fixture.file_lines)
35                .map(|(file_id, range)| (file_id.file_id(), range))
36                .collect();
37            (db, files, fixture.sysroot_files)
38        })
39        .map_err(|error| {
40            tracing::error!(
41                "cannot crate the crate graph: {}\nCrate graph:\n{}\n",
42                if let Some(&s) = error.downcast_ref::<&'static str>() {
43                    s
44                } else if let Some(s) = error.downcast_ref::<String>() {
45                    s.as_str()
46                } else {
47                    "Box<dyn Any>"
48                },
49                text,
50            );
51        })
52    }
53}
54
55pub struct RaFixtureAnalysis {
56    pub db: RootDatabase,
57    tmp_file_ids: Vec<(FileId, usize)>,
58    line_offsets: Vec<TextSize>,
59    virtual_file_id_to_line: Vec<usize>,
60    mapper: RangeMapper,
61    literal: ast::String,
62    // `minicore` etc..
63    sysroot_files: Vec<FileId>,
64    combined_len: TextSize,
65}
66
67impl RaFixtureAnalysis {
68    pub fn analyze_ra_fixture(
69        sema: &Semantics<'_, RootDatabase>,
70        literal: ast::String,
71        expanded: &ast::String,
72        minicore: MiniCore<'_>,
73        on_cursor: &mut dyn FnMut(TextRange),
74    ) -> Option<RaFixtureAnalysis> {
75        if !literal.is_raw() {
76            return None;
77        }
78
79        let active_parameter = ActiveParameter::at_token(sema, expanded.syntax().clone())?;
80        let has_rust_fixture_attr = active_parameter.attrs().is_some_and(|attrs| {
81            attrs.filter_map(|attr| attr.as_simple_path()).any(|path| {
82                path.segments()
83                    .zip(["rust_analyzer", "rust_fixture"])
84                    .all(|(seg, name)| seg.name_ref().map_or(false, |nr| nr.text() == name))
85            })
86        });
87        if !has_rust_fixture_attr {
88            return None;
89        }
90        let value = literal.value().ok()?;
91
92        let mut mapper = RangeMapper::default();
93
94        // This is used for the `Injector`, to resolve precise location in the string literal,
95        // which will then be used to resolve precise location in the enclosing file.
96        let mut offset_with_indent = TextSize::new(0);
97        // This is used to resolve the location relative to the virtual file into a location
98        // relative to the indentation-trimmed file which will then (by the `Injector`) used
99        // to resolve to a location in the actual file.
100        // Besides indentation, we also skip `$0` cursors for this, since they are not included
101        // in the virtual files.
102        let mut offset_without_indent = TextSize::new(0);
103
104        let mut text = &*value;
105        if let Some(t) = text.strip_prefix('\n') {
106            offset_with_indent += TextSize::of("\n");
107            text = t;
108        }
109        // This stores the offsets of each line, **after we remove indentation**.
110        let mut line_offsets = Vec::new();
111        for mut line in text.split_inclusive('\n') {
112            line_offsets.push(offset_without_indent);
113
114            if line.starts_with("@@") {
115                // Introducing `//` into a fixture inside fixture causes all sorts of problems,
116                // so for testing purposes we escape it as `@@` and replace it here.
117                mapper.add("//", TextRange::at(offset_with_indent, TextSize::of("@@")));
118                line = &line["@@".len()..];
119                offset_with_indent += TextSize::of("@@");
120                offset_without_indent += TextSize::of("@@");
121            }
122
123            // Remove indentation to simplify the mapping with fixture (which de-indents).
124            // Removing indentation shouldn't affect highlighting.
125            let mut unindented_line = line.trim_start();
126            if unindented_line.is_empty() {
127                // The whole line was whitespaces, but we need the newline.
128                unindented_line = "\n";
129            }
130            offset_with_indent += TextSize::of(line) - TextSize::of(unindented_line);
131
132            let marker = "$0";
133            match unindented_line.find(marker) {
134                Some(marker_pos) => {
135                    let (before_marker, after_marker) = unindented_line.split_at(marker_pos);
136                    let after_marker = &after_marker[marker.len()..];
137
138                    mapper.add(
139                        before_marker,
140                        TextRange::at(offset_with_indent, TextSize::of(before_marker)),
141                    );
142                    offset_with_indent += TextSize::of(before_marker);
143                    offset_without_indent += TextSize::of(before_marker);
144
145                    if let Some(marker_range) = literal
146                        .map_range_up(TextRange::at(offset_with_indent, TextSize::of(marker)))
147                    {
148                        on_cursor(marker_range);
149                    }
150                    offset_with_indent += TextSize::of(marker);
151
152                    mapper.add(
153                        after_marker,
154                        TextRange::at(offset_with_indent, TextSize::of(after_marker)),
155                    );
156                    offset_with_indent += TextSize::of(after_marker);
157                    offset_without_indent += TextSize::of(after_marker);
158                }
159                None => {
160                    mapper.add(
161                        unindented_line,
162                        TextRange::at(offset_with_indent, TextSize::of(unindented_line)),
163                    );
164                    offset_with_indent += TextSize::of(unindented_line);
165                    offset_without_indent += TextSize::of(unindented_line);
166                }
167            }
168        }
169
170        let combined = mapper.take_text();
171        let combined_len = TextSize::of(&combined);
172        let (analysis, tmp_file_ids, sysroot_files) =
173            RootDatabase::from_ra_fixture(&combined, minicore).ok()?;
174
175        // We use a `Vec` because we know the `FileId`s will always be close.
176        let mut virtual_file_id_to_line = Vec::new();
177        for &(file_id, line) in &tmp_file_ids {
178            virtual_file_id_to_line.resize(file_id.index() as usize + 1, usize::MAX);
179            virtual_file_id_to_line[file_id.index() as usize] = line;
180        }
181
182        Some(RaFixtureAnalysis {
183            db: analysis,
184            tmp_file_ids,
185            line_offsets,
186            virtual_file_id_to_line,
187            mapper,
188            literal,
189            sysroot_files,
190            combined_len,
191        })
192    }
193
194    pub fn files(&self) -> impl Iterator<Item = FileId> {
195        self.tmp_file_ids.iter().map(|(file, _)| *file)
196    }
197
198    /// This returns `None` for minicore or other sysroot files.
199    fn virtual_file_id_to_line(&self, file_id: FileId) -> Option<usize> {
200        if self.is_sysroot_file(file_id) {
201            None
202        } else {
203            Some(self.virtual_file_id_to_line[file_id.index() as usize])
204        }
205    }
206
207    pub fn map_offset_down(&self, offset: TextSize) -> Option<(FileId, TextSize)> {
208        let inside_literal_range = self.literal.map_offset_down(offset)?;
209        let combined_offset = self.mapper.map_offset_down(inside_literal_range)?;
210        // There is usually a small number of files, so a linear search is smaller and faster.
211        let (_, &(file_id, file_line)) =
212            self.tmp_file_ids.iter().enumerate().find(|&(idx, &(_, file_line))| {
213                let file_start = self.line_offsets[file_line];
214                let file_end = self
215                    .tmp_file_ids
216                    .get(idx + 1)
217                    .map(|&(_, next_file_line)| self.line_offsets[next_file_line])
218                    .unwrap_or_else(|| self.combined_len);
219                TextRange::new(file_start, file_end).contains(combined_offset)
220            })?;
221        let file_line_offset = self.line_offsets[file_line];
222        let file_offset = combined_offset - file_line_offset;
223        Some((file_id, file_offset))
224    }
225
226    pub fn map_range_down(&self, range: TextRange) -> Option<(FileId, TextRange)> {
227        let (start_file_id, start_offset) = self.map_offset_down(range.start())?;
228        let (end_file_id, end_offset) = self.map_offset_down(range.end())?;
229        if start_file_id != end_file_id {
230            None
231        } else {
232            Some((start_file_id, TextRange::new(start_offset, end_offset)))
233        }
234    }
235
236    pub fn map_range_up(
237        &self,
238        virtual_file: FileId,
239        range: TextRange,
240    ) -> impl Iterator<Item = TextRange> {
241        // This could be `None` if the file is empty.
242        self.virtual_file_id_to_line(virtual_file)
243            .and_then(|line| self.line_offsets.get(line))
244            .into_iter()
245            .flat_map(move |&tmp_file_offset| {
246                // Resolve the offset relative to the virtual file to an offset relative to the combined indentation-trimmed file
247                let range = range + tmp_file_offset;
248                // Then resolve that to an offset relative to the real file.
249                self.mapper.map_range_up(range)
250            })
251            // And finally resolve the offset relative to the literal to relative to the file.
252            .filter_map(|range| self.literal.map_range_up(range))
253    }
254
255    pub fn map_offset_up(&self, virtual_file: FileId, offset: TextSize) -> Option<TextSize> {
256        self.map_range_up(virtual_file, TextRange::empty(offset)).next().map(|range| range.start())
257    }
258
259    pub fn is_sysroot_file(&self, file_id: FileId) -> bool {
260        self.sysroot_files.contains(&file_id)
261    }
262}
263
264pub trait UpmapFromRaFixture: Sized {
265    fn upmap_from_ra_fixture(
266        self,
267        analysis: &RaFixtureAnalysis,
268        virtual_file_id: FileId,
269        real_file_id: FileId,
270    ) -> Result<Self, ()>;
271}
272
273trait IsEmpty {
274    fn is_empty(&self) -> bool;
275}
276
277impl<T> IsEmpty for Vec<T> {
278    fn is_empty(&self) -> bool {
279        self.is_empty()
280    }
281}
282
283impl<T, const N: usize> IsEmpty for SmallVec<[T; N]> {
284    fn is_empty(&self) -> bool {
285        self.is_empty()
286    }
287}
288
289#[allow(clippy::disallowed_types)]
290impl<K, V, S> IsEmpty for std::collections::HashMap<K, V, S> {
291    fn is_empty(&self) -> bool {
292        self.is_empty()
293    }
294}
295
296fn upmap_collection<T, Collection>(
297    collection: Collection,
298    analysis: &RaFixtureAnalysis,
299    virtual_file_id: FileId,
300    real_file_id: FileId,
301) -> Result<Collection, ()>
302where
303    T: UpmapFromRaFixture,
304    Collection: IntoIterator<Item = T> + FromIterator<T> + IsEmpty,
305{
306    if collection.is_empty() {
307        // The collection was already empty, don't mark it as failing just because of that.
308        return Ok(collection);
309    }
310    let result = collection
311        .into_iter()
312        .filter_map(|item| item.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok())
313        .collect::<Collection>();
314    if result.is_empty() {
315        // The collection was emptied by the upmapping - all items errored, therefore mark it as erroring as well.
316        Err(())
317    } else {
318        Ok(result)
319    }
320}
321
322impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Option<T> {
323    fn upmap_from_ra_fixture(
324        self,
325        analysis: &RaFixtureAnalysis,
326        virtual_file_id: FileId,
327        real_file_id: FileId,
328    ) -> Result<Self, ()> {
329        Ok(match self {
330            Some(it) => Some(it.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?),
331            None => None,
332        })
333    }
334}
335
336impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Vec<T> {
337    fn upmap_from_ra_fixture(
338        self,
339        analysis: &RaFixtureAnalysis,
340        virtual_file_id: FileId,
341        real_file_id: FileId,
342    ) -> Result<Self, ()> {
343        upmap_collection(self, analysis, virtual_file_id, real_file_id)
344    }
345}
346
347impl<T: UpmapFromRaFixture, const N: usize> UpmapFromRaFixture for SmallVec<[T; N]> {
348    fn upmap_from_ra_fixture(
349        self,
350        analysis: &RaFixtureAnalysis,
351        virtual_file_id: FileId,
352        real_file_id: FileId,
353    ) -> Result<Self, ()> {
354        upmap_collection(self, analysis, virtual_file_id, real_file_id)
355    }
356}
357
358#[allow(clippy::disallowed_types)]
359impl<K: UpmapFromRaFixture + Hash + Eq, V: UpmapFromRaFixture, S: BuildHasher + Default>
360    UpmapFromRaFixture for std::collections::HashMap<K, V, S>
361{
362    fn upmap_from_ra_fixture(
363        self,
364        analysis: &RaFixtureAnalysis,
365        virtual_file_id: FileId,
366        real_file_id: FileId,
367    ) -> Result<Self, ()> {
368        upmap_collection(self, analysis, virtual_file_id, real_file_id)
369    }
370}
371
372// A map of `FileId`s is treated as associating the ranges in the values with the keys.
373#[allow(clippy::disallowed_types)]
374impl<V: UpmapFromRaFixture, S: BuildHasher + Default> UpmapFromRaFixture
375    for std::collections::HashMap<FileId, V, S>
376{
377    fn upmap_from_ra_fixture(
378        self,
379        analysis: &RaFixtureAnalysis,
380        _virtual_file_id: FileId,
381        real_file_id: FileId,
382    ) -> Result<Self, ()> {
383        if self.is_empty() {
384            return Ok(self);
385        }
386        let result = self
387            .into_iter()
388            .filter_map(|(virtual_file_id, value)| {
389                Some((
390                    real_file_id,
391                    value.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok()?,
392                ))
393            })
394            .collect::<std::collections::HashMap<_, _, _>>();
395        if result.is_empty() { Err(()) } else { Ok(result) }
396    }
397}
398
399macro_rules! impl_tuple {
400    () => {}; // Base case.
401    ( $first:ident, $( $rest:ident, )* ) => {
402        impl<
403            $first: UpmapFromRaFixture,
404            $( $rest: UpmapFromRaFixture, )*
405        > UpmapFromRaFixture for ( $first, $( $rest, )* ) {
406            fn upmap_from_ra_fixture(
407                self,
408                analysis: &RaFixtureAnalysis,
409                virtual_file_id: FileId,
410                real_file_id: FileId,
411            ) -> Result<Self, ()> {
412                #[allow(non_snake_case)]
413                let ( $first, $($rest,)* ) = self;
414                Ok((
415                    $first.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
416                    $( $rest.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?, )*
417                ))
418            }
419        }
420
421        impl_tuple!( $($rest,)* );
422    };
423}
424impl_tuple!(A, B, C, D, E,);
425
426impl UpmapFromRaFixture for TextSize {
427    fn upmap_from_ra_fixture(
428        self,
429        analysis: &RaFixtureAnalysis,
430        virtual_file_id: FileId,
431        _real_file_id: FileId,
432    ) -> Result<Self, ()> {
433        analysis.map_offset_up(virtual_file_id, self).ok_or(())
434    }
435}
436
437impl UpmapFromRaFixture for TextRange {
438    fn upmap_from_ra_fixture(
439        self,
440        analysis: &RaFixtureAnalysis,
441        virtual_file_id: FileId,
442        _real_file_id: FileId,
443    ) -> Result<Self, ()> {
444        analysis.map_range_up(virtual_file_id, self).next().ok_or(())
445    }
446}
447
448// Deliberately do not implement that, as it's easy to get things misbehave and be treated with the wrong FileId:
449//
450// impl UpmapFromRaFixture for FileId {
451//     fn upmap_from_ra_fixture(
452//         self,
453//         _analysis: &RaFixtureAnalysis,
454//         _virtual_file_id: FileId,
455//         real_file_id: FileId,
456//     ) -> Result<Self, ()> {
457//         Ok(real_file_id)
458//     }
459// }
460
461impl UpmapFromRaFixture for FilePositionWrapper<FileId> {
462    fn upmap_from_ra_fixture(
463        self,
464        analysis: &RaFixtureAnalysis,
465        _virtual_file_id: FileId,
466        real_file_id: FileId,
467    ) -> Result<Self, ()> {
468        Ok(FilePositionWrapper {
469            file_id: real_file_id,
470            offset: self.offset.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?,
471        })
472    }
473}
474
475impl UpmapFromRaFixture for FileRangeWrapper<FileId> {
476    fn upmap_from_ra_fixture(
477        self,
478        analysis: &RaFixtureAnalysis,
479        _virtual_file_id: FileId,
480        real_file_id: FileId,
481    ) -> Result<Self, ()> {
482        Ok(FileRangeWrapper {
483            file_id: real_file_id,
484            range: self.range.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?,
485        })
486    }
487}
488
489#[macro_export]
490macro_rules! impl_empty_upmap_from_ra_fixture {
491    ( $( $ty:ty ),* $(,)? ) => {
492        $(
493            impl $crate::ra_fixture::UpmapFromRaFixture for $ty {
494                fn upmap_from_ra_fixture(
495                    self,
496                    _analysis: &$crate::ra_fixture::RaFixtureAnalysis,
497                    _virtual_file_id: $crate::ra_fixture::FileId,
498                    _real_file_id: $crate::ra_fixture::FileId,
499                ) -> Result<Self, ()> {
500                    Ok(self)
501                }
502            }
503        )*
504    };
505}
506
507impl_empty_upmap_from_ra_fixture!(
508    bool,
509    i8,
510    i16,
511    i32,
512    i64,
513    i128,
514    u8,
515    u16,
516    u32,
517    u64,
518    u128,
519    f32,
520    f64,
521    &str,
522    String,
523    Symbol,
524    SmolStr,
525    Documentation<'_>,
526    SymbolKind,
527    CfgExpr,
528    ReferenceCategory,
529);