Repository: rust-analyzer/rust-analyzer
Pull request #8898 (2021-05-20 17:56:52)

internal: resolve derive helpers
bors r+


Patch:
diff --git a/crates/hir_def/src/nameres/collector.rs b/crates/hir_def/src/nameres/collector.rs
index 3896be25d8..2d1cba632d 100644
--- a/crates/hir_def/src/nameres/collector.rs
+++ b/crates/hir_def/src/nameres/collector.rs
@@ -20,7 +20,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
 use syntax::ast;
 
 use crate::{
-    attr::{AttrId, Attrs},
+    attr::{Attr, AttrId, Attrs},
     builtin_attr,
     db::DefDatabase,
     derive_macro_as_call_id,
@@ -94,14 +94,14 @@ pub(super) fn collect_defs(
         unresolved_imports: Vec::new(),
         resolved_imports: Vec::new(),
 
-        unexpanded_macros: Vec::new(),
+        unresolved_macros: Vec::new(),
         mod_dirs: FxHashMap::default(),
         cfg_options,
         proc_macros,
         exports_proc_macros: false,
         from_glob_import: Default::default(),
-        ignore_attrs_on: FxHashSet::default(),
-        derive_helpers_in_scope: FxHashMap::default(),
+        ignore_attrs_on: Default::default(),
+        derive_helpers_in_scope: Default::default(),
     };
     match block {
         Some(block) => {
@@ -237,7 +237,7 @@ struct DefCollector<'a> {
     glob_imports: FxHashMap<LocalModuleId, Vec<(LocalModuleId, Visibility)>>,
     unresolved_imports: Vec<ImportDirective>,
     resolved_imports: Vec<ImportDirective>,
-    unexpanded_macros: Vec<MacroDirective>,
+    unresolved_macros: Vec<MacroDirective>,
     mod_dirs: FxHashMap<LocalModuleId, ModDir>,
     cfg_options: &'a CfgOptions,
     /// List of procedural macros defined by this crate. This is read from the dynamic library
@@ -247,7 +247,13 @@ struct DefCollector<'a> {
     proc_macros: Vec<(Name, ProcMacroExpander)>,
     exports_proc_macros: bool,
     from_glob_import: PerNsGlobImports,
-    ignore_attrs_on: FxHashSet<InFile<ModItem>>,
+    /// If we fail to resolve an attribute on a `ModItem`, we fall back to ignoring the attribute.
+    /// This map is used to skip all attributes up to and including the one that failed to resolve,
+    /// in order to not expand them twice.
+    ///
+    /// This also stores the attributes to skip when we resolve derive helpers and non-macro
+    /// non-builtin attributes in general.
+    ignore_attrs_on: FxHashMap<InFile<ModItem>, AttrId>,
     /// Tracks which custom derives are in scope for an item, to allow resolution of derive helper
     /// attributes.
     derive_helpers_in_scope: FxHashMap<AstId<ast::Item>, Vec<Name>>,
@@ -319,7 +325,7 @@ impl DefCollector<'_> {
                 }
             }
 
-            if self.reseed_with_unresolved_attributes() == ReachedFixedPoint::Yes {
+            if self.reseed_with_unresolved_attribute() == ReachedFixedPoint::Yes {
                 break;
             }
         }
@@ -362,25 +368,21 @@ impl DefCollector<'_> {
     }
 
     /// When the fixed-point loop reaches a stable state, we might still have some unresolved
-    /// attributes (or unexpanded attribute proc macros) left over. This takes them, and feeds the
-    /// item they're applied to back into name resolution.
+    /// attributes (or unexpanded attribute proc macros) left over. This takes one of them, and
+    /// feeds the item it's applied to back into name resolution.
     ///
     /// This effectively ignores the fact that the macro is there and just treats the items as
     /// normal code.
     ///
     /// This improves UX when proc macros are turned off or don't work, and replicates the behavior
     /// before we supported proc. attribute macros.
