Unnamed repository; edit this file 'description' to name the repository.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
//! This module resolves `mod foo;` declaration to file.
use arrayvec::ArrayVec;
use base_db::AnchoredPath;
use hir_expand::{EditionedFileId, name::Name};

use crate::{HirFileId, db::DefDatabase};

const MOD_DEPTH_LIMIT: usize = 32;

#[derive(Clone, Debug)]
pub(super) struct ModDir {
    /// `` for `mod.rs`, `lib.rs`
    /// `foo/` for `foo.rs`
    /// `foo/bar/` for `mod bar { mod x; }` nested in `foo.rs`
    /// Invariant: path.is_empty() || path.ends_with('/')
    dir_path: DirPath,
    /// inside `./foo.rs`, mods with `#[path]` should *not* be relative to `./foo/`
    root_non_dir_owner: bool,
    depth: u32,
}

impl ModDir {
    pub(super) fn root() -> ModDir {
        ModDir { dir_path: DirPath::empty(), root_non_dir_owner: false, depth: 0 }
    }

    pub(super) fn descend_into_definition(
        &self,
        name: &Name,
        attr_path: Option<&str>,
    ) -> Option<ModDir> {
        let path = match attr_path {
            None => {
                let mut path = self.dir_path.clone();
                path.push(name.as_str());
                path
            }
            Some(attr_path) => {
                let mut path = self.dir_path.join_attr(attr_path, self.root_non_dir_owner);
                if !(path.is_empty() || path.ends_with('/')) {
                    path.push('/')
                }
                DirPath::new(path)
            }
        };
        self.child(path, false)
    }

    fn child(&self, dir_path: DirPath, root_non_dir_owner: bool) -> Option<ModDir> {
        let depth = self.depth + 1;
        if depth as usize > MOD_DEPTH_LIMIT {
            tracing::error!("MOD_DEPTH_LIMIT exceeded");
            cov_mark::hit!(circular_mods);
            return None;
        }
        Some(ModDir { dir_path, root_non_dir_owner, depth })
    }

    pub(super) fn resolve_declaration(
        &self,
        db: &dyn DefDatabase,
        file_id: HirFileId,
        name: &Name,
        attr_path: Option<&str>,
    ) -> Result<(EditionedFileId, bool, ModDir), Box<[String]>> {
        let name = name.as_str();

        let mut candidate_files = ArrayVec::<_, 2>::new();
        match attr_path {
            Some(attr_path) => {
                candidate_files.push(self.dir_path.join_attr(attr_path, self.root_non_dir_owner))
            }
            None => {
                candidate_files.push(format!("{}{}.rs", self.dir_path.0, name));
                candidate_files.push(format!("{}{}/mod.rs", self.dir_path.0, name));
            }
        };

        let orig_file_id = file_id.original_file_respecting_includes(db);
        for candidate in candidate_files.iter() {
            let path = AnchoredPath { anchor: orig_file_id.file_id(db), path: candidate.as_str() };
            if let Some(file_id) = db.resolve_path(path) {
                let is_mod_rs = candidate.ends_with("/mod.rs");

                let root_dir_owner = is_mod_rs || attr_path.is_some();
                let dir_path = if root_dir_owner {
                    DirPath::empty()
                } else {
                    DirPath::new(format!("{name}/"))
                };
                if let Some(mod_dir) = self.child(dir_path, !root_dir_owner) {
                    return Ok((
                        // FIXME: Edition, is this rightr?
                        EditionedFileId::new(db, file_id, orig_file_id.edition(db)),
                        is_mod_rs,
                        mod_dir,
                    ));
                }
            }
        }
        Err(candidate_files.into_iter().collect())
    }
}

#[derive(Clone, Debug)]
struct DirPath(String);

