]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-db/src/imports/insert_use.rs
Rollup merge of #97739 - a2aaron:let_underscore, r=estebank
[rust.git] / src / tools / rust-analyzer / crates / ide-db / src / imports / insert_use.rs
1 //! Handle syntactic aspects of inserting a new `use` item.
2 #[cfg(test)]
3 mod tests;
4
5 use std::cmp::Ordering;
6
7 use hir::Semantics;
8 use syntax::{
9     algo,
10     ast::{self, make, AstNode, HasAttrs, HasModuleItem, HasVisibility, PathSegmentKind},
11     ted, Direction, NodeOrToken, SyntaxKind, SyntaxNode,
12 };
13
14 use crate::{
15     imports::merge_imports::{
16         common_prefix, eq_attrs, eq_visibility, try_merge_imports, use_tree_path_cmp, MergeBehavior,
17     },
18     RootDatabase,
19 };
20
21 pub use hir::PrefixKind;
22
23 /// How imports should be grouped into use statements.
24 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
25 pub enum ImportGranularity {
26     /// Do not change the granularity of any imports and preserve the original structure written by the developer.
27     Preserve,
28     /// Merge imports from the same crate into a single use statement.
29     Crate,
30     /// Merge imports from the same module into a single use statement.
31     Module,
32     /// Flatten imports so that each has its own use statement.
33     Item,
34 }
35
36 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
37 pub struct InsertUseConfig {
38     pub granularity: ImportGranularity,
39     pub enforce_granularity: bool,
40     pub prefix_kind: PrefixKind,
41     pub group: bool,
42     pub skip_glob_imports: bool,
43 }
44
45 #[derive(Debug, Clone)]
46 pub enum ImportScope {
47     File(ast::SourceFile),
48     Module(ast::ItemList),
49     Block(ast::StmtList),
50 }
51
52 impl ImportScope {
53     // FIXME: Remove this?
54     #[cfg(test)]
55     fn from(syntax: SyntaxNode) -> Option<Self> {
56         use syntax::match_ast;
57         fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool {
58             attrs
59                 .attrs()
60                 .any(|attr| attr.as_simple_call().map_or(false, |(ident, _)| ident == "cfg"))
61         }
62         match_ast! {
63             match syntax {
64                 ast::Module(module) => module.item_list().map(ImportScope::Module),
65                 ast::SourceFile(file) => Some(ImportScope::File(file)),
66                 ast::Fn(func) => contains_cfg_attr(&func).then(|| func.body().and_then(|it| it.stmt_list().map(ImportScope::Block))).flatten(),
67                 ast::Const(konst) => contains_cfg_attr(&konst).then(|| match konst.body()? {
68                     ast::Expr::BlockExpr(block) => Some(block),
69                     _ => None,
70                 }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)),
71                 ast::Static(statik) => contains_cfg_attr(&statik).then(|| match statik.body()? {
72                     ast::Expr::BlockExpr(block) => Some(block),
73                     _ => None,
74                 }).flatten().and_then(|it| it.stmt_list().map(ImportScope::Block)),
75                 _ => None,
76
77             }
78         }
79     }
80
81     /// Determines the containing syntax node in which to insert a `use` statement affecting `position`.
82     /// Returns the original source node inside attributes.
83     pub fn find_insert_use_container(
84         position: &SyntaxNode,
85         sema: &Semantics<'_, RootDatabase>,
86     ) -> Option<Self> {
87         fn contains_cfg_attr(attrs: &dyn HasAttrs) -> bool {
88             attrs
89                 .attrs()
90                 .any(|attr| attr.as_simple_call().map_or(false, |(ident, _)| ident == "cfg"))
91         }
92
93         // Walk up the ancestor tree searching for a suitable node to do insertions on
94         // with special handling on cfg-gated items, in which case we want to insert imports locally
95         // or FIXME: annotate inserted imports with the same cfg
96         for syntax in sema.ancestors_with_macros(position.clone()) {
97             if let Some(file) = ast::SourceFile::cast(syntax.clone()) {
98                 return Some(ImportScope::File(file));
99             } else if let Some(item) = ast::Item::cast(syntax) {
100                 return match item {
101                     ast::Item::Const(konst) if contains_cfg_attr(&konst) => {
102                         // FIXME: Instead of bailing out with None, we should note down that
103                         // this import needs an attribute added
104                         match sema.original_ast_node(konst)?.body()? {
105                             ast::Expr::BlockExpr(block) => block,
106                             _ => return None,
107                         }
108                         .stmt_list()
109                         .map(ImportScope::Block)
110                     }
111                     ast::Item::Fn(func) if contains_cfg_attr(&func) => {
112                         // FIXME: Instead of bailing out with None, we should note down that
113                         // this import needs an attribute added
114                         sema.original_ast_node(func)?.body()?.stmt_list().map(ImportScope::Block)
115                     }
116                     ast::Item::Static(statik) if contains_cfg_attr(&statik) => {
117                         // FIXME: Instead of bailing out with None, we should note down that
118                         // this import needs an attribute added
119                         match sema.original_ast_node(statik)?.body()? {
120                             ast::Expr::BlockExpr(block) => block,
121                             _ => return None,
122                         }
123                         .stmt_list()
124                         .map(ImportScope::Block)
125                     }
126                     ast::Item::Module(module) => {
127                         // early return is important here, if we can't find the original module
128                         // in the input there is no way for us to insert an import anywhere.
129                         sema.original_ast_node(module)?.item_list().map(ImportScope::Module)
130                     }
131                     _ => continue,
132                 };
133             }
134         }
135         None
136     }
137
138     pub fn as_syntax_node(&self) -> &SyntaxNode {
139         match self {
140             ImportScope::File(file) => file.syntax(),
141             ImportScope::Module(item_list) => item_list.syntax(),
142             ImportScope::Block(block) => block.syntax(),
143         }
144     }
145
146     pub fn clone_for_update(&self) -> Self {
147         match self {
148             ImportScope::File(file) => ImportScope::File(file.clone_for_update()),
149             ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()),
150             ImportScope::Block(block) => ImportScope::Block(block.clone_for_update()),
151         }
152     }
153 }
154
155 /// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
156 pub fn insert_use(scope: &ImportScope, path: ast::Path, cfg: &InsertUseConfig) {
157     let _p = profile::span("insert_use");
158     let mut mb = match cfg.granularity {
159         ImportGranularity::Crate => Some(MergeBehavior::Crate),
160         ImportGranularity::Module => Some(MergeBehavior::Module),
161         ImportGranularity::Item | ImportGranularity::Preserve => None,
162     };
163     if !cfg.enforce_granularity {
164         let file_granularity = guess_granularity_from_scope(scope);
165         mb = match file_granularity {
166             ImportGranularityGuess::Unknown => mb,
167             ImportGranularityGuess::Item => None,
168             ImportGranularityGuess::Module => Some(MergeBehavior::Module),
169             ImportGranularityGuess::ModuleOrItem => mb.and(Some(MergeBehavior::Module)),
170             ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
171             ImportGranularityGuess::CrateOrModule => mb.or(Some(MergeBehavior::Crate)),
172         };
173     }
174
175     let use_item =
176         make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update();
177     // merge into existing imports if possible
178     if let Some(mb) = mb {
179         let filter = |it: &_| !(cfg.skip_glob_imports && ast::Use::is_simple_glob(it));
180         for existing_use in
181             scope.as_syntax_node().children().filter_map(ast::Use::cast).filter(filter)
182         {
183             if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
184                 ted::replace(existing_use.syntax(), merged.syntax());
185                 return;
186             }
187         }
188     }
189
190     // either we weren't allowed to merge or there is no import that fits the merge conditions
191     // so look for the place we have to insert to
192     insert_use_(scope, &path, cfg.group, use_item);
193 }
194
195 pub fn remove_path_if_in_use_stmt(path: &ast::Path) {
196     // FIXME: improve this
197     if path.parent_path().is_some() {
198         return;
199     }
200     if let Some(use_tree) = path.syntax().parent().and_then(ast::UseTree::cast) {
201         if use_tree.use_tree_list().is_some() || use_tree.star_token().is_some() {
202             return;
203         }
204         if let Some(use_) = use_tree.syntax().parent().and_then(ast::Use::cast) {
205             use_.remove();
206             return;
207         }
208         use_tree.remove();
209     }
210 }
211
212 #[derive(Eq, PartialEq, PartialOrd, Ord)]
213 enum ImportGroup {
214     // the order here defines the order of new group inserts
215     Std,
216     ExternCrate,
217     ThisCrate,
218     ThisModule,
219     SuperModule,
220 }
221
222 impl ImportGroup {
223     fn new(path: &ast::Path) -> ImportGroup {
224         let default = ImportGroup::ExternCrate;
225
226         let first_segment = match path.first_segment() {
227             Some(it) => it,
228             None => return default,
229         };
230
231         let kind = first_segment.kind().unwrap_or(PathSegmentKind::SelfKw);
232         match kind {
233             PathSegmentKind::SelfKw => ImportGroup::ThisModule,
234             PathSegmentKind::SuperKw => ImportGroup::SuperModule,
235             PathSegmentKind::CrateKw => ImportGroup::ThisCrate,
236             PathSegmentKind::Name(name) => match name.text().as_str() {
237                 "std" => ImportGroup::Std,
238                 "core" => ImportGroup::Std,
239                 _ => ImportGroup::ExternCrate,
240             },
241             // these aren't valid use paths, so fall back to something random
242             PathSegmentKind::SelfTypeKw => ImportGroup::ExternCrate,
243             PathSegmentKind::Type { .. } => ImportGroup::ExternCrate,
244         }
245     }
246 }
247
248 #[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
249 enum ImportGranularityGuess {
250     Unknown,
251     Item,
252     Module,
253     ModuleOrItem,
254     Crate,
255     CrateOrModule,
256 }
257
258 fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
259     // The idea is simple, just check each import as well as the import and its precedent together for
260     // whether they fulfill a granularity criteria.
261     let use_stmt = |item| match item {
262         ast::Item::Use(use_) => {
263             let use_tree = use_.use_tree()?;
264             Some((use_tree, use_.visibility(), use_.attrs()))
265         }
266         _ => None,
267     };
268     let mut use_stmts = match scope {
269         ImportScope::File(f) => f.items(),
270         ImportScope::Module(m) => m.items(),
271         ImportScope::Block(b) => b.items(),
272     }
273     .filter_map(use_stmt);
274     let mut res = ImportGranularityGuess::Unknown;
275     let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
276         Some(it) => it,
277         None => return res,
278     };
279     loop {
280         if let Some(use_tree_list) = prev.use_tree_list() {
281             if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
282                 // Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
283                 break ImportGranularityGuess::Crate;
284             } else {
285                 // Could still be crate-style so continue looking.
286                 res = ImportGranularityGuess::CrateOrModule;
287             }
288         }
289
290         let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
291             Some(it) => it,
292             None => break res,
293         };
294         if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone()) {
295             if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
296                 if let Some((prev_prefix, _)) = common_prefix(&prev_path, &curr_path) {
297                     if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
298                         let prefix_c = prev_prefix.qualifiers().count();
299                         let curr_c = curr_path.qualifiers().count() - prefix_c;
300                         let prev_c = prev_path.qualifiers().count() - prefix_c;
301                         if curr_c == 1 && prev_c == 1 {
302                             // Same prefix, only differing in the last segment and no use tree lists so this has to be of item style.
303                             break ImportGranularityGuess::Item;
304                         } else {
305                             // Same prefix and no use tree list but differs in more than one segment at the end. This might be module style still.
306                             res = ImportGranularityGuess::ModuleOrItem;
307                         }
308                     } else {
309                         // Same prefix with item tree lists, has to be module style as it
310                         // can't be crate style since the trees wouldn't share a prefix then.
311                         break ImportGranularityGuess::Module;
312                     }
313                 }
314             }
315         }
316         prev = curr;
317         prev_vis = curr_vis;
318         prev_attrs = curr_attrs;
319     }
320 }
321
322 fn insert_use_(
323     scope: &ImportScope,
324     insert_path: &ast::Path,
325     group_imports: bool,
326     use_item: ast::Use,
327 ) {
328     let scope_syntax = scope.as_syntax_node();
329     let group = ImportGroup::new(insert_path);
330     let path_node_iter = scope_syntax
331         .children()
332         .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
333         .flat_map(|(use_, node)| {
334             let tree = use_.use_tree()?;
335             let path = tree.path()?;
336             let has_tl = tree.use_tree_list().is_some();
337             Some((path, has_tl, node))
338         });
339
340     if group_imports {
341         // Iterator that discards anything thats not in the required grouping
342         // This implementation allows the user to rearrange their import groups as this only takes the first group that fits
343         let group_iter = path_node_iter
344             .clone()
345             .skip_while(|(path, ..)| ImportGroup::new(path) != group)
346             .take_while(|(path, ..)| ImportGroup::new(path) == group);
347
348         // track the last element we iterated over, if this is still None after the iteration then that means we never iterated in the first place
349         let mut last = None;
350         // find the element that would come directly after our new import
351         let post_insert: Option<(_, _, SyntaxNode)> = group_iter
352             .inspect(|(.., node)| last = Some(node.clone()))
353             .find(|&(ref path, has_tl, _)| {
354                 use_tree_path_cmp(insert_path, false, path, has_tl) != Ordering::Greater
355             });
356
357         if let Some((.., node)) = post_insert {
358             cov_mark::hit!(insert_group);
359             // insert our import before that element
360             return ted::insert(ted::Position::before(node), use_item.syntax());
361         }
362         if let Some(node) = last {
363             cov_mark::hit!(insert_group_last);
364             // there is no element after our new import, so append it to the end of the group
365             return ted::insert(ted::Position::after(node), use_item.syntax());
366         }
367
368         // the group we were looking for actually doesn't exist, so insert
369
370         let mut last = None;
371         // find the group that comes after where we want to insert
372         let post_group = path_node_iter
373             .inspect(|(.., node)| last = Some(node.clone()))
374             .find(|(p, ..)| ImportGroup::new(p) > group);
375         if let Some((.., node)) = post_group {
376             cov_mark::hit!(insert_group_new_group);
377             ted::insert(ted::Position::before(&node), use_item.syntax());
378             if let Some(node) = algo::non_trivia_sibling(node.into(), Direction::Prev) {
379                 ted::insert(ted::Position::after(node), make::tokens::single_newline());
380             }
381             return;
382         }
383         // there is no such group, so append after the last one
384         if let Some(node) = last {
385             cov_mark::hit!(insert_group_no_group);
386             ted::insert(ted::Position::after(&node), use_item.syntax());
387             ted::insert(ted::Position::after(node), make::tokens::single_newline());
388             return;
389         }
390     } else {
391         // There exists a group, so append to the end of it
392         if let Some((_, _, node)) = path_node_iter.last() {
393             cov_mark::hit!(insert_no_grouping_last);
394             ted::insert(ted::Position::after(node), use_item.syntax());
395             return;
396         }
397     }
398
399     let l_curly = match scope {
400         ImportScope::File(_) => None,
401         // don't insert the imports before the item list/block expr's opening curly brace
402         ImportScope::Module(item_list) => item_list.l_curly_token(),
403         // don't insert the imports before the item list's opening curly brace
404         ImportScope::Block(block) => block.l_curly_token(),
405     };
406     // there are no imports in this file at all
407     // so put the import after all inner module attributes and possible license header comments
408     if let Some(last_inner_element) = scope_syntax
409         .children_with_tokens()
410         // skip the curly brace
411         .skip(l_curly.is_some() as usize)
412         .take_while(|child| match child {
413             NodeOrToken::Node(node) => is_inner_attribute(node.clone()),
414             NodeOrToken::Token(token) => {
415                 [SyntaxKind::WHITESPACE, SyntaxKind::COMMENT, SyntaxKind::SHEBANG]
416                     .contains(&token.kind())
417             }
418         })
419         .filter(|child| child.as_token().map_or(true, |t| t.kind() != SyntaxKind::WHITESPACE))
420         .last()
421     {
422         cov_mark::hit!(insert_empty_inner_attr);
423         ted::insert(ted::Position::after(&last_inner_element), use_item.syntax());
424         ted::insert(ted::Position::after(last_inner_element), make::tokens::single_newline());
425     } else {
426         match l_curly {
427             Some(b) => {
428                 cov_mark::hit!(insert_empty_module);
429                 ted::insert(ted::Position::after(&b), make::tokens::single_newline());
430                 ted::insert(ted::Position::after(&b), use_item.syntax());
431             }
432             None => {
433                 cov_mark::hit!(insert_empty_file);
434                 ted::insert(
435                     ted::Position::first_child_of(scope_syntax),
436                     make::tokens::blank_line(),
437                 );
438                 ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
439             }
440         }
441     }
442 }
443
444 fn is_inner_attribute(node: SyntaxNode) -> bool {
445     ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner)
446 }