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