impl DirPath {
    fn assert_invariant(&self) {
        assert!(self.0.is_empty() || self.0.ends_with('/'));
    }
    fn new(repr: String) -> DirPath {
        let res = DirPath(repr);
        res.assert_invariant();
        res
    }
    fn empty() -> DirPath {
        DirPath::new(String::new())
    }
    fn push(&mut self, name: &str) {
        self.0.push_str(name);
        self.0.push('/');
        self.assert_invariant();
    }
    fn parent(&self) -> Option<&str> {
        if self.0.is_empty() {
            return None;
        };
        let idx =
            self.0[..self.0.len() - '/'.len_utf8()].rfind('/').map_or(0, |it| it + '/'.len_utf8());
        Some(&self.0[..idx])
    }
    /// So this is the case which doesn't really work I think if we try to be
    /// 100% platform agnostic:
    ///
    /// ```ignore
    /// mod a {
    ///     #[path="C://sad/face"]
    ///     mod b { mod c; }
    /// }
    /// ```
    ///
    /// Here, we need to join logical dir path to a string path from an
    /// attribute. Ideally, we should somehow losslessly communicate the whole
    /// construction to `FileLoader`.
    fn join_attr(&self, mut attr: &str, relative_to_parent: bool) -> String {
        let base = if relative_to_parent { self.parent().unwrap() } else { &self.0 };

        if attr.starts_with("./") {
            attr = &attr["./".len()..];
        }
        let tmp;
        let attr = if attr.contains('\\') {
            tmp = attr.replace('\\', "/");
            &tmp
        } else {
            attr
        };
        let res = format!("{base}{attr}");
        res
    }
}
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474
//! File symbol extraction.

use base_db::FxIndexSet;
use either::Either;
use hir_def::{
    AdtId, AssocItemId, Complete, DefWithBodyId, ExternCrateId, HasModule, ImplId, Lookup, MacroId,
    ModuleDefId, ModuleId, TraitId,
    db::DefDatabase,
    item_scope::{ImportId, ImportOrExternCrate, ImportOrGlob},
    per_ns::Item,
    src::{HasChildSource, HasSource},
    visibility::{Visibility, VisibilityExplicitness},
};
use hir_expand::{HirFileId, name::Name};
use hir_ty::{
    db::HirDatabase,
    display::{HirDisplay, hir_display_with_store},
};
use intern::Symbol;
use rustc_hash::FxHashMap;
use syntax::{AstNode, AstPtr, SmolStr, SyntaxNode, SyntaxNodePtr, ToSmolStr, ast::HasName};

use crate::{HasCrate, Module, ModuleDef, Semantics};

/// The actual data that is stored in the index. It should be as compact as
/// possible.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FileSymbol {
    pub name: Symbol,
    pub def: ModuleDef,
    pub loc: DeclarationLocation,
    pub container_name: Option<SmolStr>,
    /// Whether this symbol is a doc alias for the original symbol.
    pub is_alias: bool,
    pub is_assoc: bool,
    pub is_import: bool,
    pub do_not_complete: Complete,
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeclarationLocation {
    /// The file id for both the `ptr` and `name_ptr`.
    pub hir_file_id: HirFileId,
    /// This points to the whole syntax node of the declaration.
    pub ptr: SyntaxNodePtr,
    /// This points to the [`syntax::ast::Name`] identifier of the declaration.
    pub name_ptr: AstPtr<Either<syntax::ast::Name, syntax::ast::NameRef>>,
}

impl DeclarationLocation {
    pub fn syntax<DB: HirDatabase>(&self, sema: &Semantics<'_, DB>) -> SyntaxNode {
        let root = sema.parse_or_expand(self.hir_file_id);
        self.ptr.to_node(&root)
    }
}

/// Represents an outstanding module that the symbol collector must collect symbols from.
#[derive(Debug)]
struct SymbolCollectorWork {
    module_id: ModuleId,
    parent: Option<Name>,
}

pub struct SymbolCollector<'a> {
    db: &'a dyn HirDatabase,
    symbols: FxIndexSet<FileSymbol>,
    work: Vec<SymbolCollectorWork>,
    current_container_name: Option<SmolStr>,
}

/// Given a [`ModuleId`] and a [`HirDatabase`], use the DefMap for the module's crate to collect
/// all symbols that should be indexed for the given module.
impl<'a> SymbolCollector<'a> {
    pub fn new(db: &'a dyn HirDatabase) -> Self {
        SymbolCollector {
            db,
            symbols: Default::default(),
            work: Default::default(),
            current_container_name: None,
        }
    }

    pub fn new_module(db: &dyn HirDatabase, module: Module) -> Box<[FileSymbol]> {
        let mut symbol_collector = SymbolCollector::new(db);
        symbol_collector.collect(module);
        symbol_collector.finish()
    }

