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