-    fn reseed_with_unresolved_attributes(&mut self) -> ReachedFixedPoint {
+    fn reseed_with_unresolved_attribute(&mut self) -> ReachedFixedPoint {
         cov_mark::hit!(unresolved_attribute_fallback);
 
-        let mut added_items = false;
-        let unexpanded_macros = std::mem::replace(&mut self.unexpanded_macros, Vec::new());
-        for directive in &unexpanded_macros {
-            if let MacroDirectiveKind::Attr { ast_id, mod_item, .. } = &directive.kind {
-                // Make sure to only add such items once.
-                if !self.ignore_attrs_on.insert(ast_id.ast_id.with_value(*mod_item)) {
-                    continue;
-                }
+        let mut unresolved_macros = std::mem::replace(&mut self.unresolved_macros, Vec::new());
+        let pos = unresolved_macros.iter().position(|directive| {
+            if let MacroDirectiveKind::Attr { ast_id, mod_item, attr } = &directive.kind {
+                self.ignore_attrs_on.insert(ast_id.ast_id.with_value(*mod_item), *attr);
 
                 let file_id = self.def_map[directive.module_id].definition_source(self.db).file_id;
                 let item_tree = self.db.file_item_tree(file_id);
@@ -394,14 +396,20 @@ impl DefCollector<'_> {
                     mod_dir,
                 }
                 .collect(&[*mod_item]);
-                added_items = true;
+                true
+            } else {
+                false
             }
+        });
+
+        if let Some(pos) = pos {
+            unresolved_macros.remove(pos);
         }
 
         // The collection above might add new unresolved macros (eg. derives), so merge the lists.
-        self.unexpanded_macros.extend(unexpanded_macros);
+        self.unresolved_macros.extend(unresolved_macros);
 
-        if added_items {
+        if pos.is_some() {
             // Continue name resolution with the new data.
             ReachedFixedPoint::No
         } else {
@@ -873,7 +881,7 @@ impl DefCollector<'_> {
     }
 
     fn resolve_macros(&mut self) -> ReachedFixedPoint {
-        let mut macros = std::mem::replace(&mut self.unexpanded_macros, Vec::new());
+        let mut macros = std::mem::replace(&mut self.unresolved_macros, Vec::new());
         let mut resolved = Vec::new();
         let mut res = ReachedFixedPoint::Yes;
         macros.retain(|directive| {
@@ -922,14 +930,45 @@ impl DefCollector<'_> {
                         Err(UnresolvedMacro { .. }) => (),
                     }
                 }
-                MacroDirectiveKind::Attr { .. } => {
-                    // not yet :)
+                MacroDirectiveKind::Attr { ast_id, mod_item, attr } => {
+                    if let Some(ident) = ast_id.path.as_ident() {
+                        if let Some(helpers) = self.derive_helpers_in_scope.get(&ast_id.ast_id) {
+                            if helpers.contains(ident) {
+                                cov_mark::hit!(resolved_derive_helper);
+
+                                // Resolved to derive helper. Collect the item's attributes again,
+                                // starting after the derive helper.
+                                let file_id = self.def_map[directive.module_id]
+                                    .definition_source(self.db)
+                                    .file_id;
+                                let item_tree = self.db.file_item_tree(file_id);
+                                let mod_dir = self.mod_dirs[&directive.module_id].clone();
+                                self.ignore_attrs_on.insert(InFile::new(file_id, *mod_item), *attr);
+                                ModCollector {
+                                    def_collector: &mut *self,
+                                    macro_depth: directive.depth,
+                                    module_id: directive.module_id,
+                                    file_id,
+                                    item_tree: &item_tree,
+                                    mod_dir,
+                                }
+                                .collect(&[*mod_item]);
+
+                                // Remove the original directive since we resolved it.
+                                return false;
+                            }
+                        }
+                    }
+
+                    // Not resolved to a derive helper, so try to resolve as a macro.
+                    // FIXME: not yet :)
                 }
             }
 
             true
         });
-        self.unexpanded_macros = macros;
+        // Attribute resolution can add unresolved macro invocations, so concatenate the lists.
+        self.unresolved_macros.extend(macros);
 
         for (module_id, macro_call_id, depth) in resolved {
             self.collect_macro_expansion(module_id, macro_call_id, depth);
@@ -1000,7 +1039,7 @@ impl DefCollector<'_> {
     fn finish(mut self) -> DefMap {
         // Emit diagnostics for all remaining unexpanded macros.
 
-        for directive in &self.unexpanded_macros {
+        for directive in &self.unresolved_macros {
             match &directive.kind {
                 MacroDirectiveKind::FnLike { ast_id, fragment } => match macro_call_as_call_id(
                     ast_id,
@@ -1102,7 +1141,7 @@ impl ModCollector<'_, '_> {
 
         // Prelude module is always considered to be `#[macro_use]`.
         if let Some(prelude_module) = self.def_collector.def_map.prelude {
-            if prelude_module.krate != self.def_collector.def_map.krate {
+            if prelude_module.krate != krate {
                 cov_mark::hit!(prelude_is_macro_use);
                 self.def_collector.import_all_macros_exported(self.module_id, prelude_module.krate);
             }
@@ -1203,11 +1242,6 @@ impl ModCollector<'_, '_> {
                 ModItem::Struct(id) => {
                     let it = &self.item_tree[id];
 
-                    // FIXME: check attrs to see if this is an attribute macro invocation;
-                    // in which case we don't add the invocation, just a single attribute
-                    // macro invocation
-                    self.collect_derives(&attrs, it.ast_id.upcast());
-
                     def = Some(DefData {
                         id: StructLoc { container: module, id: ItemTreeId::new(self.file_id, id) }
                             .intern(self.def_collector.db)
@@ -1220,11 +1254,6 @@ impl ModCollector<'_, '_> {
                 ModItem::Union(id) => {
                     let it = &self.item_tree[id];
 
-                    // FIXME: check attrs to see if this is an attribute macro invocation;
-                    // in which case we don't add the invocation, just a single attribute
-                    // macro invocation
-                    self.collect_derives(&attrs, it.ast_id.upcast());
-
                     def = Some(DefData {
                         id: UnionLoc { container: module, id: ItemTreeId::new(self.file_id, id) }
                             .intern(self.def_collector.db)
@@ -1237,11 +1266,6 @@ impl ModCollector<'_, '_> {
                 ModItem::Enum(id) => {
                     let it = &self.item_tree[id];
 
-                    // FIXME: check attrs to see if this is an attribute macro invocation;
-                    // in which case we don't add the invocation, just a single attribute
-                    // macro invocation
-                    self.collect_derives(&attrs, it.ast_id.upcast());
-
                     def = Some(DefData {
                         id: EnumLoc { container: module, id: ItemTreeId::new(self.file_id, id) }
                             .intern(self.def_collector.db)
@@ -1453,6 +1477,9 @@ impl ModCollector<'_, '_> {
     ///
     /// Returns `Err` when some attributes could not be resolved to builtins and have been
     /// registered as unresolved.
+    ///
+    /// If `ignore_up_to` is `Some`, attributes precending and including that attribute will be
+    /// assumed to be resolved already.
     fn resolve_attributes(&mut self, attrs: &Attrs, mod_item: ModItem) -> Result<(), ()> {
         fn is_builtin_attr(path: &ModPath) -> bool {
             if path.kind == PathKind::Plain {
@@ -1478,51 +1505,68 @@ impl ModCollector<'_, '_> {
             false
         }
 
-        // We failed to resolve an attribute on this item earlier, and are falling back to treating
-        // the item as-is.
-        if self.def_collector.ignore_attrs_on.contains(&InFile::new(self.file_id, mod_item)) {
-            return Ok(());
-        }
-
-        match attrs.iter().find(|attr| !is_builtin_attr(&attr.path)) {
-            Some(non_builtin_attr) => {
-                log::debug!("non-builtin attribute {}", non_builtin_attr.path);
+        let mut ignore_up_to =
+            self.def_collector.ignore_attrs_on.get(&InFile::new(self.file_id, mod_item)).copied();
+        for attr in attrs.iter().skip_while(|attr| match ignore_up_to {
+            Some(id) if attr.id == id => {
+                ignore_up_to = None;
+                true
+            }
+            Some(_) => true,
+            None => false,
+        }) {
+            if attr.path.as_ident() == Some(&hir_expand::name![derive]) {
+                self.collect_derive(attr, mod_item);
+            } else if is_builtin_attr(&attr.path) {
+                continue;
+            } else {
+                log::debug!("non-builtin attribute {}", attr.path);
 
                 let ast_id = AstIdWithPath::new(
                     self.file_id,
                     mod_item.ast_id(self.item_tree),
-                    non_builtin_attr.path.as_ref().clone(),
+                    attr.path.as_ref().clone(),
                 );
-                self.def_collector.unexpanded_macros.push(MacroDirective {
+                self.def_collector.unresolved_macros.push(MacroDirective {
                     module_id: self.module_id,
                     depth: self.macro_depth + 1,
-                    kind: MacroDirectiveKind::Attr { ast_id, attr: non_builtin_attr.id, mod_item },
+                    kind: MacroDirectiveKind::Attr { ast_id, attr: attr.id, mod_item },
                 });
 
-                Err(())
+                return Err(());
             }
-            None => Ok(()),
         }
+
+        Ok(())
     }
 
-    fn collect_derives(&mut self, attrs: &Attrs, ast_id: FileAstId<ast::Item>) {
-        for derive in attrs.by_key("derive").attrs() {
-            match derive.parse_derive() {
-                Some(derive_macros) => {
-                    for path in derive_macros {
-                        let ast_id = AstIdWithPath::new(self.file_id, ast_id, path);
-                        self.def_collector.unexpanded_macros.push(MacroDirective {
-                            module_id: self.module_id,
-                            depth: self.macro_depth + 1,
-                            kind: MacroDirectiveKind::Derive { ast_id, derive_attr: derive.id },
-                        });
-                    }
-                }
-                None => {
-                    // FIXME: diagnose
-                    log::debug!("malformed derive: {:?}", derive);
+    fn collect_derive(&mut self, attr: &Attr, mod_item: ModItem) {
+        let ast_id: FileAstId<ast::Item> = match mod_item {
+            ModItem::Struct(it) => self.item_tree[it].ast_id.upcast(),
+            ModItem::Union(it) => self.item_tree[it].ast_id.upcast(),
+            ModItem::Enum(it) => self.item_tree[it].ast_id.upcast(),
+            _ => {
+                // Cannot use derive on this item.
+                // FIXME: diagnose
+                return;
+            }
+        };
+
+        match attr.parse_derive() {
+            Some(derive_macros) => {
+                for path in derive_macros {
+                    let ast_id = AstIdWithPath::new(self.file_id, ast_id, path);
+                    self.def_collector.unresolved_macros.push(MacroDirective {
+                        module_id: self.module_id,
+                        depth: self.macro_depth + 1,
+                        kind: MacroDirectiveKind::Derive { ast_id, derive_attr: attr.id },
+                    });
                 }
             }
+            None => {
+                // FIXME: diagnose
+                log::debug!("malformed derive: {:?}", attr);
+            }
         }
     }
 
@@ -1686,7 +1730,7 @@ impl ModCollector<'_, '_> {
             ast_id.path.kind = PathKind::Super(0);
         }
 
-        self.def_collector.unexpanded_macros.push(MacroDirective {
+        self.def_collector.unresolved_macros.push(MacroDirective {
             module_id: self.module_id,
             depth: self.macro_depth + 1,
             kind: MacroDirectiveKind::FnLike { ast_id, fragment: mac.fragment },
@@ -1731,13 +1775,13 @@ mod tests {
             glob_imports: FxHashMap::default(),
             unresolved_imports: Vec::new(),
             resolved_imports: Vec::new(),
-            unexpanded_macros: Vec::new(),
+            unresolved_macros: Vec::new(),
             mod_dirs: FxHashMap::default(),
             cfg_options: &CfgOptions::default(),
             proc_macros: Default::default(),
             exports_proc_macros: false,
             from_glob_import: Default::default(),
-            ignore_attrs_on: FxHashSet::default(),
+            ignore_attrs_on: Default::default(),
             derive_helpers_in_scope: FxHashMap::default(),
         };
         collector.seed_with_top_level();
diff --git a/crates/ide_assists/src/handlers/auto_import.rs b/crates/ide_assists/src/handlers/auto_import.rs
index d4748ef3a0..dda5a6631c 100644
--- a/crates/ide_assists/src/handlers/auto_import.rs
+++ b/crates/ide_assists/src/handlers/auto_import.rs
@@ -33,19 +33,20 @@ use crate::{AssistContext, AssistId, AssistKind, Assists, GroupLabel};
 // use super::AssistContext;
 // ```
 //
-// .Import Granularity
+// .Merge Behavior
 //
-// It is possible to configure how use-trees are merged with the `importGranularity` setting.
+// It is possible to configure how use-trees are merged with the `importMergeBehavior` setting.
 // It has the following configurations:
 //
-// - `crate`: Merge imports from the same crate into a single use statement. This kind of
+// - `full`: This setting will cause auto-import to always completely merge use-trees that share the
+//  same path prefix while also merging inner trees that share the same path-prefix. This kind of
 //  nesting is only supported in Rust versions later than 1.24.
-// - `module`: Merge imports from the same module into a single use statement.
-// - `item`: Don't merge imports at all, creating one import per item.
-// - `preserve`: Do not change the granularity of any imports. For auto-import this has the same
-//  effect as `item`.
+// - `last`: This setting will cause auto-import to merge use-trees as long as the resulting tree
+//  will only contain a nesting of single segment paths at the very end.
+// - `none`: This setting will cause auto-import to never merge use-trees keeping them as simple
+//  paths.
 //
-// In `VS Code` the configuration for this is `rust-analyzer.assist.importGranularity`.
+// In `VS Code` the configuration for this is `rust-analyzer.assist.importMergeBehavior`.
 //
 // .Import Prefix
 //
diff --git a/crates/ide_assists/src/handlers/merge_imports.rs b/crates/ide_assists/src/handlers/merge_imports.rs
index fc117bd9a8..31854840c2 100644
--- a/crates/ide_assists/src/handlers/merge_imports.rs
+++ b/crates/ide_assists/src/handlers/merge_imports.rs
@@ -212,20 +212,6 @@ pub(crate) use std::fmt::{Debug, Display};
         )
     }
 
-    #[test]
-    fn merge_pub_in_path_crate() {
-        check_assist(
-            merge_imports,
-            r"
-pub(in this::path) use std::fmt$0::Debug;
-pub(in this::path) use std::fmt::Display;
-",
-            r"
-pub(in this::path) use std::fmt::{Debug, Display};
-",
-        )
-    }
-
     #[test]
     fn test_merge_nested() {
         check_assist(
diff --git a/crates/ide_db/src/helpers/insert_use.rs b/crates/ide_db/src/helpers/insert_use.rs
index aa61c5bcb4..55cdc4da3f 100644
--- a/crates/ide_db/src/helpers/insert_use.rs
+++ b/crates/ide_db/src/helpers/insert_use.rs
@@ -4,36 +4,20 @@ use std::cmp::Ordering;
 use hir::Semantics;
 use syntax::{
     algo,
-    ast::{self, make, AstNode, AttrsOwner, ModuleItemOwner, PathSegmentKind, VisibilityOwner},
+    ast::{self, make, AstNode, PathSegmentKind},
     ted, AstToken, Direction, NodeOrToken, SyntaxNode, SyntaxToken,
 };
 
 use crate::{
-    helpers::merge_imports::{
-        common_prefix, eq_attrs, eq_visibility, try_merge_imports, use_tree_path_cmp, MergeBehavior,
-    },
+    helpers::merge_imports::{try_merge_imports, use_tree_path_cmp, MergeBehavior},
     RootDatabase,
 };
 
 pub use hir::PrefixKind;
 
-/// How imports should be grouped into use statements.
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum ImportGranularity {
-    /// Do not change the granularity of any imports and preserve the original structure written by the developer.
-    Preserve,
-    /// Merge imports from the same crate into a single use statement.
-    Crate,
-    /// Merge imports from the same module into a single use statement.
-    Module,
-    /// Flatten imports so that each has its own use statement.
-    Item,
-}
-
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 pub struct InsertUseConfig {
-    pub granularity: ImportGranularity,
-    pub enforce_granularity: bool,
+    pub merge: Option<MergeBehavior>,
     pub prefix_kind: PrefixKind,
     pub group: bool,
 }
@@ -81,96 +65,15 @@ impl ImportScope {
             ImportScope::Module(item_list) => ImportScope::Module(item_list.clone_for_update()),
         }
     }
-
-    fn guess_granularity_from_scope(&self) -> ImportGranularityGuess {
-        // The idea is simple, just check each import as well as the import and its precedent together for
-        // whether they fulfill a granularity criteria.
-        let use_stmt = |item| match item {
-            ast::Item::Use(use_) => {
-                let use_tree = use_.use_tree()?;
-                Some((use_tree, use_.visibility(), use_.attrs()))
-            }
-            _ => None,
-        };
-        let mut use_stmts = match self {
-            ImportScope::File(f) => f.items(),
-            ImportScope::Module(m) => m.items(),
-        }
-        .filter_map(use_stmt);
-        let mut res = ImportGranularityGuess::Unknown;
-        let (mut prev, mut prev_vis, mut prev_attrs) = match use_stmts.next() {
-            Some(it) => it,
-            None => return res,
-        };
-        loop {
-            if let Some(use_tree_list) = prev.use_tree_list() {
-                if use_tree_list.use_trees().any(|tree| tree.use_tree_list().is_some()) {
-                    // Nested tree lists can only occur in crate style, or with no proper style being enforced in the file.
-                    break ImportGranularityGuess::Crate;
-                } else {
-                    // Could still be crate-style so continue looking.
-                    res = ImportGranularityGuess::CrateOrModule;
-                }
-            }
-
-            let (curr, curr_vis, curr_attrs) = match use_stmts.next() {
-                Some(it) => it,
-                None => break res,
-            };
-            if eq_visibility(prev_vis, curr_vis.clone()) && eq_attrs(prev_attrs, curr_attrs.clone())
-            {
-                if let Some((prev_path, curr_path)) = prev.path().zip(curr.path()) {
-                    if let Some(_) = common_prefix(&prev_path, &curr_path) {
-                        if prev.use_tree_list().is_none() && curr.use_tree_list().is_none() {
-                            // Same prefix but no use tree lists so this has to be of item style.
-                            break ImportGranularityGuess::Item; // this overwrites CrateOrModule, technically the file doesn't adhere to anything here.
-                        } else {
-                            // Same prefix with item tree lists, has to be module style as it
-                            // can't be crate style since the trees wouldn't share a prefix then.
-                            break ImportGranularityGuess::Module;
-                        }
-                    }
-                }
-            }
-            prev = curr;
-            prev_vis = curr_vis;
-            prev_attrs = curr_attrs;
-        }
-    }
-}
-
-#[derive(PartialEq, PartialOrd, Debug, Clone, Copy)]
-enum ImportGranularityGuess {
-    Unknown,
-    Item,
-    Module,
-    Crate,
-    CrateOrModule,
 }
 
 /// Insert an import path into the given file/node. A `merge` value of none indicates that no import merging is allowed to occur.
 pub fn insert_use<'a>(scope: &ImportScope, path: ast::Path, cfg: InsertUseConfig) {
     let _p = profile::span("insert_use");
-    let mut mb = match cfg.granularity {
-        ImportGranularity::Crate => Some(MergeBehavior::Crate),
-        ImportGranularity::Module => Some(MergeBehavior::Module),
-        ImportGranularity::Item | ImportGranularity::Preserve => None,
-    };
-    if !cfg.enforce_granularity {
-        let file_granularity = scope.guess_granularity_from_scope();
-        mb = match file_granularity {
-            ImportGranularityGuess::Unknown => mb,
-            ImportGranularityGuess::Item => None,
-            ImportGranularityGuess::Module => Some(MergeBehavior::Module),
-            ImportGranularityGuess::Crate => Some(MergeBehavior::Crate),
-            ImportGranularityGuess::CrateOrModule => mb.or(Some(MergeBehavior::Crate)),
-        };
-    }
-
     let use_item =
         make::use_(None, make::use_tree(path.clone(), None, None, false)).clone_for_update();
     // merge into existing imports if possible
-    if let Some(mb) = mb {
+    if let Some(mb) = cfg.merge {
         for existing_use in scope.as_syntax_node().children().filter_map(ast::Use::cast) {
             if let Some(merged) = try_merge_imports(&existing_use, &use_item, mb) {
                 ted::replace(existing_use.syntax(), merged.syntax());
diff --git a/crates/ide_db/src/helpers/merge_imports.rs b/crates/ide_db/src/helpers/merge_imports.rs
index 0dbabb44fb..8fb40e8371 100644
--- a/crates/ide_db/src/helpers/merge_imports.rs
+++ b/crates/ide_db/src/helpers/merge_imports.rs
@@ -181,7 +181,7 @@ fn recursive_merge(
 }
 
 /// Traverses both paths until they differ, returning the common prefix of both.
-pub fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
+fn common_prefix(lhs: &ast::Path, rhs: &ast::Path) -> Option<(ast::Path, ast::Path)> {
     let mut res = None;
     let mut lhs_curr = lhs.first_qualifier_or_self();
     let mut rhs_curr = rhs.first_qualifier_or_self();
@@ -289,26 +289,23 @@ fn path_segment_cmp(a: &ast::PathSegment, b: &ast::PathSegment) -> Ordering {
     a.as_ref().map(ast::NameRef::text).cmp(&b.as_ref().map(ast::NameRef::text))
 }
 
-pub fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
+fn eq_visibility(vis0: Option<ast::Visibility>, vis1: Option<ast::Visibility>) -> bool {
     match (vis0, vis1) {
         (None, None) => true,
-        (Some(vis0), Some(vis1)) => vis0.is_eq_to(&vis1),
+        // FIXME: Don't use the string representation to check for equality
+        // spaces inside of the node would break this comparison
+        (Some(vis0), Some(vis1)) => vis0.to_string() == vis1.to_string(),
         _ => false,
     }
 }
 
-pub fn eq_attrs(
+fn eq_attrs(
     attrs0: impl Iterator<Item = ast::Attr>,
     attrs1: impl Iterator<Item = ast::Attr>,
 ) -> bool {
-    // FIXME order of attributes should not matter
-    let attrs0 = attrs0
-        .flat_map(|attr| attr.syntax().descendants_with_tokens())
-        .flat_map(|it| it.into_token());
-    let attrs1 = attrs1
-        .flat_map(|attr| attr.syntax().descendants_with_tokens())
-        .flat_map(|it| it.into_token());
-    stdx::iter_eq_by(attrs0, attrs1, |tok, tok2| tok.text() == tok2.text())
+    let attrs0 = attrs0.map(|attr| attr.to_string());
+    let attrs1 = attrs1.map(|attr| attr.to_string());
+    attrs0.eq(attrs1)
 }
 
 fn path_is_self(path: &ast::Path) -> bool {
diff --git a/crates/rust-analyzer/src/config.rs b/crates/rust-analyzer/src/config.rs
index b700d025f1..339014fd31 100644
--- a/crates/rust-analyzer/src/config.rs
+++ b/crates/rust-analyzer/src/config.rs
@@ -12,7 +12,8 @@ use std::{ffi::OsString, iter, path::PathBuf};
 use flycheck::FlycheckConfig;
 use ide::{AssistConfig, CompletionConfig, DiagnosticsConfig, HoverConfig, InlayHintsConfig};
 use ide_db::helpers::{
-    insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
+    insert_use::{InsertUseConfig, PrefixKind},
+    merge_imports::MergeBehavior,
     SnippetCap,
 };
 use lsp_types::{ClientCapabilities, MarkupKind};
@@ -34,18 +35,15 @@ use crate::{
 // be specified directly in `package.json`.
 config_data! {
     struct ConfigData {
-        /// How imports should be grouped into use statements.
-        assist_importGranularity |
+        /// The strategy to use when inserting new imports or merging imports.
         assist_importMergeBehavior |
-        assist_importMergeBehaviour: ImportGranularityDef  = "\"crate\"",
-        /// Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.
-        assist_importEnforceGranularity: bool              = "false",
+        assist_importMergeBehaviour: MergeBehaviorDef  = "\"crate\"",
         /// The path structure for newly inserted paths to use.
-        assist_importPrefix: ImportPrefixDef               = "\"plain\"",
+        assist_importPrefix: ImportPrefixDef           = "\"plain\"",
         /// Group inserted imports by the [following order](https://rust-analyzer.github.io/manual.html#auto-import). Groups are separated by newlines.
-        assist_importGroup: bool                           = "true",
+        assist_importGroup: bool                       = "true",
         /// Show function name and docs in parameter hints.
-        callInfo_full: bool                                = "true",
+        callInfo_full: bool = "true",
 
         /// Automatically refresh project info via `cargo metadata` on
         /// `Cargo.toml` changes.
@@ -626,13 +624,11 @@ impl Config {
     }
     fn insert_use_config(&self) -> InsertUseConfig {
         InsertUseConfig {
-            granularity: match self.data.assist_importGranularity {
-                ImportGranularityDef::Preserve => ImportGranularity::Preserve,
-                ImportGranularityDef::Item => ImportGranularity::Item,
-                ImportGranularityDef::Crate => ImportGranularity::Crate,
-                ImportGranularityDef::Module => ImportGranularity::Module,
+            merge: match self.data.assist_importMergeBehavior {
+                MergeBehaviorDef::None => None,
+                MergeBehaviorDef::Crate => Some(MergeBehavior::Crate),
+                MergeBehaviorDef::Module => Some(MergeBehavior::Module),
             },
-            enforce_granularity: self.data.assist_importEnforceGranularity,
             prefix_kind: match self.data.assist_importPrefix {
                 ImportPrefixDef::Plain => PrefixKind::Plain,
                 ImportPrefixDef::ByCrate => PrefixKind::ByCrate,
@@ -752,10 +748,8 @@ enum ManifestOrProjectJson {
 
 #[derive(Deserialize, Debug, Clone)]
 #[serde(rename_all = "snake_case")]
-enum ImportGranularityDef {
-    Preserve,
-    #[serde(alias = "none")]
-    Item,
+enum MergeBehaviorDef {
+    None,
     #[serde(alias = "full")]
     Crate,
     #[serde(alias = "last")]
@@ -788,7 +782,7 @@ macro_rules! _config_data {
     (struct $name:ident {
         $(
             $(#[doc=$doc:literal])*
-            $field:ident $(| $alias:ident)*: $ty:ty = $default:expr,
+            $field:ident $(| $alias:ident)?: $ty:ty = $default:expr,
         )*
     }) => {
         #[allow(non_snake_case)]
@@ -800,7 +794,7 @@ macro_rules! _config_data {
                     $field: get_field(
                         &mut json,
                         stringify!($field),
-                        None$(.or(Some(stringify!($alias))))*,
+                        None$(.or(Some(stringify!($alias))))?,
                         $default,
                     ),
                 )*}
@@ -937,16 +931,6 @@ fn field_props(field: &str, ty: &str, doc: &[&str], default: &str) -> serde_json
                 "Merge imports from the same module into a single `use` statement."
             ],
         },
-        "ImportGranularityDef" => set! {
-            "type": "string",
-            "enum": ["preserve", "crate", "module", "item"],
-            "enumDescriptions": [
-                "Do not change the granularity of any imports and preserve the original structure written by the developer.",
-                "Merge imports from the same crate into a single use statement. Conversely, imports from different crates are split into separate statements.",
-                "Merge imports from the same module into a single use statement. Conversely, imports from different modules are split into separate statements.",
-                "Flatten imports so that each has its own use statement."
-            ],
-        },
         "ImportPrefixDef" => set! {
             "type": "string",
             "enum": [
diff --git a/crates/rust-analyzer/src/integrated_benchmarks.rs b/crates/rust-analyzer/src/integrated_benchmarks.rs
index 781073fe5b..ba2790acbd 100644
--- a/crates/rust-analyzer/src/integrated_benchmarks.rs
+++ b/crates/rust-analyzer/src/integrated_benchmarks.rs
@@ -13,10 +13,7 @@
 use std::{convert::TryFrom, sync::Arc};
 
 use ide::{Change, CompletionConfig, FilePosition, TextSize};
-use ide_db::helpers::{
-    insert_use::{ImportGranularity, InsertUseConfig},
-    SnippetCap,
-};
+use ide_db::helpers::{insert_use::InsertUseConfig, merge_imports::MergeBehavior, SnippetCap};
 use test_utils::project_root;
 use vfs::{AbsPathBuf, VfsPath};
 
@@ -136,9 +133,8 @@ fn integrated_completion_benchmark() {
             add_call_argument_snippets: true,
             snippet_cap: SnippetCap::new(true),
             insert_use: InsertUseConfig {
-                granularity: ImportGranularity::Crate,
+                merge: Some(MergeBehavior::Crate),
                 prefix_kind: hir::PrefixKind::ByCrate,
-                enforce_granularity: true,
                 group: true,
             },
         };
@@ -170,9 +166,8 @@ fn integrated_completion_benchmark() {
             add_call_argument_snippets: true,
             snippet_cap: SnippetCap::new(true),
             insert_use: InsertUseConfig {
-                granularity: ImportGranularity::Crate,
+                merge: Some(MergeBehavior::Crate),
                 prefix_kind: hir::PrefixKind::ByCrate,
-                enforce_granularity: true,
                 group: true,
             },
         };
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index ef9e0aee9b..9dec46c789 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -1145,7 +1145,7 @@ mod tests {
 
     use ide::Analysis;
     use ide_db::helpers::{
-        insert_use::{ImportGranularity, InsertUseConfig, PrefixKind},
+        insert_use::{InsertUseConfig, PrefixKind},
         SnippetCap,
     };
 
@@ -1177,9 +1177,8 @@ mod tests {
                     add_call_argument_snippets: true,
                     snippet_cap: SnippetCap::new(true),
                     insert_use: InsertUseConfig {
-                        granularity: ImportGranularity::Item,
+                        merge: None,
                         prefix_kind: PrefixKind::Plain,
-                        enforce_granularity: true,
                         group: true,
                     },
                 },
diff --git a/crates/stdx/src/lib.rs b/crates/stdx/src/lib.rs
index 18d5fadb9c..340fcacfa3 100644
--- a/crates/stdx/src/lib.rs
+++ b/crates/stdx/src/lib.rs
@@ -140,34 +140,6 @@ impl JodChild {
     }
 }
 
-// feature: iter_order_by
-// Iterator::eq_by
-pub fn iter_eq_by<I, I2, F>(this: I2, other: I, mut eq: F) -> bool
-where
-    I: IntoIterator,
-    I2: IntoIterator,
-    F: FnMut(I2::Item, I::Item) -> bool,
-{
-    let mut other = other.into_iter();
-    let mut this = this.into_iter();
-
-    loop {
-        let x = match this.next() {
-            None => return other.next().is_none(),
-            Some(val) => val,
-        };
-
-        let y = match other.next() {
-            None => return false,
-            Some(val) => val,
-        };
-
-        if !eq(x, y) {
-            return false;
-        }
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/crates/syntax/src/ast/node_ext.rs b/crates/syntax/src/ast/node_ext.rs
index df8f98b5b5..bef49238f5 100644
--- a/crates/syntax/src/ast/node_ext.rs
+++ b/crates/syntax/src/ast/node_ext.rs
@@ -608,29 +608,6 @@ impl ast::Visibility {
             None => VisibilityKind::Pub,
         }
     }
-
-    pub fn is_eq_to(&self, other: &Self) -> bool {
-        match (self.kind(), other.kind()) {
-            (VisibilityKind::In(this), VisibilityKind::In(other)) => {
-                stdx::iter_eq_by(this.segments(), other.segments(), |lhs, rhs| {
-                    lhs.kind().zip(rhs.kind()).map_or(false, |it| match it {
-                        (PathSegmentKind::CrateKw, PathSegmentKind::CrateKw)
-                        | (PathSegmentKind::SelfKw, PathSegmentKind::SelfKw)
-                        | (PathSegmentKind::SuperKw, PathSegmentKind::SuperKw) => true,
-                        (PathSegmentKind::Name(lhs), PathSegmentKind::Name(rhs)) => {
-                            lhs.text() == rhs.text()
-                        }
-                        _ => false,
-                    })
-                })
-            }
-            (VisibilityKind::PubSelf, VisibilityKind::PubSelf)
-            | (VisibilityKind::PubSuper, VisibilityKind::PubSuper)
-            | (VisibilityKind::PubCrate, VisibilityKind::PubCrate)
-            | (VisibilityKind::Pub, VisibilityKind::Pub) => true,
-            _ => false,
-        }
-    }
 }
 
 impl ast::LifetimeParam {
diff --git a/docs/user/generated_config.adoc b/docs/user/generated_config.adoc
index c02bab7cc3..b324118875 100644
--- a/docs/user/generated_config.adoc
+++ b/docs/user/generated_config.adoc
@@ -1,12 +1,7 @@
-[[rust-analyzer.assist.importGranularity]]rust-analyzer.assist.importGranularity (default: `"crate"`)::
+[[rust-analyzer.assist.importMergeBehavior]]rust-analyzer.assist.importMergeBehavior (default: `"crate"`)::
 +
 --
-How imports should be grouped into use statements.
---
-[[rust-analyzer.assist.importEnforceGranularity]]rust-analyzer.assist.importEnforceGranularity (default: `false`)::
-+
---
-Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.
+The strategy to use when inserting new imports or merging imports.
 --
 [[rust-analyzer.assist.importPrefix]]rust-analyzer.assist.importPrefix (default: `"plain"`)::
 +
diff --git a/docs/user/manual.adoc b/docs/user/manual.adoc
index 1d8a1930a9..f96c09a79f 100644
--- a/docs/user/manual.adoc
+++ b/docs/user/manual.adoc
@@ -276,7 +276,7 @@ nvim_lsp.rust_analyzer.setup({
     settings = {
         ["rust-analyzer"] = {
             assist = {
-                importGranularity = "module",
+                importMergeBehavior = "last",
                 importPrefix = "by_self",
             },
             cargo = {
diff --git a/editors/code/package.json b/editors/code/package.json
index 1743b374c0..99223c4e8c 100644
--- a/editors/code/package.json
+++ b/editors/code/package.json
@@ -385,28 +385,21 @@
                     "markdownDescription": "Optional settings passed to the debug engine. Example: `{ \"lldb\": { \"terminal\":\"external\"} }`"
                 },
                 "$generated-start": false,
-                "rust-analyzer.assist.importGranularity": {
-                    "markdownDescription": "How imports should be grouped into use statements.",
+                "rust-analyzer.assist.importMergeBehavior": {
+                    "markdownDescription": "The strategy to use when inserting new imports or merging imports.",
                     "default": "crate",
                     "type": "string",
                     "enum": [
-                        "preserve",
+                        "none",
                         "crate",
-                        "module",
-                        "item"
+                        "module"
                     ],
                     "enumDescriptions": [
-                        "Do not change the granularity of any imports and preserve the original structure written by the developer.",
-                        "Merge imports from the same crate into a single use statement. Conversely, imports from different crates are split into separate statements.",
-                        "Merge imports from the same module into a single use statement. Conversely, imports from different modules are split into separate statements.",
-                        "Flatten imports so that each has its own use statement."
+                        "Do not merge imports at all.",
+                        "Merge imports from the same crate into a single `use` statement.",
+                        "Merge imports from the same module into a single `use` statement."
                     ]
                 },
-                "rust-analyzer.assist.importEnforceGranularity": {
-                    "markdownDescription": "Whether to enforce the import granularity setting for all files. If set to false rust-analyzer will try to keep import styles consistent per file.",
-                    "default": false,
-                    "type": "boolean"
-                },
                 "rust-analyzer.assist.importPrefix": {
                     "markdownDescription": "The path structure for newly inserted paths to use.",
                     "default": "plain",


Test patch:
diff --git a/crates/hir_def/src/nameres/tests/macros.rs b/crates/hir_def/src/nameres/tests/macros.rs
index 6eb5f97be1..04de107f5b 100644
--- a/crates/hir_def/src/nameres/tests/macros.rs
+++ b/crates/hir_def/src/nameres/tests/macros.rs
@@ -735,6 +735,28 @@ fn unresolved_attributes_fall_back_track_per_file_moditems() {
     );
 }
 
+#[test]
+fn resolves_derive_helper() {
+    cov_mark::check!(resolved_derive_helper);
+    check(
+        r#"
+//- /main.rs crate:main deps:proc
+#[derive(proc::Derive)]
+#[helper]
+#[unresolved]
+struct S;
+
+//- /proc.rs crate:proc
+#[proc_macro_derive(Derive, attributes(helper))]
+fn derive() {}
+        "#,
+        expect![[r#"
+            crate
+            S: t v
+        "#]],
+    )
+}
+
 #[test]
 fn macro_expansion_overflow() {
     cov_mark::check!(macro_expansion_overflow);
diff --git a/crates/ide_assists/src/tests.rs b/crates/ide_assists/src/tests.rs
index 1739302bf0..0d3969c36d 100644
--- a/crates/ide_assists/src/tests.rs
+++ b/crates/ide_assists/src/tests.rs
@@ -4,10 +4,7 @@ use expect_test::expect;
 use hir::Semantics;
 use ide_db::{
     base_db::{fixture::WithFixture, FileId, FileRange, SourceDatabaseExt},
-    helpers::{
-        insert_use::{ImportGranularity, InsertUseConfig},
-        SnippetCap,
-    },
+    helpers::{insert_use::InsertUseConfig, merge_imports::MergeBehavior, SnippetCap},
     source_change::FileSystemEdit,
     RootDatabase,
 };
@@ -24,9 +21,8 @@ pub(crate) const TEST_CONFIG: AssistConfig = AssistConfig {
     snippet_cap: SnippetCap::new(true),
     allowed: None,
     insert_use: InsertUseConfig {
-        granularity: ImportGranularity::Crate,
+        merge: Some(MergeBehavior::Crate),
         prefix_kind: hir::PrefixKind::Plain,
-        enforce_granularity: true,
         group: true,
     },
 };
diff --git a/crates/ide_completion/src/test_utils.rs b/crates/ide_completion/src/test_utils.rs
index 37be575e55..939fb2d470 100644
--- a/crates/ide_completion/src/test_utils.rs
+++ b/crates/ide_completion/src/test_utils.rs
@@ -3,10 +3,7 @@
 use hir::{PrefixKind, Semantics};
 use ide_db::{
     base_db::{fixture::ChangeFixture, FileLoader, FilePosition},
-    helpers::{
-        insert_use::{ImportGranularity, InsertUseConfig},
-        SnippetCap,
-    },
+    helpers::{insert_use::InsertUseConfig, merge_imports::MergeBehavior, SnippetCap},
     RootDatabase,
 };
 use itertools::Itertools;
@@ -23,9 +20,8 @@ pub(crate) const TEST_CONFIG: CompletionConfig = CompletionConfig {
     add_call_argument_snippets: true,
     snippet_cap: SnippetCap::new(true),
     insert_use: InsertUseConfig {
-        granularity: ImportGranularity::Crate,
+        merge: Some(MergeBehavior::Crate),
         prefix_kind: PrefixKind::Plain,
-        enforce_granularity: true,
         group: true,
     },
 };
diff --git a/crates/ide_db/src/helpers/insert_use/tests.rs b/crates/ide_db/src/helpers/insert_use/tests.rs
index 78a2a87b38..248227d295 100644
--- a/crates/ide_db/src/helpers/insert_use/tests.rs
+++ b/crates/ide_db/src/helpers/insert_use/tests.rs
@@ -21,7 +21,7 @@ use crate::bar::A;
 use self::bar::A;
 use super::bar::A;
 use external_crate2::bar::A;",
-        ImportGranularity::Item,
+        None,
         false,
         false,
     );
@@ -36,7 +36,7 @@ fn insert_not_group_empty() {
         r"use external_crate2::bar::A;
 
 ",
-        ImportGranularity::Item,
+        None,
         false,
         false,
     );
@@ -281,7 +281,7 @@ fn insert_empty_module() {
         r"{
     use foo::bar;
 }",
-        ImportGranularity::Item,
+        None,
         true,
         true,
     )
@@ -631,121 +631,11 @@ fn merge_last_fail3() {
     );
 }
 
-#[test]
-fn guess_empty() {
-    check_guess("", ImportGranularityGuess::Unknown);
-}
-
-#[test]
-fn guess_single() {
-    check_guess(r"use foo::{baz::{qux, quux}, bar};", ImportGranularityGuess::Crate);
-    check_guess(r"use foo::bar;", ImportGranularityGuess::Unknown);
-    check_guess(r"use foo::bar::{baz, qux};", ImportGranularityGuess::CrateOrModule);
-}
-
-#[test]
-fn guess_unknown() {
-    check_guess(
-        r"
-use foo::bar::baz;
-use oof::rab::xuq;
-",
-        ImportGranularityGuess::Unknown,
-    );
-}
-
-#[test]
-fn guess_item() {
-    check_guess(
-        r"
-use foo::bar::baz;
-use foo::bar::qux;
-",
-        ImportGranularityGuess::Item,
-    );
-}
-
-#[test]
-fn guess_module() {
-    check_guess(
-        r"
-use foo::bar::baz;
-use foo::bar::{qux, quux};
-",
-        ImportGranularityGuess::Module,
-    );
-    // this is a rather odd case, technically this file isn't following any style properly.
-    check_guess(
-        r"
-use foo::bar::baz;
-use foo::{baz::{qux, quux}, bar};
-",
-        ImportGranularityGuess::Module,
-    );
-}
-
-#[test]
-fn guess_crate_or_module() {
-    check_guess(
-        r"
-use foo::bar::baz;
-use oof::bar::{qux, quux};
-",
-        ImportGranularityGuess::CrateOrModule,
-    );
-}
-
-#[test]
-fn guess_crate() {
-    check_guess(
-        r"
-use frob::bar::baz;
-use foo::{baz::{qux, quux}, bar};
-",
-        ImportGranularityGuess::Crate,
-    );
-}
-
-#[test]
-fn guess_skips_differing_vis() {
-    check_guess(
-        r"
-use foo::bar::baz;
-pub use foo::bar::qux;
-",
-        ImportGranularityGuess::Unknown,
-    );
-}
-
-#[test]
-fn guess_skips_differing_attrs() {
-    check_guess(
-        r"
-pub use foo::bar::baz;
-#[doc(hidden)]
-pub use foo::bar::qux;
-",
-        ImportGranularityGuess::Unknown,
-    );
-}
-
-#[test]
-fn guess_grouping_matters() {
-    check_guess(
-        r"
-use foo::bar::baz;
-use oof::bar::baz;
-use foo::bar::qux;
-",
-        ImportGranularityGuess::Unknown,
-    );
-}
-
 fn check(
     path: &str,
     ra_fixture_before: &str,
     ra_fixture_after: &str,
-    granularity: ImportGranularity,
+    mb: Option<MergeBehavior>,
     module: bool,
     group: bool,
 ) {
@@ -761,30 +651,21 @@ fn check(
         .find_map(ast::Path::cast)
         .unwrap();
 
-    insert_use(
-        &file,
-        path,
-        InsertUseConfig {
-            granularity,
-            enforce_granularity: true,
-            prefix_kind: PrefixKind::Plain,
-            group,
-        },
-    );
+    insert_use(&file, path, InsertUseConfig { merge: mb, prefix_kind: PrefixKind::Plain, group });
     let result = file.as_syntax_node().to_string();
     assert_eq_text!(ra_fixture_after, &result);
 }
 
 fn check_crate(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
-    check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Crate, false, true)
+    check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Crate), false, true)
 }
 
 fn check_module(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
-    check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Module, false, true)
+    check(path, ra_fixture_before, ra_fixture_after, Some(MergeBehavior::Module), false, true)
 }
 
 fn check_none(path: &str, ra_fixture_before: &str, ra_fixture_after: &str) {
-    check(path, ra_fixture_before, ra_fixture_after, ImportGranularity::Item, false, true)
+    check(path, ra_fixture_before, ra_fixture_after, None, false, true)
 }
 
 fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior) {
@@ -805,9 +686,3 @@ fn check_merge_only_fail(ra_fixture0: &str, ra_fixture1: &str, mb: MergeBehavior
     let result = try_merge_imports(&use0, &use1, mb);
     assert_eq!(result.map(|u| u.to_string()), None);
 }
-
-fn check_guess(ra_fixture: &str, expected: ImportGranularityGuess) {
-    let syntax = ast::SourceFile::parse(ra_fixture).tree().syntax().clone();
-    let file = super::ImportScope::from(syntax).unwrap();
-    assert_eq!(file.guess_granularity_from_scope(), expected);
-}