1 //! Look up accessible paths for items.
3 AsAssocItem, AssocItem, AssocItemContainer, Crate, ItemInNs, MacroDef, ModPath, Module,
4 ModuleDef, PathResolution, PrefixKind, ScopeDef, Semantics, Type,
6 use itertools::Itertools;
7 use rustc_hash::FxHashSet;
10 utils::path_to_string_stripping_turbo_fish,
11 AstNode, AstToken, SyntaxNode,
15 helpers::get_path_in_derive_attr,
16 items_locator::{self, AssocItemSearch, DEFAULT_QUERY_SEARCH_LIMIT},
22 /// A candidate for import, derived during various IDE activities:
23 /// * completion with imports on the fly proposals
24 /// * completion edit resolve requests
28 pub enum ImportCandidate {
29 /// A path, qualified (`std::collections::HashMap`) or not (`HashMap`).
30 Path(PathImportCandidate),
31 /// A trait associated function (with no self parameter) or an associated constant.
32 /// For 'test_mod::TestEnum::test_function', `ty` is the `test_mod::TestEnum` expression type
33 /// and `name` is the `test_function`
34 TraitAssocItem(TraitImportCandidate),
35 /// A trait method with self parameter.
36 /// For 'test_enum.test_method()', `ty` is the `test_enum` expression type
37 /// and `name` is the `test_method`
38 TraitMethod(TraitImportCandidate),
41 /// A trait import needed for a given associated item access.
42 /// For `some::path::SomeStruct::ASSOC_`, contains the
43 /// type of `some::path::SomeStruct` and `ASSOC_` as the item name.
45 pub struct TraitImportCandidate {
46 /// A type of the item that has the associated item accessed at.
47 pub receiver_ty: Type,
48 /// The associated item name that the trait to import should contain.
49 pub assoc_item_name: NameToImport,
52 /// Path import for a given name, qualified or not.
54 pub struct PathImportCandidate {
55 /// Optional qualifier before name.
56 pub qualifier: Option<FirstSegmentUnresolved>,
57 /// The name the item (struct, trait, enum, etc.) should have.
58 pub name: NameToImport,
61 /// A qualifier that has a first segment and it's unresolved.
63 pub struct FirstSegmentUnresolved {
64 fist_segment: ast::NameRef,
65 full_qualifier: ast::Path,
68 /// A name that will be used during item lookups.
69 #[derive(Debug, Clone)]
70 pub enum NameToImport {
71 /// Requires items with names that exactly match the given string, bool indicatse case-sensitivity.
73 /// Requires items with names that case-insensitively contain all letters from the string,
74 /// in the same order, but not necessary adjacent.
79 pub fn exact_case_sensitive(s: String) -> NameToImport {
80 NameToImport::Exact(s, true)
85 pub fn text(&self) -> &str {
87 NameToImport::Exact(text, _) => text.as_str(),
88 NameToImport::Fuzzy(text) => text.as_str(),
93 /// A struct to find imports in the project, given a certain name (or its part) and the context.
95 pub struct ImportAssets {
96 import_candidate: ImportCandidate,
97 candidate_node: SyntaxNode,
98 module_with_candidate: Module,
102 pub fn for_method_call(
103 method_call: &ast::MethodCallExpr,
104 sema: &Semantics<RootDatabase>,
106 let candidate_node = method_call.syntax().clone();
108 import_candidate: ImportCandidate::for_method_call(sema, method_call)?,
109 module_with_candidate: sema.scope(&candidate_node).module()?,
114 pub fn for_exact_path(
115 fully_qualified_path: &ast::Path,
116 sema: &Semantics<RootDatabase>,
118 let candidate_node = fully_qualified_path.syntax().clone();
119 if candidate_node.ancestors().find_map(ast::Use::cast).is_some() {
123 import_candidate: ImportCandidate::for_regular_path(sema, fully_qualified_path)?,
124 module_with_candidate: sema.scope(&candidate_node).module()?,
129 pub fn for_ident_pat(sema: &Semantics<RootDatabase>, pat: &ast::IdentPat) -> Option<Self> {
130 if !pat.is_simple_ident() {
133 let name = pat.name()?;
134 let candidate_node = pat.syntax().clone();
136 import_candidate: ImportCandidate::for_name(sema, &name)?,
137 module_with_candidate: sema.scope(&candidate_node).module()?,
142 pub fn for_derive_ident(sema: &Semantics<RootDatabase>, ident: &ast::Ident) -> Option<Self> {
143 let attr = ident.syntax().ancestors().find_map(ast::Attr::cast)?;
144 let path = get_path_in_derive_attr(sema, &attr, ident)?;
146 if let Some(_) = path.qualifier() {
150 let name = NameToImport::exact_case_sensitive(path.segment()?.name_ref()?.to_string());
151 let candidate_node = attr.syntax().clone();
153 import_candidate: ImportCandidate::Path(PathImportCandidate { qualifier: None, name }),
154 module_with_candidate: sema.scope(&candidate_node).module()?,
159 pub fn for_fuzzy_path(
160 module_with_candidate: Module,
161 qualifier: Option<ast::Path>,
163 sema: &Semantics<RootDatabase>,
164 candidate_node: SyntaxNode,
167 import_candidate: ImportCandidate::for_fuzzy_path(qualifier, fuzzy_name, sema)?,
168 module_with_candidate,
173 pub fn for_fuzzy_method_call(
174 module_with_method_call: Module,
176 fuzzy_method_name: String,
177 candidate_node: SyntaxNode,
180 import_candidate: ImportCandidate::TraitMethod(TraitImportCandidate {
182 assoc_item_name: NameToImport::Fuzzy(fuzzy_method_name),
184 module_with_candidate: module_with_method_call,
190 /// An import (not necessary the only one) that corresponds a certain given [`PathImportCandidate`].
191 /// (the structure is not entirely correct, since there can be situations requiring two imports, see FIXME below for the details)
192 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
193 pub struct LocatedImport {
194 /// The path to use in the `use` statement for a given candidate to be imported.
195 pub import_path: ModPath,
196 /// An item that will be imported with the import path given.
197 pub item_to_import: ItemInNs,
198 /// The path import candidate, resolved.
200 /// Not necessary matches the import:
201 /// For any associated constant from the trait, we try to access as `some::path::SomeStruct::ASSOC_`
202 /// the original item is the associated constant, but the import has to be a trait that
203 /// defines this constant.
204 pub original_item: ItemInNs,
205 /// A path of the original item.
206 pub original_path: Option<ModPath>,
211 import_path: ModPath,
212 item_to_import: ItemInNs,
213 original_item: ItemInNs,
214 original_path: Option<ModPath>,
216 Self { import_path, item_to_import, original_item, original_path }
221 pub fn import_candidate(&self) -> &ImportCandidate {
222 &self.import_candidate
225 pub fn search_for_imports(
227 sema: &Semantics<RootDatabase>,
228 prefix_kind: PrefixKind,
229 ) -> Vec<LocatedImport> {
230 let _p = profile::span("import_assets::search_for_imports");
231 self.search_for(sema, Some(prefix_kind))
234 /// This may return non-absolute paths if a part of the returned path is already imported into scope.
235 pub fn search_for_relative_paths(&self, sema: &Semantics<RootDatabase>) -> Vec<LocatedImport> {
236 let _p = profile::span("import_assets::search_for_relative_paths");
237 self.search_for(sema, None)
240 pub fn path_fuzzy_name_to_exact(&mut self, case_sensitive: bool) {
241 if let ImportCandidate::Path(PathImportCandidate { name: to_import, .. }) =
242 &mut self.import_candidate
244 let name = match to_import {
245 NameToImport::Fuzzy(name) => std::mem::take(name),
248 *to_import = NameToImport::Exact(name, case_sensitive);
254 sema: &Semantics<RootDatabase>,
255 prefixed: Option<PrefixKind>,
256 ) -> Vec<LocatedImport> {
257 let _p = profile::span("import_assets::search_for");
259 let scope_definitions = self.scope_definitions(sema);
260 let current_crate = self.module_with_candidate.krate();
261 let mod_path = |item| {
264 item_for_path_search(sema.db, item)?,
265 &self.module_with_candidate,
270 match &self.import_candidate {
271 ImportCandidate::Path(path_candidate) => {
272 path_applicable_imports(sema, current_crate, path_candidate, mod_path)
274 ImportCandidate::TraitAssocItem(trait_candidate) => {
275 trait_applicable_items(sema, current_crate, trait_candidate, true, mod_path)
277 ImportCandidate::TraitMethod(trait_candidate) => {
278 trait_applicable_items(sema, current_crate, trait_candidate, false, mod_path)
282 .filter(|import| import.import_path.len() > 1)
283 .filter(|import| !scope_definitions.contains(&ScopeDef::from(import.item_to_import)))
284 .sorted_by(|a, b| a.import_path.cmp(&b.import_path))
288 fn scope_definitions(&self, sema: &Semantics<RootDatabase>) -> FxHashSet<ScopeDef> {
289 let _p = profile::span("import_assets::scope_definitions");
290 let mut scope_definitions = FxHashSet::default();
291 sema.scope(&self.candidate_node).process_all_names(&mut |_, scope_def| {
292 scope_definitions.insert(scope_def);
298 fn path_applicable_imports(
299 sema: &Semantics<RootDatabase>,
300 current_crate: Crate,
301 path_candidate: &PathImportCandidate,
302 mod_path: impl Fn(ItemInNs) -> Option<ModPath> + Copy,
303 ) -> FxHashSet<LocatedImport> {
304 let _p = profile::span("import_assets::path_applicable_imports");
306 match &path_candidate.qualifier {
308 items_locator::items_with_name(
311 path_candidate.name.clone(),
312 // FIXME: we could look up assoc items by the input and propose those in completion,
313 // but that requires more preparation first:
314 // * store non-trait assoc items in import_map to fully enable this lookup
315 // * ensure that does not degrade the performance (benchmark it)
316 // * write more logic to check for corresponding trait presence requirement (we're unable to flyimport multiple item right now)
317 // * improve the associated completion item matching and/or scoring to ensure no noisy completions appear
319 // see also an ignored test under FIXME comment in the qualify_path.rs module
320 AssocItemSearch::Exclude,
321 Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
324 let mod_path = mod_path(item)?;
325 Some(LocatedImport::new(mod_path.clone(), item, item, Some(mod_path)))
329 Some(first_segment_unresolved) => {
330 let unresolved_qualifier =
331 path_to_string_stripping_turbo_fish(&first_segment_unresolved.full_qualifier);
332 let unresolved_first_segment = first_segment_unresolved.fist_segment.text();
333 items_locator::items_with_name(
336 path_candidate.name.clone(),
337 AssocItemSearch::Include,
338 Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
344 &unresolved_first_segment,
345 &unresolved_qualifier,
356 mod_path: impl Fn(ItemInNs) -> Option<ModPath>,
357 unresolved_first_segment: &str,
358 unresolved_qualifier: &str,
359 original_item: ItemInNs,
360 ) -> Option<LocatedImport> {
361 let _p = profile::span("import_assets::import_for_item");
363 let original_item_candidate = item_for_path_search(db, original_item)?;
364 let import_path_candidate = mod_path(original_item_candidate)?;
365 let import_path_string = import_path_candidate.to_string();
367 let expected_import_end = if item_as_assoc(db, original_item).is_some() {
368 unresolved_qualifier.to_string()
370 format!("{}::{}", unresolved_qualifier, item_name(db, original_item)?)
372 if !import_path_string.contains(unresolved_first_segment)
373 || !import_path_string.ends_with(&expected_import_end)
379 find_import_for_segment(db, original_item_candidate, unresolved_first_segment)?;
380 let trait_item_to_import = item_as_assoc(db, original_item)
381 .and_then(|assoc| assoc.containing_trait(db))
382 .map(|trait_| ItemInNs::from(ModuleDef::from(trait_)));
383 Some(match (segment_import == original_item_candidate, trait_item_to_import) {
385 // FIXME we should be able to import both the trait and the segment,
386 // but it's unclear what to do with overlapping edits (merge imports?)
387 // especially in case of lazy completion edit resolutions.
390 (false, Some(trait_to_import)) => LocatedImport::new(
391 mod_path(trait_to_import)?,
394 mod_path(original_item),
396 (true, None) => LocatedImport::new(
397 import_path_candidate,
398 original_item_candidate,
400 mod_path(original_item),
402 (false, None) => LocatedImport::new(
403 mod_path(segment_import)?,
406 mod_path(original_item),
411 pub fn item_for_path_search(db: &RootDatabase, item: ItemInNs) -> Option<ItemInNs> {
413 ItemInNs::Types(_) | ItemInNs::Values(_) => match item_as_assoc(db, item) {
414 Some(assoc_item) => match assoc_item.container(db) {
415 AssocItemContainer::Trait(trait_) => ItemInNs::from(ModuleDef::from(trait_)),
416 AssocItemContainer::Impl(impl_) => {
417 ItemInNs::from(ModuleDef::from(impl_.self_ty(db).as_adt()?))
422 ItemInNs::Macros(_) => item,
426 fn find_import_for_segment(
428 original_item: ItemInNs,
429 unresolved_first_segment: &str,
430 ) -> Option<ItemInNs> {
431 let segment_is_name = item_name(db, original_item)
432 .map(|name| name.to_smol_str() == unresolved_first_segment)
435 Some(if segment_is_name {
438 let matching_module =
439 module_with_segment_name(db, unresolved_first_segment, original_item)?;
440 ItemInNs::from(ModuleDef::from(matching_module))
444 fn module_with_segment_name(
448 ) -> Option<Module> {
449 let mut current_module = match candidate {
450 ItemInNs::Types(module_def_id) => ModuleDef::from(module_def_id).module(db),
451 ItemInNs::Values(module_def_id) => ModuleDef::from(module_def_id).module(db),
452 ItemInNs::Macros(macro_def_id) => MacroDef::from(macro_def_id).module(db),
454 while let Some(module) = current_module {
455 if let Some(module_name) = module.name(db) {
456 if module_name.to_smol_str() == segment_name {
460 current_module = module.parent(db);
465 fn trait_applicable_items(
466 sema: &Semantics<RootDatabase>,
467 current_crate: Crate,
468 trait_candidate: &TraitImportCandidate,
469 trait_assoc_item: bool,
470 mod_path: impl Fn(ItemInNs) -> Option<ModPath>,
471 ) -> FxHashSet<LocatedImport> {
472 let _p = profile::span("import_assets::trait_applicable_items");
476 let inherent_traits = trait_candidate.receiver_ty.applicable_inherent_traits(db);
477 let env_traits = trait_candidate.receiver_ty.env_traits(db);
478 let related_traits = inherent_traits.chain(env_traits).collect::<FxHashSet<_>>();
480 let mut required_assoc_items = FxHashSet::default();
481 let trait_candidates = items_locator::items_with_name(
484 trait_candidate.assoc_item_name.clone(),
485 AssocItemSearch::AssocItemsOnly,
486 Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
488 .filter_map(|input| item_as_assoc(db, input))
489 .filter_map(|assoc| {
490 let assoc_item_trait = assoc.containing_trait(db)?;
491 if related_traits.contains(&assoc_item_trait) {
494 required_assoc_items.insert(assoc);
495 Some(assoc_item_trait.into())
500 let mut located_imports = FxHashSet::default();
502 if trait_assoc_item {
503 trait_candidate.receiver_ty.iterate_path_candidates(
510 if required_assoc_items.contains(&assoc) {
511 if let AssocItem::Function(f) = assoc {
512 if f.self_param(db).is_some() {
516 let located_trait = assoc.containing_trait(db)?;
517 let trait_item = ItemInNs::from(ModuleDef::from(located_trait));
518 let original_item = assoc_to_item(assoc);
519 located_imports.insert(LocatedImport::new(
520 mod_path(trait_item)?,
523 mod_path(original_item),
530 trait_candidate.receiver_ty.iterate_method_candidates(
537 let assoc = function.as_assoc_item(db)?;
538 if required_assoc_items.contains(&assoc) {
539 let located_trait = assoc.containing_trait(db)?;
540 let trait_item = ItemInNs::from(ModuleDef::from(located_trait));
541 let original_item = assoc_to_item(assoc);
542 located_imports.insert(LocatedImport::new(
543 mod_path(trait_item)?,
546 mod_path(original_item),
557 fn assoc_to_item(assoc: AssocItem) -> ItemInNs {
559 AssocItem::Function(f) => ItemInNs::from(ModuleDef::from(f)),
560 AssocItem::Const(c) => ItemInNs::from(ModuleDef::from(c)),
561 AssocItem::TypeAlias(t) => ItemInNs::from(ModuleDef::from(t)),
567 item_to_search: ItemInNs,
568 module_with_candidate: &Module,
569 prefixed: Option<PrefixKind>,
570 ) -> Option<ModPath> {
571 if let Some(prefix_kind) = prefixed {
572 module_with_candidate.find_use_path_prefixed(db, item_to_search, prefix_kind)
574 module_with_candidate.find_use_path(db, item_to_search)
578 impl ImportCandidate {
580 sema: &Semantics<RootDatabase>,
581 method_call: &ast::MethodCallExpr,
583 match sema.resolve_method_call(method_call) {
585 None => Some(Self::TraitMethod(TraitImportCandidate {
586 receiver_ty: sema.type_of_expr(&method_call.receiver()?)?.adjusted(),
587 assoc_item_name: NameToImport::exact_case_sensitive(
588 method_call.name_ref()?.to_string(),
594 fn for_regular_path(sema: &Semantics<RootDatabase>, path: &ast::Path) -> Option<Self> {
595 if sema.resolve_path(path).is_some() {
598 path_import_candidate(
601 NameToImport::exact_case_sensitive(path.segment()?.name_ref()?.to_string()),
605 fn for_name(sema: &Semantics<RootDatabase>, name: &ast::Name) -> Option<Self> {
607 .scope(name.syntax())
608 .speculative_resolve(&ast::make::ext::ident_path(&name.text()))
613 Some(ImportCandidate::Path(PathImportCandidate {
615 name: NameToImport::exact_case_sensitive(name.to_string()),
620 qualifier: Option<ast::Path>,
622 sema: &Semantics<RootDatabase>,
624 path_import_candidate(sema, qualifier, NameToImport::Fuzzy(fuzzy_name))
628 fn path_import_candidate(
629 sema: &Semantics<RootDatabase>,
630 qualifier: Option<ast::Path>,
632 ) -> Option<ImportCandidate> {
633 Some(match qualifier {
634 Some(qualifier) => match sema.resolve_path(&qualifier) {
636 let qualifier_start =
637 qualifier.syntax().descendants().find_map(ast::NameRef::cast)?;
638 let qualifier_start_path =
639 qualifier_start.syntax().ancestors().find_map(ast::Path::cast)?;
640 if sema.resolve_path(&qualifier_start_path).is_none() {
641 ImportCandidate::Path(PathImportCandidate {
642 qualifier: Some(FirstSegmentUnresolved {
643 fist_segment: qualifier_start,
644 full_qualifier: qualifier,
652 Some(PathResolution::Def(ModuleDef::Adt(assoc_item_path))) => {
653 ImportCandidate::TraitAssocItem(TraitImportCandidate {
654 receiver_ty: assoc_item_path.ty(sema.db),
655 assoc_item_name: name,
658 Some(_) => return None,
660 None => ImportCandidate::Path(PathImportCandidate { qualifier: None, name }),
664 fn item_as_assoc(db: &RootDatabase, item: ItemInNs) -> Option<AssocItem> {
665 item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db))