1use 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 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 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() == "rust_analyzer" && segment2.text() == "rust_fixture"
106 })
107 });
108 if !has_rust_fixture_attr {
109 return None;
110 }
111 let value = literal.value().ok()?;
112
113 let mut mapper = RangeMapper::default();
114
115 let mut offset_with_indent = TextSize::new(0);
118 let mut offset_without_indent = TextSize::new(0);
124
125 let mut text = &*value;
126 if let Some(t) = text.strip_prefix('\n') {
127 offset_with_indent += TextSize::of("\n");
128 text = t;
129 }
130 let mut line_offsets = Vec::new();
132 for mut line in text.split_inclusive('\n') {
133 line_offsets.push(offset_without_indent);
134
135 if line.starts_with("@@") {
136 mapper.add("//", TextRange::at(offset_with_indent, TextSize::of("@@")));
139 line = &line["@@".len()..];
140 offset_with_indent += TextSize::of("@@");
141 offset_without_indent += TextSize::of("@@");
142 }
143
144 let mut unindented_line = line.trim_start();
147 if unindented_line.is_empty() {
148 unindented_line = "\n";
150 }
151 offset_with_indent += TextSize::of(line) - TextSize::of(unindented_line);
152
153 let marker = "$0";
154 match unindented_line.find(marker) {
155 Some(marker_pos) => {
156 let (before_marker, after_marker) = unindented_line.split_at(marker_pos);
157 let after_marker = &after_marker[marker.len()..];
158
159 mapper.add(
160 before_marker,
161 TextRange::at(offset_with_indent, TextSize::of(before_marker)),
162 );
163 offset_with_indent += TextSize::of(before_marker);
164 offset_without_indent += TextSize::of(before_marker);
165
166 if let Some(marker_range) = literal
167 .map_range_up(TextRange::at(offset_with_indent, TextSize::of(marker)))
168 {
169 on_cursor(marker_range);
170 }
171 offset_with_indent += TextSize::of(marker);
172
173 mapper.add(
174 after_marker,
175 TextRange::at(offset_with_indent, TextSize::of(after_marker)),
176 );
177 offset_with_indent += TextSize::of(after_marker);
178 offset_without_indent += TextSize::of(after_marker);
179 }
180 None => {
181 mapper.add(
182 unindented_line,
183 TextRange::at(offset_with_indent, TextSize::of(unindented_line)),
184 );
185 offset_with_indent += TextSize::of(unindented_line);
186 offset_without_indent += TextSize::of(unindented_line);
187 }
188 }
189 }
190
191 let combined = mapper.take_text();
192 let combined_len = TextSize::of(&combined);
193 let (analysis, tmp_file_ids, sysroot_files) =
194 RootDatabase::from_ra_fixture(&combined, minicore).ok()?;
195
196 let mut virtual_file_id_to_line = Vec::new();
198 for &(file_id, line) in &tmp_file_ids {
199 virtual_file_id_to_line.resize(file_id.index() as usize + 1, usize::MAX);
200 virtual_file_id_to_line[file_id.index() as usize] = line;
201 }
202
203 Some(RaFixtureAnalysis {
204 db: analysis,
205 tmp_file_ids,
206 line_offsets,
207 virtual_file_id_to_line,
208 mapper,
209 literal,
210 sysroot_files,
211 combined_len,
212 })
213 }
214
215 pub fn files(&self) -> impl Iterator<Item = FileId> {
216 self.tmp_file_ids.iter().map(|(file, _)| *file)
217 }
218
219 fn virtual_file_id_to_line(&self, file_id: FileId) -> Option<usize> {
221 if self.is_sysroot_file(file_id) {
222 None
223 } else {
224 Some(self.virtual_file_id_to_line[file_id.index() as usize])
225 }
226 }
227
228 pub fn map_offset_down(&self, offset: TextSize) -> Option<(FileId, TextSize)> {
229 let inside_literal_range = self.literal.map_offset_down(offset)?;
230 let combined_offset = self.mapper.map_offset_down(inside_literal_range)?;
231 let (_, &(file_id, file_line)) =
233 self.tmp_file_ids.iter().enumerate().find(|&(idx, &(_, file_line))| {
234 let file_start = self.line_offsets[file_line];
235 let file_end = self
236 .tmp_file_ids
237 .get(idx + 1)
238 .map(|&(_, next_file_line)| self.line_offsets[next_file_line])
239 .unwrap_or_else(|| self.combined_len);
240 TextRange::new(file_start, file_end).contains(combined_offset)
241 })?;
242 let file_line_offset = self.line_offsets[file_line];
243 let file_offset = combined_offset - file_line_offset;
244 Some((file_id, file_offset))
245 }
246
247 pub fn map_range_down(&self, range: TextRange) -> Option<(FileId, TextRange)> {
248 let (start_file_id, start_offset) = self.map_offset_down(range.start())?;
249 let (end_file_id, end_offset) = self.map_offset_down(range.end())?;
250 if start_file_id != end_file_id {
251 None
252 } else {
253 Some((start_file_id, TextRange::new(start_offset, end_offset)))
254 }
255 }
256
257 pub fn map_range_up(
258 &self,
259 virtual_file: FileId,
260 range: TextRange,
261 ) -> impl Iterator<Item = TextRange> {
262 self.virtual_file_id_to_line(virtual_file)
264 .and_then(|line| self.line_offsets.get(line))
265 .into_iter()
266 .flat_map(move |&tmp_file_offset| {
267 let range = range + tmp_file_offset;
269 self.mapper.map_range_up(range)
271 })
272 .filter_map(|range| self.literal.map_range_up(range))
274 }
275
276 pub fn map_offset_up(&self, virtual_file: FileId, offset: TextSize) -> Option<TextSize> {
277 self.map_range_up(virtual_file, TextRange::empty(offset)).next().map(|range| range.start())
278 }
279
280 pub fn is_sysroot_file(&self, file_id: FileId) -> bool {
281 self.sysroot_files.contains(&file_id)
282 }
283}
284
285pub trait UpmapFromRaFixture: Sized {
286 fn upmap_from_ra_fixture(
287 self,
288 analysis: &RaFixtureAnalysis,
289 virtual_file_id: FileId,
290 real_file_id: FileId,
291 ) -> Result<Self, ()>;
292}
293
294trait IsEmpty {
295 fn is_empty(&self) -> bool;
296}
297
298impl<T> IsEmpty for Vec<T> {
299 fn is_empty(&self) -> bool {
300 self.is_empty()
301 }
302}
303
304impl<T, const N: usize> IsEmpty for SmallVec<[T; N]> {
305 fn is_empty(&self) -> bool {
306 self.is_empty()
307 }
308}
309
310#[allow(clippy::disallowed_types)]
311impl<K, V, S> IsEmpty for std::collections::HashMap<K, V, S> {
312 fn is_empty(&self) -> bool {
313 self.is_empty()
314 }
315}
316
317fn upmap_collection<T, Collection>(
318 collection: Collection,
319 analysis: &RaFixtureAnalysis,
320 virtual_file_id: FileId,
321 real_file_id: FileId,
322) -> Result<Collection, ()>
323where
324 T: UpmapFromRaFixture,
325 Collection: IntoIterator<Item = T> + FromIterator<T> + IsEmpty,
326{
327 if collection.is_empty() {
328 return Ok(collection);
330 }
331 let result = collection
332 .into_iter()
333 .filter_map(|item| item.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok())
334 .collect::<Collection>();
335 if result.is_empty() {
336 Err(())
338 } else {
339 Ok(result)
340 }
341}
342
343impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Option<T> {
344 fn upmap_from_ra_fixture(
345 self,
346 analysis: &RaFixtureAnalysis,
347 virtual_file_id: FileId,
348 real_file_id: FileId,
349 ) -> Result<Self, ()> {
350 Ok(match self {
351 Some(it) => Some(it.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?),
352 None => None,
353 })
354 }
355}
356
357impl<T: UpmapFromRaFixture> UpmapFromRaFixture for Vec<T> {
358 fn upmap_from_ra_fixture(
359 self,
360 analysis: &RaFixtureAnalysis,
361 virtual_file_id: FileId,
362 real_file_id: FileId,
363 ) -> Result<Self, ()> {
364 upmap_collection(self, analysis, virtual_file_id, real_file_id)
365 }
366}
367
368impl<T: UpmapFromRaFixture, const N: usize> UpmapFromRaFixture for SmallVec<[T; N]> {
369 fn upmap_from_ra_fixture(
370 self,
371 analysis: &RaFixtureAnalysis,
372 virtual_file_id: FileId,
373 real_file_id: FileId,
374 ) -> Result<Self, ()> {
375 upmap_collection(self, analysis, virtual_file_id, real_file_id)
376 }
377}
378
379#[allow(clippy::disallowed_types)]
380impl<K: UpmapFromRaFixture + Hash + Eq, V: UpmapFromRaFixture, S: BuildHasher + Default>
381 UpmapFromRaFixture for std::collections::HashMap<K, V, S>
382{
383 fn upmap_from_ra_fixture(
384 self,
385 analysis: &RaFixtureAnalysis,
386 virtual_file_id: FileId,
387 real_file_id: FileId,
388 ) -> Result<Self, ()> {
389 upmap_collection(self, analysis, virtual_file_id, real_file_id)
390 }
391}
392
393#[allow(clippy::disallowed_types)]
395impl<V: UpmapFromRaFixture, S: BuildHasher + Default> UpmapFromRaFixture
396 for std::collections::HashMap<FileId, V, S>
397{
398 fn upmap_from_ra_fixture(
399 self,
400 analysis: &RaFixtureAnalysis,
401 _virtual_file_id: FileId,
402 real_file_id: FileId,
403 ) -> Result<Self, ()> {
404 if self.is_empty() {
405 return Ok(self);
406 }
407 let result = self
408 .into_iter()
409 .filter_map(|(virtual_file_id, value)| {
410 Some((
411 real_file_id,
412 value.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id).ok()?,
413 ))
414 })
415 .collect::<std::collections::HashMap<_, _, _>>();
416 if result.is_empty() { Err(()) } else { Ok(result) }
417 }
418}
419
420macro_rules! impl_tuple {
421 () => {}; ( $first:ident, $( $rest:ident, )* ) => {
423 impl<
424 $first: UpmapFromRaFixture,
425 $( $rest: UpmapFromRaFixture, )*
426 > UpmapFromRaFixture for ( $first, $( $rest, )* ) {
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 #[allow(non_snake_case)]
434 let ( $first, $($rest,)* ) = self;
435 Ok((
436 $first.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?,
437 $( $rest.upmap_from_ra_fixture(analysis, virtual_file_id, real_file_id)?, )*
438 ))
439 }
440 }
441
442 impl_tuple!( $($rest,)* );
443 };
444}
445impl_tuple!(A, B, C, D, E,);
446
447impl UpmapFromRaFixture for TextSize {
448 fn upmap_from_ra_fixture(
449 self,
450 analysis: &RaFixtureAnalysis,
451 virtual_file_id: FileId,
452 _real_file_id: FileId,
453 ) -> Result<Self, ()> {
454 analysis.map_offset_up(virtual_file_id, self).ok_or(())
455 }
456}
457
458impl UpmapFromRaFixture for TextRange {
459 fn upmap_from_ra_fixture(
460 self,
461 analysis: &RaFixtureAnalysis,
462 virtual_file_id: FileId,
463 _real_file_id: FileId,
464 ) -> Result<Self, ()> {
465 analysis.map_range_up(virtual_file_id, self).next().ok_or(())
466 }
467}
468
469impl UpmapFromRaFixture for FilePositionWrapper<FileId> {
483 fn upmap_from_ra_fixture(
484 self,
485 analysis: &RaFixtureAnalysis,
486 _virtual_file_id: FileId,
487 real_file_id: FileId,
488 ) -> Result<Self, ()> {
489 Ok(FilePositionWrapper {
490 file_id: real_file_id,
491 offset: self.offset.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?,
492 })
493 }
494}
495
496impl UpmapFromRaFixture for FileRangeWrapper<FileId> {
497 fn upmap_from_ra_fixture(
498 self,
499 analysis: &RaFixtureAnalysis,
500 _virtual_file_id: FileId,
501 real_file_id: FileId,
502 ) -> Result<Self, ()> {
503 Ok(FileRangeWrapper {
504 file_id: real_file_id,
505 range: self.range.upmap_from_ra_fixture(analysis, self.file_id, real_file_id)?,
506 })
507 }
508}
509
510#[macro_export]
511macro_rules! impl_empty_upmap_from_ra_fixture {
512 ( $( $ty:ty ),* $(,)? ) => {
513 $(
514 impl $crate::ra_fixture::UpmapFromRaFixture for $ty {
515 fn upmap_from_ra_fixture(
516 self,
517 _analysis: &$crate::ra_fixture::RaFixtureAnalysis,
518 _virtual_file_id: $crate::ra_fixture::FileId,
519 _real_file_id: $crate::ra_fixture::FileId,
520 ) -> Result<Self, ()> {
521 Ok(self)
522 }
523 }
524 )*
525 };
526}
527
528impl_empty_upmap_from_ra_fixture!(
529 bool,
530 i8,
531 i16,
532 i32,
533 i64,
534 i128,
535 u8,
536 u16,
537 u32,
538 u64,
539 u128,
540 f32,
541 f64,
542 &str,
543 String,
544 Symbol,
545 SmolStr,
546 Documentation<'_>,
547 SymbolKind,
548 CfgExpr,
549 ReferenceCategory,
550);