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