ide_db/imports/
insert_use.rs

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