ide/
static_index.rs

1//! This module provides `StaticIndex` which is used for powering
2//! read-only code browsers and emitting LSIF
3
4use arrayvec::ArrayVec;
5use hir::{Crate, Module, Semantics, db::HirDatabase};
6use ide_db::{
7    FileId, FileRange, FxHashMap, FxHashSet, MiniCore, RootDatabase,
8    base_db::{RootQueryDb, SourceDatabase, VfsPath},
9    defs::{Definition, IdentClass},
10    documentation::Documentation,
11    famous_defs::FamousDefs,
12};
13use syntax::{AstNode, SyntaxNode, SyntaxToken, TextRange};
14
15use crate::navigation_target::UpmappingResult;
16use crate::{
17    Analysis, Fold, HoverConfig, HoverResult, InlayHint, InlayHintsConfig, TryToNav,
18    hover::{SubstTyLen, hover_for_definition},
19    inlay_hints::{AdjustmentHintsMode, InlayFieldsToResolve},
20    moniker::{MonikerResult, SymbolInformationKind, def_to_kind, def_to_moniker},
21    parent_module::crates_for,
22};
23
24/// A static representation of fully analyzed source code.
25///
26/// The intended use-case is powering read-only code browsers and emitting LSIF/SCIP.
27#[derive(Debug)]
28pub struct StaticIndex<'a> {
29    pub files: Vec<StaticIndexedFile>,
30    pub tokens: TokenStore,
31    analysis: &'a Analysis,
32    db: &'a RootDatabase,
33    def_map: FxHashMap<Definition, TokenId>,
34}
35
36#[derive(Debug)]
37pub struct ReferenceData {
38    pub range: FileRange,
39    pub is_definition: bool,
40}
41
42#[derive(Debug)]
43pub struct TokenStaticData {
44    // FIXME: Make this have the lifetime of the database.
45    pub documentation: Option<Documentation<'static>>,
46    pub hover: Option<HoverResult>,
47    /// The position of the token itself.
48    ///
49    /// For example, in `fn foo() {}` this is the position of `foo`.
50    pub definition: Option<FileRange>,
51    /// The position of the entire definition that this token belongs to.
52    ///
53    /// For example, in `fn foo() {}` this is the position from `fn`
54    /// to the closing brace.
55    pub definition_body: Option<FileRange>,
56    pub references: Vec<ReferenceData>,
57    pub moniker: Option<MonikerResult>,
58    pub display_name: Option<String>,
59    pub signature: Option<String>,
60    pub kind: SymbolInformationKind,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct TokenId(usize);
65
66impl TokenId {
67    pub fn raw(self) -> usize {
68        self.0
69    }
70}
71
72#[derive(Default, Debug)]
73pub struct TokenStore(Vec<TokenStaticData>);
74
75impl TokenStore {
76    pub fn insert(&mut self, data: TokenStaticData) -> TokenId {
77        let id = TokenId(self.0.len());
78        self.0.push(data);
79        id
80    }
81
82    pub fn get_mut(&mut self, id: TokenId) -> Option<&mut TokenStaticData> {
83        self.0.get_mut(id.0)
84    }
85
86    pub fn get(&self, id: TokenId) -> Option<&TokenStaticData> {
87        self.0.get(id.0)
88    }
89
90    pub fn iter(self) -> impl Iterator<Item = (TokenId, TokenStaticData)> {
91        self.0.into_iter().enumerate().map(|(id, data)| (TokenId(id), data))
92    }
93}
94
95#[derive(Debug)]
96pub struct StaticIndexedFile {
97    pub file_id: FileId,
98    pub folds: Vec<Fold>,
99    pub inlay_hints: Vec<InlayHint>,
100    pub tokens: Vec<(TextRange, TokenId)>,
101}
102
103fn all_modules(db: &dyn HirDatabase) -> Vec<Module> {
104    let mut worklist: Vec<_> =
105        Crate::all(db).into_iter().map(|krate| krate.root_module(db)).collect();
106    let mut modules = Vec::new();
107
108    while let Some(module) = worklist.pop() {
109        modules.push(module);
110        worklist.extend(module.children(db));
111    }
112
113    modules
114}
115
116fn documentation_for_definition(
117    sema: &Semantics<'_, RootDatabase>,
118    def: Definition,
119    scope_node: &SyntaxNode,
120) -> Option<Documentation<'static>> {
121    let famous_defs = match &def {
122        Definition::BuiltinType(_) => Some(FamousDefs(sema, sema.scope(scope_node)?.krate())),
123        _ => None,
124    };
125
126    def.docs(
127        sema.db,
128        famous_defs.as_ref(),
129        def.krate(sema.db)
130            .unwrap_or_else(|| {
131                (*sema.db.all_crates().last().expect("no crate graph present")).into()
132            })
133            .to_display_target(sema.db),
134    )
135    .map(Documentation::into_owned)
136}
137
138// FIXME: This is a weird function
139fn get_definitions<'db>(
140    sema: &Semantics<'db, RootDatabase>,
141    token: SyntaxToken,
142) -> Option<ArrayVec<(Definition, Option<hir::GenericSubstitution<'db>>), 2>> {
143    for token in sema.descend_into_macros_exact(token) {
144        let def = IdentClass::classify_token(sema, &token).map(IdentClass::definitions);
145        if let Some(defs) = def
146            && !defs.is_empty()
147        {
148            return Some(defs);
149        }
150    }
151    None
152}
153
154pub enum VendoredLibrariesConfig<'a> {
155    Included { workspace_root: &'a VfsPath },
156    Excluded,
157}
158
159impl StaticIndex<'_> {
160    fn add_file(&mut self, file_id: FileId) {
161        let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
162        let folds = self.analysis.folding_ranges(file_id).unwrap();
163        let inlay_hints = self
164            .analysis
165            .inlay_hints(
166                &InlayHintsConfig {
167                    render_colons: true,
168                    discriminant_hints: crate::DiscriminantHints::Fieldless,
169                    type_hints: true,
170                    sized_bound: false,
171                    parameter_hints: true,
172                    parameter_hints_for_missing_arguments: false,
173                    generic_parameter_hints: crate::GenericParameterHints {
174                        type_hints: false,
175                        lifetime_hints: false,
176                        const_hints: true,
177                    },
178                    chaining_hints: true,
179                    closure_return_type_hints: crate::ClosureReturnTypeHints::WithBlock,
180                    lifetime_elision_hints: crate::LifetimeElisionHints::Never,
181                    adjustment_hints: crate::AdjustmentHints::Never,
182                    adjustment_hints_disable_reborrows: true,
183                    adjustment_hints_mode: AdjustmentHintsMode::Prefix,
184                    adjustment_hints_hide_outside_unsafe: false,
185                    implicit_drop_hints: false,
186                    implied_dyn_trait_hints: false,
187                    hide_inferred_type_hints: false,
188                    hide_named_constructor_hints: false,
189                    hide_closure_initialization_hints: false,
190                    hide_closure_parameter_hints: false,
191                    closure_style: hir::ClosureStyle::ImplFn,
192                    param_names_for_lifetime_elision_hints: false,
193                    binding_mode_hints: false,
194                    max_length: Some(25),
195                    closure_capture_hints: false,
196                    closing_brace_hints_min_lines: Some(25),
197                    fields_to_resolve: InlayFieldsToResolve::empty(),
198                    range_exclusive_hints: false,
199                    minicore: MiniCore::default(),
200                },
201                file_id,
202                None,
203            )
204            .unwrap();
205        // hovers
206        let sema = hir::Semantics::new(self.db);
207        let root = sema.parse_guess_edition(file_id).syntax().clone();
208        let edition = sema.attach_first_edition(file_id).edition(sema.db);
209        let display_target = match sema.first_crate(file_id) {
210            Some(krate) => krate.to_display_target(sema.db),
211            None => return,
212        };
213        let tokens = root.descendants_with_tokens().filter_map(|it| match it {
214            syntax::NodeOrToken::Node(_) => None,
215            syntax::NodeOrToken::Token(it) => Some(it),
216        });
217        let hover_config = HoverConfig {
218            links_in_hover: true,
219            memory_layout: None,
220            documentation: true,
221            keywords: true,
222            format: crate::HoverDocFormat::Markdown,
223            max_trait_assoc_items_count: None,
224            max_fields_count: Some(5),
225            max_enum_variants_count: Some(5),
226            max_subst_ty_len: SubstTyLen::Unlimited,
227            show_drop_glue: true,
228            minicore: MiniCore::default(),
229        };
230        let mut result = StaticIndexedFile { file_id, inlay_hints, folds, tokens: vec![] };
231
232        let mut add_token = |def: Definition, range: TextRange, scope_node: &SyntaxNode| {
233            let id = if let Some(it) = self.def_map.get(&def) {
234                *it
235            } else {
236                let it = self.tokens.insert(TokenStaticData {
237                    documentation: documentation_for_definition(&sema, def, scope_node),
238                    hover: Some(hover_for_definition(
239                        &sema,
240                        file_id,
241                        def,
242                        None,
243                        scope_node,
244                        None,
245                        false,
246                        &hover_config,
247                        edition,
248                        display_target,
249                    )),
250                    definition: def.try_to_nav(&sema).map(UpmappingResult::call_site).map(|it| {
251                        FileRange { file_id: it.file_id, range: it.focus_or_full_range() }
252                    }),
253                    definition_body: def
254                        .try_to_nav(&sema)
255                        .map(UpmappingResult::call_site)
256                        .map(|it| FileRange { file_id: it.file_id, range: it.full_range }),
257                    references: vec![],
258                    moniker: current_crate.and_then(|cc| def_to_moniker(self.db, def, cc)),
259                    display_name: def
260                        .name(self.db)
261                        .map(|name| name.display(self.db, edition).to_string()),
262                    signature: Some(def.label(self.db, display_target)),
263                    kind: def_to_kind(self.db, def),
264                });
265                self.def_map.insert(def, it);
266                it
267            };
268            let token = self.tokens.get_mut(id).unwrap();
269            token.references.push(ReferenceData {
270                range: FileRange { range, file_id },
271                is_definition: match def.try_to_nav(&sema).map(UpmappingResult::call_site) {
272                    Some(it) => it.file_id == file_id && it.focus_or_full_range() == range,
273                    None => false,
274                },
275            });
276            result.tokens.push((range, id));
277        };
278
279        if let Some(module) = sema.file_to_module_def(file_id) {
280            let def = Definition::Module(module);
281            let range = root.text_range();
282            add_token(def, range, &root);
283        }
284
285        for token in tokens {
286            let range = token.text_range();
287            let node = token.parent().unwrap();
288            match hir::attach_db(self.db, || get_definitions(&sema, token.clone())) {
289                Some(defs) => {
290                    for (def, _) in defs {
291                        add_token(def, range, &node);
292                    }
293                }
294                None => continue,
295            };
296        }
297        self.files.push(result);
298    }
299
300    pub fn compute<'a>(
301        analysis: &'a Analysis,
302        vendored_libs_config: VendoredLibrariesConfig<'_>,
303    ) -> StaticIndex<'a> {
304        let db = &analysis.db;
305        hir::attach_db(db, || {
306            let work = all_modules(db).into_iter().filter(|module| {
307                let file_id = module.definition_source_file_id(db).original_file(db);
308                let source_root =
309                    db.file_source_root(file_id.file_id(&analysis.db)).source_root_id(db);
310                let source_root = db.source_root(source_root).source_root(db);
311                let is_vendored = match vendored_libs_config {
312                    VendoredLibrariesConfig::Included { workspace_root } => source_root
313                        .path_for_file(&file_id.file_id(&analysis.db))
314                        .is_some_and(|module_path| module_path.starts_with(workspace_root)),
315                    VendoredLibrariesConfig::Excluded => false,
316                };
317
318                !source_root.is_library || is_vendored
319            });
320            let mut this = StaticIndex {
321                files: vec![],
322                tokens: Default::default(),
323                analysis,
324                db,
325                def_map: Default::default(),
326            };
327            let mut visited_files = FxHashSet::default();
328            for module in work {
329                let file_id =
330                    module.definition_source_file_id(db).original_file(db).file_id(&analysis.db);
331                if visited_files.contains(&file_id) {
332                    continue;
333                }
334                this.add_file(file_id);
335                visited_files.insert(file_id);
336            }
337            this
338        })
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use crate::{StaticIndex, fixture};
345    use ide_db::{FileRange, FxHashMap, FxHashSet, base_db::VfsPath};
346    use syntax::TextSize;
347
348    use super::VendoredLibrariesConfig;
349
350    fn check_all_ranges(
351        #[rust_analyzer::rust_fixture] ra_fixture: &str,
352        vendored_libs_config: VendoredLibrariesConfig<'_>,
353    ) {
354        let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture);
355        let s = StaticIndex::compute(&analysis, vendored_libs_config);
356        let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect();
357        for f in s.files {
358            for (range, _) in f.tokens {
359                if range.start() == TextSize::from(0) {
360                    // ignore whole file range corresponding to module definition
361                    continue;
362                }
363                let it = FileRange { file_id: f.file_id, range };
364                if !range_set.contains(&it) {
365                    panic!("additional range {it:?}");
366                }
367                range_set.remove(&it);
368            }
369        }
370        if !range_set.is_empty() {
371            panic!("unfound ranges {range_set:?}");
372        }
373    }
374
375    #[track_caller]
376    fn check_definitions(
377        #[rust_analyzer::rust_fixture] ra_fixture: &str,
378        vendored_libs_config: VendoredLibrariesConfig<'_>,
379    ) {
380        let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture);
381        let s = StaticIndex::compute(&analysis, vendored_libs_config);
382        let mut range_set: FxHashSet<_> = ranges.iter().map(|it| it.0).collect();
383        for (_, t) in s.tokens.iter() {
384            if let Some(t) = t.definition {
385                if t.range.start() == TextSize::from(0) {
386                    // ignore definitions that are whole of file
387                    continue;
388                }
389                if !range_set.contains(&t) {
390                    panic!("additional definition {t:?}");
391                }
392                range_set.remove(&t);
393            }
394        }
395        if !range_set.is_empty() {
396            panic!("unfound definitions {range_set:?}");
397        }
398    }
399
400    #[track_caller]
401    fn check_references(
402        #[rust_analyzer::rust_fixture] ra_fixture: &str,
403        vendored_libs_config: VendoredLibrariesConfig<'_>,
404    ) {
405        let (analysis, ranges) = fixture::annotations_without_marker(ra_fixture);
406        let s = StaticIndex::compute(&analysis, vendored_libs_config);
407        let mut range_set: FxHashMap<_, i32> = ranges.iter().map(|it| (it.0, 0)).collect();
408
409        // Make sure that all references have at least one range. We use a HashMap instead of a
410        // a HashSet so that we can have more than one reference at the same range.
411        for (_, t) in s.tokens.iter() {
412            for r in &t.references {
413                if r.is_definition {
414                    continue;
415                }
416                if r.range.range.start() == TextSize::from(0) {
417                    // ignore whole file range corresponding to module definition
418                    continue;
419                }
420                match range_set.entry(r.range) {
421                    std::collections::hash_map::Entry::Occupied(mut entry) => {
422                        let count = entry.get_mut();
423                        *count += 1;
424                    }
425                    std::collections::hash_map::Entry::Vacant(_) => {
426                        panic!("additional reference {r:?}");
427                    }
428                }
429            }
430        }
431        for (range, count) in range_set.iter() {
432            if *count == 0 {
433                panic!("unfound reference {range:?}");
434            }
435        }
436    }
437
438    #[test]
439    fn field_initialization() {
440        check_references(
441            r#"
442struct Point {
443    x: f64,
444     //^^^
445    y: f64,
446     //^^^
447}
448    fn foo() {
449        let x = 5.;
450        let y = 10.;
451        let mut p = Point { x, y };
452                  //^^^^^   ^  ^
453        p.x = 9.;
454      //^ ^
455        p.y = 10.;
456      //^ ^
457    }
458"#,
459            VendoredLibrariesConfig::Included {
460                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
461            },
462        );
463    }
464
465    #[test]
466    fn struct_and_enum() {
467        check_all_ranges(
468            r#"
469struct Foo;
470     //^^^
471enum E { X(Foo) }
472   //^   ^ ^^^
473"#,
474            VendoredLibrariesConfig::Included {
475                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
476            },
477        );
478        check_definitions(
479            r#"
480struct Foo;
481     //^^^
482enum E { X(Foo) }
483   //^   ^
484"#,
485            VendoredLibrariesConfig::Included {
486                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
487            },
488        );
489
490        check_references(
491            r#"
492struct Foo;
493enum E { X(Foo) }
494   //      ^^^
495"#,
496            VendoredLibrariesConfig::Included {
497                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
498            },
499        );
500    }
501
502    #[test]
503    fn multi_crate() {
504        check_definitions(
505            r#"
506//- /workspace/main.rs crate:main deps:foo
507
508
509use foo::func;
510
511fn main() {
512 //^^^^
513    func();
514}
515//- /workspace/foo/lib.rs crate:foo
516
517pub func() {
518
519}
520"#,
521            VendoredLibrariesConfig::Included {
522                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
523            },
524        );
525    }
526
527    #[test]
528    fn vendored_crate() {
529        check_all_ranges(
530            r#"
531//- /workspace/main.rs crate:main deps:external,vendored
532struct Main(i32);
533     //^^^^ ^^^
534
535//- /external/lib.rs new_source_root:library crate:external@0.1.0,https://a.b/foo.git library
536struct ExternalLibrary(i32);
537
538//- /workspace/vendored/lib.rs new_source_root:library crate:vendored@0.1.0,https://a.b/bar.git library
539struct VendoredLibrary(i32);
540     //^^^^^^^^^^^^^^^ ^^^
541"#,
542            VendoredLibrariesConfig::Included {
543                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
544            },
545        );
546    }
547
548    #[test]
549    fn vendored_crate_excluded() {
550        check_all_ranges(
551            r#"
552//- /workspace/main.rs crate:main deps:external,vendored
553struct Main(i32);
554     //^^^^ ^^^
555
556//- /external/lib.rs new_source_root:library crate:external@0.1.0,https://a.b/foo.git library
557struct ExternalLibrary(i32);
558
559//- /workspace/vendored/lib.rs new_source_root:library crate:vendored@0.1.0,https://a.b/bar.git library
560struct VendoredLibrary(i32);
561"#,
562            VendoredLibrariesConfig::Excluded,
563        )
564    }
565
566    #[test]
567    fn derives() {
568        check_all_ranges(
569            r#"
570//- minicore:derive
571#[rustc_builtin_macro]
572//^^^^^^^^^^^^^^^^^^^
573pub macro Copy {}
574        //^^^^
575#[derive(Copy)]
576//^^^^^^ ^^^^
577struct Hello(i32);
578     //^^^^^ ^^^
579"#,
580            VendoredLibrariesConfig::Included {
581                workspace_root: &VfsPath::new_virtual_path("/workspace".to_owned()),
582            },
583        );
584    }
585}