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