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