    pub fn collect(&mut self, module: Module) {
        let _p = tracing::info_span!("SymbolCollector::collect", ?module).entered();
        tracing::info!(?module, "SymbolCollector::collect");

        // The initial work is the root module we're collecting, additional work will
        // be populated as we traverse the module's definitions.
        self.work.push(SymbolCollectorWork { module_id: module.into(), parent: None });

        while let Some(work) = self.work.pop() {
            self.do_work(work);
        }
    }

    pub fn finish(self) -> Box<[FileSymbol]> {
        self.symbols.into_iter().collect()
    }

    fn do_work(&mut self, work: SymbolCollectorWork) {
        let _p = tracing::info_span!("SymbolCollector::do_work", ?work).entered();
        tracing::info!(?work, "SymbolCollector::do_work");
        self.db.unwind_if_revision_cancelled();

        let parent_name = work.parent.map(|name| name.as_str().to_smolstr());
        self.with_container_name(parent_name, |s| s.collect_from_module(work.module_id));
    }

    fn collect_from_module(&mut self, module_id: ModuleId) {
        let push_decl = |this: &mut Self, def, name| {
            match def {
                ModuleDefId::ModuleId(id) => this.push_module(id, name),
                ModuleDefId::FunctionId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::AdtId(AdtId::StructId(id)) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::AdtId(AdtId::EnumId(id)) => {
                    this.push_decl(id, name, false, None);
                    let enum_name = this.db.enum_signature(id).name.as_str().to_smolstr();
                    this.with_container_name(Some(enum_name), |this| {
                        let variants = id.enum_variants(this.db);
                        for (variant_id, variant_name, _) in &variants.variants {
                            this.push_decl(*variant_id, variant_name, true, None);
                        }
                    });
                }
                ModuleDefId::AdtId(AdtId::UnionId(id)) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::ConstId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::StaticId(id) => {
                    this.push_decl(id, name, false, None);
                    this.collect_from_body(id, Some(name.clone()));
                }
                ModuleDefId::TraitId(id) => {
                    let trait_do_not_complete = this.push_decl(id, name, false, None);
                    this.collect_from_trait(id, trait_do_not_complete);
                }
                ModuleDefId::TraitAliasId(id) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::TypeAliasId(id) => {
                    this.push_decl(id, name, false, None);
                }
                ModuleDefId::MacroId(id) => {
                    match id {
                        MacroId::Macro2Id(id) => this.push_decl(id, name, false, None),
                        MacroId::MacroRulesId(id) => this.push_decl(id, name, false, None),
                        MacroId::ProcMacroId(id) => this.push_decl(id, name, false, None),
                    };
                }
                // Don't index these.
                ModuleDefId::BuiltinType(_) => {}
                ModuleDefId::EnumVariantId(_) => {}
            }
        };

        // Nested trees are very common, so a cache here will hit a lot.
        let import_child_source_cache = &mut FxHashMap::default();

        let is_explicit_import = |vis| match vis {
            Visibility::Public => true,
            Visibility::PubCrate(_) => true,
            Visibility::Module(_, VisibilityExplicitness::Explicit) => true,
            Visibility::Module(_, VisibilityExplicitness::Implicit) => false,
        };

        let mut push_import = |this: &mut Self, i: ImportId, name: &Name, def: ModuleDefId, vis| {
            let source = import_child_source_cache
                .entry(i.use_)
                .or_insert_with(|| i.use_.child_source(this.db));
            let Some(use_tree_src) = source.value.get(i.idx) else { return };
            let rename = use_tree_src.rename().and_then(|rename| rename.name());
            let name_syntax = match rename {
                Some(name) => Some(Either::Left(name)),
                None if is_explicit_import(vis) => {
                    (|| use_tree_src.path()?.segment()?.name_ref().map(Either::Right))()
                }
                None => None,
            };
            let Some(name_syntax) = name_syntax else {
                return;
            };
            let dec_loc = DeclarationLocation {
                hir_file_id: source.file_id,
                ptr: SyntaxNodePtr::new(use_tree_src.syntax()),
                name_ptr: AstPtr::new(&name_syntax),
            };
            this.symbols.insert(FileSymbol {
                name: name.symbol().clone(),
                def: def.into(),
                container_name: this.current_container_name.clone(),
                loc: dec_loc,
                is_alias: false,
                is_assoc: false,
                is_import: true,
                do_not_complete: Complete::Yes,
            });
        };

        let push_extern_crate =
            |this: &mut Self, i: ExternCrateId, name: &Name, def: ModuleDefId, vis| {
                let loc = i.lookup(this.db);
                let source = loc.source(this.db);
                let rename = source.value.rename().and_then(|rename| rename.name());

                let name_syntax = match rename {
                    Some(name) => Some(Either::Left(name)),
                    None if is_explicit_import(vis) => None,
                    None => source.value.name_ref().map(Either::Right),
                };
                let Some(name_syntax) = name_syntax else {
                    return;
                };
                let dec_loc = DeclarationLocation {
                    hir_file_id: source.file_id,
                    ptr: SyntaxNodePtr::new(source.value.syntax()),
                    name_ptr: AstPtr::new(&name_syntax),
                };
                this.symbols.insert(FileSymbol {
                    name: name.symbol().clone(),
                    def: def.into(),
                    container_name: this.current_container_name.clone(),
                    loc: dec_loc,
                    is_alias: false,
                    is_assoc: false,
                    is_import: false,
                    do_not_complete: Complete::Yes,
                });
            };

        let def_map = module_id.def_map(self.db);
        let scope = &def_map[module_id.local_id].scope;

        for impl_id in scope.impls() {
            self.collect_from_impl(impl_id);
        }

        for (name, Item { def, vis, import }) in scope.types() {
            if let Some(i) = import {
                match i {
                    ImportOrExternCrate::Import(i) => push_import(self, i, name, def, vis),
                    ImportOrExternCrate::Glob(_) => (),
                    ImportOrExternCrate::ExternCrate(i) => {
                        push_extern_crate(self, i, name, def, vis)
                    }
                }

                continue;
            }
            // self is a declaration
            push_decl(self, def, name)
        }

        for (name, Item { def, vis, import }) in scope.macros() {
            if let Some(i) = import {
                match i {
                    ImportOrExternCrate::Import(i) => push_import(self, i, name, def.into(), vis),
                    ImportOrExternCrate::Glob(_) => (),
                    ImportOrExternCrate::ExternCrate(_) => (),
                }
                continue;
            }
            // self is a declaration
            push_decl(self, def.into(), name)
        }

        for (name, Item { def, vis, import }) in scope.values() {
            if let Some(i) = import {
                match i {
                    ImportOrGlob::Import(i) => push_import(self, i, name, def, vis),
                    ImportOrGlob::Glob(_) => (),
                }
                continue;
            }
            // self is a declaration
            push_decl(self, def, name)
        }

        for const_id in scope.unnamed_consts() {
            self.collect_from_body(const_id, None);
        }

        for (name, id) in scope.legacy_macros() {
            for &id in id {
                if id.module(self.db) == module_id {
                    match id {
                        MacroId::Macro2Id(id) => self.push_decl(id, name, false, None),
                        MacroId::MacroRulesId(id) => self.push_decl(id, name, false, None),
                        MacroId::ProcMacroId(id) => self.push_decl(id, name, false, None),
                    };
                }
            }
        }
    }

