Skip to main content

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