1use 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 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 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 let mut offset_with_indent = TextSize::new(0);
114 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 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 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 let mut unindented_line = line.trim_start();
143 if unindented_line.is_empty() {
144 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 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 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 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 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 let range = range + tmp_file_offset;
265 self.mapper.map_range_up(range)
267 })
268 .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 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 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#[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 () => {}; ( $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
465impl 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);