    fn collect_from_body(&mut self, body_id: impl Into<DefWithBodyId>, name: Option<Name>) {
        let body_id = body_id.into();
        let body = self.db.body(body_id);

        // Descend into the blocks and enqueue collection of all modules within.
        for (_, def_map) in body.blocks(self.db) {
            for (id, _) in def_map.modules() {
                self.work.push(SymbolCollectorWork {
                    module_id: def_map.module_id(id),
                    parent: name.clone(),
                });
            }
        }
    }

    fn collect_from_impl(&mut self, impl_id: ImplId) {
        let impl_data = self.db.impl_signature(impl_id);
        let impl_name = Some(
            hir_display_with_store(impl_data.self_ty, &impl_data.store)
                .display(
                    self.db,
                    crate::Impl::from(impl_id).krate(self.db).to_display_target(self.db),
                )
                .to_smolstr(),
        );
        self.with_container_name(impl_name, |s| {
            for &(ref name, assoc_item_id) in &impl_id.impl_items(self.db).items {
                s.push_assoc_item(assoc_item_id, name, None)
            }
        })
    }

    fn collect_from_trait(&mut self, trait_id: TraitId, trait_do_not_complete: Complete) {
        let trait_data = self.db.trait_signature(trait_id);
        self.with_container_name(Some(trait_data.name.as_str().into()), |s| {
            for &(ref name, assoc_item_id) in &trait_id.trait_items(self.db).items {
                s.push_assoc_item(assoc_item_id, name, Some(trait_do_not_complete));
            }
        });
    }

