Skip to main content

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