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_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 let mut offset_with_indent = TextSize::new(0);
119 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 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 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 let mut unindented_line = line.trim_start();
148 if unindented_line.is_empty() {
149 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 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 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 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 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 let range = range + tmp_file_offset;
270 self.mapper.map_range_up(range)
272 })
273 .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 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 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#[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 () => {}; ( $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
470impl 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);