    fn with_container_name(&mut self, container_name: Option<SmolStr>, f: impl FnOnce(&mut Self)) {
        if let Some(container_name) = container_name {
            let prev = self.current_container_name.replace(container_name);
            f(self);
            self.current_container_name = prev;
        } else {
            f(self);
        }
    }

    fn push_assoc_item(
        &mut self,
        assoc_item_id: AssocItemId,
        name: &Name,
        trait_do_not_complete: Option<Complete>,
    ) {
        match assoc_item_id {
            AssocItemId::FunctionId(id) => self.push_decl(id, name, true, trait_do_not_complete),
            AssocItemId::ConstId(id) => self.push_decl(id, name, true, trait_do_not_complete),
            AssocItemId::TypeAliasId(id) => self.push_decl(id, name, true, trait_do_not_complete),
        };
    }

    fn push_decl<L>(
        &mut self,
        id: L,
        name: &Name,
        is_assoc: bool,
        trait_do_not_complete: Option<Complete>,
    ) -> Complete
    where
        L: Lookup<Database = dyn DefDatabase> + Into<ModuleDefId>,
        <L as Lookup>::Data: HasSource,
        <<L as Lookup>::Data as HasSource>::Value: HasName,
    {
        let loc = id.lookup(self.db);
        let source = loc.source(self.db);
        let Some(name_node) = source.value.name() else { return Complete::Yes };
        let def = ModuleDef::from(id.into());
        let dec_loc = DeclarationLocation {
            hir_file_id: source.file_id,
            ptr: SyntaxNodePtr::new(source.value.syntax()),
            name_ptr: AstPtr::new(&name_node).wrap_left(),
        };

        let mut do_not_complete = Complete::Yes;

        if let Some(attrs) = def.attrs(self.db) {
            do_not_complete = Complete::extract(matches!(def, ModuleDef::Trait(_)), &attrs);
            if let Some(trait_do_not_complete) = trait_do_not_complete {
                do_not_complete = Complete::for_trait_item(trait_do_not_complete, do_not_complete);
            }

            for alias in attrs.doc_aliases() {
                self.symbols.insert(FileSymbol {
                    name: alias.clone(),
                    def,
                    loc: dec_loc.clone(),
                    container_name: self.current_container_name.clone(),
                    is_alias: true,
                    is_assoc,
                    is_import: false,
                    do_not_complete,
                });
            }
        }

        self.symbols.insert(FileSymbol {
            name: name.symbol().clone(),
            def,
            container_name: self.current_container_name.clone(),
            loc: dec_loc,
            is_alias: false,
            is_assoc,
            is_import: false,
            do_not_complete,
        });

        do_not_complete
    }

    fn push_module(&mut self, module_id: ModuleId, name: &Name) {
        let def_map = module_id.def_map(self.db);
        let module_data = &def_map[module_id.local_id];
        let Some(declaration) = module_data.origin.declaration() else { return };
        let module = declaration.to_node(self.db);
        let Some(name_node) = module.name() else { return };
        let dec_loc = DeclarationLocation {
            hir_file_id: declaration.file_id,
            ptr: SyntaxNodePtr::new(module.syntax()),
            name_ptr: AstPtr::new(&name_node).wrap_left(),
        };

        let def = ModuleDef::Module(module_id.into());

        let mut do_not_complete = Complete::Yes;
        if let Some(attrs) = def.attrs(self.db) {
            do_not_complete = Complete::extract(matches!(def, ModuleDef::Trait(_)), &attrs);

            for alias in attrs.doc_aliases() {
                self.symbols.insert(FileSymbol {
                    name: alias.clone(),
                    def,
                    loc: dec_loc.clone(),
                    container_name: self.current_container_name.clone(),
                    is_alias: true,
                    is_assoc: false,
                    is_import: false,
                    do_not_complete,
                });
            }
        }

        self.symbols.insert(FileSymbol {
            name: name.symbol().clone(),
            def: ModuleDef::Module(module_id.into()),
            container_name: self.current_container_name.clone(),
            loc: dec_loc,
            is_alias: false,
            is_assoc: false,
            is_import: false,
            do_not_complete,
        });
    }
}