]> git.lizzy.rs Git - rust.git/blob - crates/ide_db/src/helpers/import_assets.rs
minor: Remove unnecessary clones
[rust.git] / crates / ide_db / src / helpers / import_assets.rs
1 //! Look up accessible paths for items.
2 use hir::{
3     AsAssocItem, AssocItem, AssocItemContainer, Crate, ItemInNs, MacroDef, ModPath, Module,
4     ModuleDef, PathResolution, PrefixKind, ScopeDef, Semantics, Type,
5 };
6 use itertools::Itertools;
7 use rustc_hash::FxHashSet;
8 use syntax::{
9     ast::{self, HasName},
10     utils::path_to_string_stripping_turbo_fish,
11     AstNode, AstToken, SyntaxNode,
12 };
13
14 use crate::{
15     helpers::get_path_in_derive_attr,
16     items_locator::{self, AssocItemSearch, DEFAULT_QUERY_SEARCH_LIMIT},
17     RootDatabase,
18 };
19
20 use super::item_name;
21
22 /// A candidate for import, derived during various IDE activities:
23 /// * completion with imports on the fly proposals
24 /// * completion edit resolve requests
25 /// * assists
26 /// * etc.
27 #[derive(Debug)]
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),
39 }
40
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.
44 #[derive(Debug)]
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,
50 }
51
52 /// Path import for a given name, qualified or not.
53 #[derive(Debug)]
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,
59 }
60
61 /// A qualifier that has a first segment and it's unresolved.
62 #[derive(Debug)]
63 pub struct FirstSegmentUnresolved {
64     fist_segment: ast::NameRef,
65     full_qualifier: ast::Path,
66 }
67
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.
72     Exact(String, bool),
73     /// Requires items with names that case-insensitively contain all letters from the string,
74     /// in the same order, but not necessary adjacent.
75     Fuzzy(String),
76 }
77
78 impl NameToImport {
79     pub fn exact_case_sensitive(s: String) -> NameToImport {
80         NameToImport::Exact(s, true)
81     }
82 }
83
84 impl NameToImport {
85     pub fn text(&self) -> &str {
86         match self {
87             NameToImport::Exact(text, _) => text.as_str(),
88             NameToImport::Fuzzy(text) => text.as_str(),
89         }
90     }
91 }
92
93 /// A struct to find imports in the project, given a certain name (or its part) and the context.
94 #[derive(Debug)]
95 pub struct ImportAssets {
96     import_candidate: ImportCandidate,
97     candidate_node: SyntaxNode,
98     module_with_candidate: Module,
99 }
100
101 impl ImportAssets {
102     pub fn for_method_call(
103         method_call: &ast::MethodCallExpr,
104         sema: &Semantics<RootDatabase>,
105     ) -> Option<Self> {
106         let candidate_node = method_call.syntax().clone();
107         Some(Self {
108             import_candidate: ImportCandidate::for_method_call(sema, method_call)?,
109             module_with_candidate: sema.scope(&candidate_node).module()?,
110             candidate_node,
111         })
112     }
113
114     pub fn for_exact_path(
115         fully_qualified_path: &ast::Path,
116         sema: &Semantics<RootDatabase>,
117     ) -> Option<Self> {
118         let candidate_node = fully_qualified_path.syntax().clone();
119         if candidate_node.ancestors().find_map(ast::Use::cast).is_some() {
120             return None;
121         }
122         Some(Self {
123             import_candidate: ImportCandidate::for_regular_path(sema, fully_qualified_path)?,
124             module_with_candidate: sema.scope(&candidate_node).module()?,
125             candidate_node,
126         })
127     }
128
129     pub fn for_ident_pat(sema: &Semantics<RootDatabase>, pat: &ast::IdentPat) -> Option<Self> {
130         if !pat.is_simple_ident() {
131             return None;
132         }
133         let name = pat.name()?;
134         let candidate_node = pat.syntax().clone();
135         Some(Self {
136             import_candidate: ImportCandidate::for_name(sema, &name)?,
137             module_with_candidate: sema.scope(&candidate_node).module()?,
138             candidate_node,
139         })
140     }
141
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)?;
145
146         if let Some(_) = path.qualifier() {
147             return None;
148         }
149         let name = NameToImport::exact_case_sensitive(path.segment()?.name_ref()?.to_string());
150         let candidate_node = attr.syntax().clone();
151         Some(Self {
152             import_candidate: ImportCandidate::Path(PathImportCandidate { qualifier: None, name }),
153             module_with_candidate: sema.scope(&candidate_node).module()?,
154             candidate_node,
155         })
156     }
157
158     pub fn for_fuzzy_path(
159         module_with_candidate: Module,
160         qualifier: Option<ast::Path>,
161         fuzzy_name: String,
162         sema: &Semantics<RootDatabase>,
163         candidate_node: SyntaxNode,
164     ) -> Option<Self> {
165         Some(Self {
166             import_candidate: ImportCandidate::for_fuzzy_path(qualifier, fuzzy_name, sema)?,
167             module_with_candidate,
168             candidate_node,
169         })
170     }
171
172     pub fn for_fuzzy_method_call(
173         module_with_method_call: Module,
174         receiver_ty: Type,
175         fuzzy_method_name: String,
176         candidate_node: SyntaxNode,
177     ) -> Option<Self> {
178         Some(Self {
179             import_candidate: ImportCandidate::TraitMethod(TraitImportCandidate {
180                 receiver_ty,
181                 assoc_item_name: NameToImport::Fuzzy(fuzzy_method_name),
182             }),
183             module_with_candidate: module_with_method_call,
184             candidate_node,
185         })
186     }
187 }
188
189 /// An import (not necessary the only one) that corresponds a certain given [`PathImportCandidate`].
190 /// (the structure is not entirely correct, since there can be situations requiring two imports, see FIXME below for the details)
191 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
192 pub struct LocatedImport {
193     /// The path to use in the `use` statement for a given candidate to be imported.
194     pub import_path: ModPath,
195     /// An item that will be imported with the import path given.
196     pub item_to_import: ItemInNs,
197     /// The path import candidate, resolved.
198     ///
199     /// Not necessary matches the import:
200     /// For any associated constant from the trait, we try to access as `some::path::SomeStruct::ASSOC_`
201     /// the original item is the associated constant, but the import has to be a trait that
202     /// defines this constant.
203     pub original_item: ItemInNs,
204     /// A path of the original item.
205     pub original_path: Option<ModPath>,
206 }
207
208 impl LocatedImport {
209     pub fn new(
210         import_path: ModPath,
211         item_to_import: ItemInNs,
212         original_item: ItemInNs,
213         original_path: Option<ModPath>,
214     ) -> Self {
215         Self { import_path, item_to_import, original_item, original_path }
216     }
217 }
218
219 impl ImportAssets {
220     pub fn import_candidate(&self) -> &ImportCandidate {
221         &self.import_candidate
222     }
223
224     pub fn search_for_imports(
225         &self,
226         sema: &Semantics<RootDatabase>,
227         prefix_kind: PrefixKind,
228     ) -> Vec<LocatedImport> {
229         let _p = profile::span("import_assets::search_for_imports");
230         self.search_for(sema, Some(prefix_kind))
231     }
232
233     /// This may return non-absolute paths if a part of the returned path is already imported into scope.
234     pub fn search_for_relative_paths(&self, sema: &Semantics<RootDatabase>) -> Vec<LocatedImport> {
235         let _p = profile::span("import_assets::search_for_relative_paths");
236         self.search_for(sema, None)
237     }
238
239     pub fn path_fuzzy_name_to_exact(&mut self, case_sensitive: bool) {
240         if let ImportCandidate::Path(PathImportCandidate { name: to_import, .. }) =
241             &mut self.import_candidate
242         {
243             let name = match to_import {
244                 NameToImport::Fuzzy(name) => std::mem::take(name),
245                 _ => return,
246             };
247             *to_import = NameToImport::Exact(name, case_sensitive);
248         }
249     }
250
251     fn search_for(
252         &self,
253         sema: &Semantics<RootDatabase>,
254         prefixed: Option<PrefixKind>,
255     ) -> Vec<LocatedImport> {
256         let _p = profile::span("import_assets::search_for");
257
258         let scope_definitions = self.scope_definitions(sema);
259         let current_crate = self.module_with_candidate.krate();
260         let mod_path = |item| {
261             get_mod_path(
262                 sema.db,
263                 item_for_path_search(sema.db, item)?,
264                 &self.module_with_candidate,
265                 prefixed,
266             )
267         };
268
269         match &self.import_candidate {
270             ImportCandidate::Path(path_candidate) => {
271                 path_applicable_imports(sema, current_crate, path_candidate, mod_path)
272             }
273             ImportCandidate::TraitAssocItem(trait_candidate) => {
274                 trait_applicable_items(sema, current_crate, trait_candidate, true, mod_path)
275             }
276             ImportCandidate::TraitMethod(trait_candidate) => {
277                 trait_applicable_items(sema, current_crate, trait_candidate, false, mod_path)
278             }
279         }
280         .into_iter()
281         .filter(|import| import.import_path.len() > 1)
282         .filter(|import| !scope_definitions.contains(&ScopeDef::from(import.item_to_import)))
283         .sorted_by(|a, b| a.import_path.cmp(&b.import_path))
284         .collect()
285     }
286
287     fn scope_definitions(&self, sema: &Semantics<RootDatabase>) -> FxHashSet<ScopeDef> {
288         let _p = profile::span("import_assets::scope_definitions");
289         let mut scope_definitions = FxHashSet::default();
290         sema.scope(&self.candidate_node).process_all_names(&mut |_, scope_def| {
291             scope_definitions.insert(scope_def);
292         });
293         scope_definitions
294     }
295 }
296
297 fn path_applicable_imports(
298     sema: &Semantics<RootDatabase>,
299     current_crate: Crate,
300     path_candidate: &PathImportCandidate,
301     mod_path: impl Fn(ItemInNs) -> Option<ModPath> + Copy,
302 ) -> FxHashSet<LocatedImport> {
303     let _p = profile::span("import_assets::path_applicable_imports");
304
305     match &path_candidate.qualifier {
306         None => {
307             items_locator::items_with_name(
308                 sema,
309                 current_crate,
310                 path_candidate.name.clone(),
311                 // FIXME: we could look up assoc items by the input and propose those in completion,
312                 // but that requires more preparation first:
313                 // * store non-trait assoc items in import_map to fully enable this lookup
314                 // * ensure that does not degrade the performance (benchmark it)
315                 // * write more logic to check for corresponding trait presence requirement (we're unable to flyimport multiple item right now)
316                 // * improve the associated completion item matching and/or scoring to ensure no noisy completions appear
317                 //
318                 // see also an ignored test under FIXME comment in the qualify_path.rs module
319                 AssocItemSearch::Exclude,
320                 Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
321             )
322             .filter_map(|item| {
323                 let mod_path = mod_path(item)?;
324                 Some(LocatedImport::new(mod_path.clone(), item, item, Some(mod_path)))
325             })
326             .collect()
327         }
328         Some(first_segment_unresolved) => {
329             let unresolved_qualifier =
330                 path_to_string_stripping_turbo_fish(&first_segment_unresolved.full_qualifier);
331             let unresolved_first_segment = first_segment_unresolved.fist_segment.text();
332             items_locator::items_with_name(
333                 sema,
334                 current_crate,
335                 path_candidate.name.clone(),
336                 AssocItemSearch::Include,
337                 Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
338             )
339             .filter_map(|item| {
340                 import_for_item(
341                     sema.db,
342                     mod_path,
343                     &unresolved_first_segment,
344                     &unresolved_qualifier,
345                     item,
346                 )
347             })
348             .collect()
349         }
350     }
351 }
352
353 fn import_for_item(
354     db: &RootDatabase,
355     mod_path: impl Fn(ItemInNs) -> Option<ModPath>,
356     unresolved_first_segment: &str,
357     unresolved_qualifier: &str,
358     original_item: ItemInNs,
359 ) -> Option<LocatedImport> {
360     let _p = profile::span("import_assets::import_for_item");
361
362     let original_item_candidate = item_for_path_search(db, original_item)?;
363     let import_path_candidate = mod_path(original_item_candidate)?;
364     let import_path_string = import_path_candidate.to_string();
365
366     let expected_import_end = if item_as_assoc(db, original_item).is_some() {
367         unresolved_qualifier.to_string()
368     } else {
369         format!("{}::{}", unresolved_qualifier, item_name(db, original_item)?)
370     };
371     if !import_path_string.contains(unresolved_first_segment)
372         || !import_path_string.ends_with(&expected_import_end)
373     {
374         return None;
375     }
376
377     let segment_import =
378         find_import_for_segment(db, original_item_candidate, unresolved_first_segment)?;
379     let trait_item_to_import = item_as_assoc(db, original_item)
380         .and_then(|assoc| assoc.containing_trait(db))
381         .map(|trait_| ItemInNs::from(ModuleDef::from(trait_)));
382     Some(match (segment_import == original_item_candidate, trait_item_to_import) {
383         (true, Some(_)) => {
384             // FIXME we should be able to import both the trait and the segment,
385             // but it's unclear what to do with overlapping edits (merge imports?)
386             // especially in case of lazy completion edit resolutions.
387             return None;
388         }
389         (false, Some(trait_to_import)) => LocatedImport::new(
390             mod_path(trait_to_import)?,
391             trait_to_import,
392             original_item,
393             mod_path(original_item),
394         ),
395         (true, None) => LocatedImport::new(
396             import_path_candidate,
397             original_item_candidate,
398             original_item,
399             mod_path(original_item),
400         ),
401         (false, None) => LocatedImport::new(
402             mod_path(segment_import)?,
403             segment_import,
404             original_item,
405             mod_path(original_item),
406         ),
407     })
408 }
409
410 pub fn item_for_path_search(db: &RootDatabase, item: ItemInNs) -> Option<ItemInNs> {
411     Some(match item {
412         ItemInNs::Types(_) | ItemInNs::Values(_) => match item_as_assoc(db, item) {
413             Some(assoc_item) => match assoc_item.container(db) {
414                 AssocItemContainer::Trait(trait_) => ItemInNs::from(ModuleDef::from(trait_)),
415                 AssocItemContainer::Impl(impl_) => {
416                     ItemInNs::from(ModuleDef::from(impl_.self_ty(db).as_adt()?))
417                 }
418             },
419             None => item,
420         },
421         ItemInNs::Macros(_) => item,
422     })
423 }
424
425 fn find_import_for_segment(
426     db: &RootDatabase,
427     original_item: ItemInNs,
428     unresolved_first_segment: &str,
429 ) -> Option<ItemInNs> {
430     let segment_is_name = item_name(db, original_item)
431         .map(|name| name.to_smol_str() == unresolved_first_segment)
432         .unwrap_or(false);
433
434     Some(if segment_is_name {
435         original_item
436     } else {
437         let matching_module =
438             module_with_segment_name(db, unresolved_first_segment, original_item)?;
439         ItemInNs::from(ModuleDef::from(matching_module))
440     })
441 }
442
443 fn module_with_segment_name(
444     db: &RootDatabase,
445     segment_name: &str,
446     candidate: ItemInNs,
447 ) -> Option<Module> {
448     let mut current_module = match candidate {
449         ItemInNs::Types(module_def_id) => ModuleDef::from(module_def_id).module(db),
450         ItemInNs::Values(module_def_id) => ModuleDef::from(module_def_id).module(db),
451         ItemInNs::Macros(macro_def_id) => MacroDef::from(macro_def_id).module(db),
452     };
453     while let Some(module) = current_module {
454         if let Some(module_name) = module.name(db) {
455             if module_name.to_smol_str() == segment_name {
456                 return Some(module);
457             }
458         }
459         current_module = module.parent(db);
460     }
461     None
462 }
463
464 fn trait_applicable_items(
465     sema: &Semantics<RootDatabase>,
466     current_crate: Crate,
467     trait_candidate: &TraitImportCandidate,
468     trait_assoc_item: bool,
469     mod_path: impl Fn(ItemInNs) -> Option<ModPath>,
470 ) -> FxHashSet<LocatedImport> {
471     let _p = profile::span("import_assets::trait_applicable_items");
472
473     let db = sema.db;
474
475     let related_dyn_traits =
476         trait_candidate.receiver_ty.applicable_inherent_traits(db).collect::<FxHashSet<_>>();
477     let mut required_assoc_items = FxHashSet::default();
478     let trait_candidates = items_locator::items_with_name(
479         sema,
480         current_crate,
481         trait_candidate.assoc_item_name.clone(),
482         AssocItemSearch::AssocItemsOnly,
483         Some(DEFAULT_QUERY_SEARCH_LIMIT.inner()),
484     )
485     .filter_map(|input| item_as_assoc(db, input))
486     .filter_map(|assoc| {
487         let assoc_item_trait = assoc.containing_trait(db)?;
488         if related_dyn_traits.contains(&assoc_item_trait) {
489             None
490         } else {
491             required_assoc_items.insert(assoc);
492             Some(assoc_item_trait.into())
493         }
494     })
495     .collect();
496
497     let mut located_imports = FxHashSet::default();
498
499     if trait_assoc_item {
500         trait_candidate.receiver_ty.iterate_path_candidates(
501             db,
502             current_crate,
503             &trait_candidates,
504             None,
505             |_, assoc| {
506                 if required_assoc_items.contains(&assoc) {
507                     if let AssocItem::Function(f) = assoc {
508                         if f.self_param(db).is_some() {
509                             return None;
510                         }
511                     }
512                     let located_trait = assoc.containing_trait(db)?;
513                     let trait_item = ItemInNs::from(ModuleDef::from(located_trait));
514                     let original_item = assoc_to_item(assoc);
515                     located_imports.insert(LocatedImport::new(
516                         mod_path(trait_item)?,
517                         trait_item,
518                         original_item,
519                         mod_path(original_item),
520                     ));
521                 }
522                 None::<()>
523             },
524         )
525     } else {
526         trait_candidate.receiver_ty.iterate_method_candidates(
527             db,
528             current_crate,
529             &trait_candidates,
530             None,
531             |_, function| {
532                 let assoc = function.as_assoc_item(db)?;
533                 if required_assoc_items.contains(&assoc) {
534                     let located_trait = assoc.containing_trait(db)?;
535                     let trait_item = ItemInNs::from(ModuleDef::from(located_trait));
536                     let original_item = assoc_to_item(assoc);
537                     located_imports.insert(LocatedImport::new(
538                         mod_path(trait_item)?,
539                         trait_item,
540                         original_item,
541                         mod_path(original_item),
542                     ));
543                 }
544                 None::<()>
545             },
546         )
547     };
548
549     located_imports
550 }
551
552 fn assoc_to_item(assoc: AssocItem) -> ItemInNs {
553     match assoc {
554         AssocItem::Function(f) => ItemInNs::from(ModuleDef::from(f)),
555         AssocItem::Const(c) => ItemInNs::from(ModuleDef::from(c)),
556         AssocItem::TypeAlias(t) => ItemInNs::from(ModuleDef::from(t)),
557     }
558 }
559
560 fn get_mod_path(
561     db: &RootDatabase,
562     item_to_search: ItemInNs,
563     module_with_candidate: &Module,
564     prefixed: Option<PrefixKind>,
565 ) -> Option<ModPath> {
566     if let Some(prefix_kind) = prefixed {
567         module_with_candidate.find_use_path_prefixed(db, item_to_search, prefix_kind)
568     } else {
569         module_with_candidate.find_use_path(db, item_to_search)
570     }
571 }
572
573 impl ImportCandidate {
574     fn for_method_call(
575         sema: &Semantics<RootDatabase>,
576         method_call: &ast::MethodCallExpr,
577     ) -> Option<Self> {
578         match sema.resolve_method_call(method_call) {
579             Some(_) => None,
580             None => Some(Self::TraitMethod(TraitImportCandidate {
581                 receiver_ty: sema.type_of_expr(&method_call.receiver()?)?.adjusted(),
582                 assoc_item_name: NameToImport::exact_case_sensitive(
583                     method_call.name_ref()?.to_string(),
584                 ),
585             })),
586         }
587     }
588
589     fn for_regular_path(sema: &Semantics<RootDatabase>, path: &ast::Path) -> Option<Self> {
590         if sema.resolve_path(path).is_some() {
591             return None;
592         }
593         path_import_candidate(
594             sema,
595             path.qualifier(),
596             NameToImport::exact_case_sensitive(path.segment()?.name_ref()?.to_string()),
597         )
598     }
599
600     fn for_name(sema: &Semantics<RootDatabase>, name: &ast::Name) -> Option<Self> {
601         if sema
602             .scope(name.syntax())
603             .speculative_resolve(&ast::make::ext::ident_path(&name.text()))
604             .is_some()
605         {
606             return None;
607         }
608         Some(ImportCandidate::Path(PathImportCandidate {
609             qualifier: None,
610             name: NameToImport::exact_case_sensitive(name.to_string()),
611         }))
612     }
613
614     fn for_fuzzy_path(
615         qualifier: Option<ast::Path>,
616         fuzzy_name: String,
617         sema: &Semantics<RootDatabase>,
618     ) -> Option<Self> {
619         path_import_candidate(sema, qualifier, NameToImport::Fuzzy(fuzzy_name))
620     }
621 }
622
623 fn path_import_candidate(
624     sema: &Semantics<RootDatabase>,
625     qualifier: Option<ast::Path>,
626     name: NameToImport,
627 ) -> Option<ImportCandidate> {
628     Some(match qualifier {
629         Some(qualifier) => match sema.resolve_path(&qualifier) {
630             None => {
631                 let qualifier_start =
632                     qualifier.syntax().descendants().find_map(ast::NameRef::cast)?;
633                 let qualifier_start_path =
634                     qualifier_start.syntax().ancestors().find_map(ast::Path::cast)?;
635                 if sema.resolve_path(&qualifier_start_path).is_none() {
636                     ImportCandidate::Path(PathImportCandidate {
637                         qualifier: Some(FirstSegmentUnresolved {
638                             fist_segment: qualifier_start,
639                             full_qualifier: qualifier,
640                         }),
641                         name,
642                     })
643                 } else {
644                     return None;
645                 }
646             }
647             Some(PathResolution::Def(ModuleDef::Adt(assoc_item_path))) => {
648                 ImportCandidate::TraitAssocItem(TraitImportCandidate {
649                     receiver_ty: assoc_item_path.ty(sema.db),
650                     assoc_item_name: name,
651                 })
652             }
653             Some(_) => return None,
654         },
655         None => ImportCandidate::Path(PathImportCandidate { qualifier: None, name }),
656     })
657 }
658
659 fn item_as_assoc(db: &RootDatabase, item: ItemInNs) -> Option<AssocItem> {
660     item.as_module_def().and_then(|module_def| module_def.as_assoc_item(db))
661 }