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, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken,
12 };
13 
14 use crate::{
15     helpers::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)]
from(syntax: SyntaxNode) -> Option<Self>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.
find_insert_use_container( position: &SyntaxNode, sema: &Semantics<'_, RootDatabase>, ) -> Option<Self>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 
as_syntax_node(&self) -> &SyntaxNode138     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 
clone_for_update(&self) -> Self146     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.
insert_use(scope: &ImportScope, path: ast::Path, cfg: &InsertUseConfig)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 
remove_path_if_in_use_stmt(path: &ast::Path)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 {
new(path: &ast::Path) -> ImportGroup223     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             PathSegmentKind::Type { .. } => unreachable!(),
242         }
243     }
244 }
245 
246 #[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
247 enum ImportGranularityGuess {
248     Unknown,
249     Item,
250     Module,
251     ModuleOrItem,
252     Crate,
253     CrateOrModule,
254 }
255 
guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess256 fn guess_granularity_from_scope(scope: &ImportScope) -> ImportGranularityGuess {
257     // The idea is simple, just check each import as well as the import and its precedent together for
258     // whether they fulfill a granularity criteria.
259     let use_stmt = |item| match item {
260         ast::Item::Use(use_) => {
261             let use_tree = use_.use_tree()?;
262             Some((use_tree, use_.visibility(), use_.attrs()))
263         }
264         _ => None,
265     };
266     let mut use_stmts = match scope {
267         ImportScope::File(f) => f.items(),
268         ImportScope::Module(m) => m.items(),
269         ImportScope::Block(b) => b.items(),
270     }
271     .filter_map(use_stmt);
272     let mut res = ImportGranularityGuess::Unknown;
273     let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
274         Some(it) => it,
275         None => return res,
276     };
277     loop {
278         if let Some(use_tree_list) = prev.use_tree_list() {
279             if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
280                 // Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
281                 break ImportGranularityGuess::Crate;
282             } else {
283                 // Could still be crate-style so continue looking.
284                 res = ImportGranularityGuess::CrateOrModule;
285             }
286         }
287 
288         let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
289             Some(it) => it,
290             None => break res,
291         };
292         if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone()) {
293             if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
294                 if let Some((prev_prefix, _)) = common_prefix(&prev_path, &curr_path) {
295                     if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
296                         let prefix_c = prev_prefix.qualifiers().count();
297                         let curr_c = curr_path.qualifiers().count() - prefix_c;
298                         let prev_c = prev_path.qualifiers().count() - prefix_c;
299                         if curr_c == 1 && prev_c == 1 {
300                             // Same prefix, only differing in the last segment and no use tree lists so this has to be of item style.
301                             break ImportGranularityGuess::Item;
302                         } else {
303                             // Same prefix and no use tree list but differs in more than one segment at the end. This might be module style still.
304                             res = ImportGranularityGuess::ModuleOrItem;
305                         }
306                     } else {
307                         // Same prefix with item tree lists, has to be module style as it
308                         // can't be crate style since the trees wouldn't share a prefix then.
309                         break ImportGranularityGuess::Module;
310                     }
311                 }
312             }
313         }
314         prev = curr;
315         prev_vis = curr_vis;
316         prev_attrs = curr_attrs;
317     }
318 }
319 
insert_use_( scope: &ImportScope, insert_path: &ast::Path, group_imports: bool, use_item: ast::Use, )320 fn insert_use_(
321     scope: &ImportScope,
322     insert_path: &ast::Path,
323     group_imports: bool,
324     use_item: ast::Use,
325 ) {
326     let scope_syntax = scope.as_syntax_node();
327     let group = ImportGroup::new(insert_path);
328     let path_node_iter = scope_syntax
329         .children()
330         .filter_map(|node| ast::Use::cast(node.clone()).zip(Some(node)))
331         .flat_map(|(use_, node)| {
332             let tree = use_.use_tree()?;
333             let path = tree.path()?;
334             let has_tl = tree.use_tree_list().is_some();
335             Some((path, has_tl, node))
336         });
337 
338     if !group_imports {
339         if let Some((_, _, node)) = path_node_iter.last() {
340             cov_mark::hit!(insert_no_grouping_last);
341             ted::insert(ted::Position::after(node), use_item.syntax());
342         } else {
343             cov_mark::hit!(insert_no_grouping_last2);
344             ted::insert(ted::Position::first_child_of(scope_syntax), make::tokens::blank_line());
345             ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
346         }
347         return;
348     }
349 
350     // Iterator that discards anything thats not in the required grouping
351     // This implementation allows the user to rearrange their import groups as this only takes the first group that fits
352     let group_iter = path_node_iter
353         .clone()
354         .skip_while(|(path, ..)| ImportGroup::new(path) != group)
355         .take_while(|(path, ..)| ImportGroup::new(path) == group);
356 
357     // 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
358     let mut last = None;
359     // find the element that would come directly after our new import
360     let post_insert: Option<(_, _, SyntaxNode)> = group_iter
361         .inspect(|(.., node)| last = Some(node.clone()))
362         .find(|&(ref path, has_tl, _)| {
363             use_tree_path_cmp(insert_path, false, path, has_tl) != Ordering::Greater
364         });
365 
366     if let Some((.., node)) = post_insert {
367         cov_mark::hit!(insert_group);
368         // insert our import before that element
369         return ted::insert(ted::Position::before(node), use_item.syntax());
370     }
371     if let Some(node) = last {
372         cov_mark::hit!(insert_group_last);
373         // there is no element after our new import, so append it to the end of the group
374         return ted::insert(ted::Position::after(node), use_item.syntax());
375     }
376 
377     // the group we were looking for actually doesn't exist, so insert
378 
379     let mut last = None;
380     // find the group that comes after where we want to insert
381     let post_group = path_node_iter
382         .inspect(|(.., node)| last = Some(node.clone()))
383         .find(|(p, ..)| ImportGroup::new(p) > group);
384     if let Some((.., node)) = post_group {
385         cov_mark::hit!(insert_group_new_group);
386         ted::insert(ted::Position::before(&node), use_item.syntax());
387         if let Some(node) = algo::non_trivia_sibling(node.into(), Direction::Prev) {
388             ted::insert(ted::Position::after(node), make::tokens::single_newline());
389         }
390         return;
391     }
392     // there is no such group, so append after the last one
393     if let Some(node) = last {
394         cov_mark::hit!(insert_group_no_group);
395         ted::insert(ted::Position::after(&node), use_item.syntax());
396         ted::insert(ted::Position::after(node), make::tokens::single_newline());
397         return;
398     }
399     // there are no imports in this file at all
400     if let Some(last_inner_element) = scope_syntax
401         .children_with_tokens()
402         .filter(|child| match child {
403             NodeOrToken::Node(node) => is_inner_attribute(node.clone()),
404             NodeOrToken::Token(token) => is_inner_comment(token.clone()),
405         })
406         .last()
407     {
408         cov_mark::hit!(insert_group_empty_inner_attr);
409         ted::insert(ted::Position::after(&last_inner_element), use_item.syntax());
410         ted::insert(ted::Position::after(last_inner_element), make::tokens::single_newline());
411         return;
412     }
413     let l_curly = match scope {
414         ImportScope::File(_) => {
415             cov_mark::hit!(insert_group_empty_file);
416             ted::insert(ted::Position::first_child_of(scope_syntax), make::tokens::blank_line());
417             ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
418             return;
419         }
420         // don't insert the imports before the item list/block expr's opening curly brace
421         ImportScope::Module(item_list) => item_list.l_curly_token(),
422         // don't insert the imports before the item list's opening curly brace
423         ImportScope::Block(block) => block.l_curly_token(),
424     };
425     match l_curly {
426         Some(b) => {
427             cov_mark::hit!(insert_group_empty_module);
428             ted::insert(ted::Position::after(&b), make::tokens::single_newline());
429             ted::insert(ted::Position::after(&b), use_item.syntax());
430         }
431         None => {
432             // This should never happens, broken module syntax node
433             ted::insert(ted::Position::first_child_of(scope_syntax), make::tokens::blank_line());
434             ted::insert(ted::Position::first_child_of(scope_syntax), use_item.syntax());
435         }
436     }
437 }
438 
is_inner_attribute(node: SyntaxNode) -> bool439 fn is_inner_attribute(node: SyntaxNode) -> bool {
440     ast::Attr::cast(node).map(|attr| attr.kind()) == Some(ast::AttrKind::Inner)
441 }
442 
is_inner_comment(token: SyntaxToken) -> bool443 fn is_inner_comment(token: SyntaxToken) -> bool {
444     ast::Comment::cast(token).and_then(|comment| comment.kind().doc)
445         == Some(ast::CommentPlacement::Inner)
446 }
447