about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crates/ide-assists/src/handlers/remove_unused_imports.rs739
-rw-r--r--crates/ide-assists/src/lib.rs2
-rw-r--r--crates/ide-assists/src/tests/generated.rs18
-rw-r--r--crates/syntax/src/ast/edit_in_place.rs36
4 files changed, 795 insertions, 0 deletions
diff --git a/crates/ide-assists/src/handlers/remove_unused_imports.rs b/crates/ide-assists/src/handlers/remove_unused_imports.rs
new file mode 100644
index 00000000000..dd4839351fb
--- /dev/null
+++ b/crates/ide-assists/src/handlers/remove_unused_imports.rs
@@ -0,0 +1,739 @@
+use std::collections::{hash_map::Entry, HashMap};
+
+use hir::{InFile, Module, ModuleSource};
+use ide_db::{
+    base_db::FileRange,
+    defs::Definition,
+    search::{FileReference, ReferenceCategory, SearchScope},
+    RootDatabase,
+};
+use syntax::{ast, AstNode};
+use text_edit::TextRange;
+
+use crate::{AssistContext, AssistId, AssistKind, Assists};
+
+// Assist: remove_unused_imports
+//
+// Removes any use statements in the current selection that are unused.
+//
+// ```
+// struct X();
+// mod foo {
+//     use super::X$0;
+// }
+// ```
+// ->
+// ```
+// struct X();
+// mod foo {
+// }
+// ```
+pub(crate) fn remove_unused_imports(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
+    // First, grab the uses that intersect with the current selection.
+    let selected_el = match ctx.covering_element() {
+        syntax::NodeOrToken::Node(n) => n,
+        syntax::NodeOrToken::Token(t) => t.parent()?,
+    };
+
+    // This applies to all uses that are selected, or are ancestors of our selection.
+    let uses_up = selected_el.ancestors().skip(1).filter_map(ast::Use::cast);
+    let uses_down = selected_el
+        .descendants()
+        .filter(|x| x.text_range().intersect(ctx.selection_trimmed()).is_some())
+        .filter_map(ast::Use::cast);
+    let uses = uses_up.chain(uses_down).collect::<Vec<_>>();
+
+    // Maps use nodes to the scope that we should search through to find
+    let mut search_scopes = HashMap::<Module, Vec<SearchScope>>::new();
+
+    // iterator over all unused use trees
+    let mut unused = uses
+        .into_iter()
+        .flat_map(|u| u.syntax().descendants().filter_map(ast::UseTree::cast))
+        .filter(|u| u.use_tree_list().is_none())
+        .filter_map(|u| {
+            // Find any uses trees that are unused
+
+            let use_module = ctx.sema.scope(&u.syntax()).map(|s| s.module())?;
+            let scope = match search_scopes.entry(use_module) {
+                Entry::Occupied(o) => o.into_mut(),
+                Entry::Vacant(v) => v.insert(module_search_scope(ctx.db(), use_module)),
+            };
+
+            // Gets the path associated with this use tree. If there isn't one, then ignore this use tree.
+            let path = if let Some(path) = u.path() {
+                path
+            } else if u.star_token().is_some() {
+                // This case maps to the situation where the * token is braced.
+                // In this case, the parent use tree's path is the one we should use to resolve the glob.
+                match u.syntax().ancestors().skip(1).find_map(ast::UseTree::cast) {
+                    Some(parent_u) if parent_u.path().is_some() => parent_u.path().unwrap(),
+                    _ => return None,
+                }
+            } else {
+                return None;
+            };
+
+            // Get the actual definition associated with this use item.
+            let res = match ctx.sema.resolve_path(&path) {
+                Some(x) => x,
+                None => {
+                    return None;
+                }
+            };
+
+            let def = match res {
+                hir::PathResolution::Def(d) => Definition::from(d),
+                _ => return None,
+            };
+
+            if u.star_token().is_some() {
+                // Check if any of the children of this module are used
+                let def_mod = match def {
+                    Definition::Module(module) => module,
+                    _ => return None,
+                };
+
+                if !def_mod
+                    .scope(ctx.db(), Some(use_module))
+                    .iter()
+                    .filter_map(|(_, x)| match x {
+                        hir::ScopeDef::ModuleDef(d) => Some(Definition::from(*d)),
+                        _ => None,
+                    })
+                    .any(|d| used_once_in_scope(ctx, d, scope))
+                {
+                    return Some(u);
+                }
+            } else if let Definition::Trait(ref t) = def {
+                // If the trait or any item is used.
+                if !std::iter::once(def)
+                    .chain(t.items(ctx.db()).into_iter().map(Definition::from))
+                    .any(|d| used_once_in_scope(ctx, d, scope))
+                {
+                    return Some(u);
+                }
+            } else {
+                if !used_once_in_scope(ctx, def, &scope) {
+                    return Some(u);
+                }
+            }
+
+            None
+        })
+        .peekable();
+
+    // Peek so we terminate early if an unused use is found. Only do the rest of the work if the user selects the assist.
+    if unused.peek().is_some() {
+        acc.add(
+            AssistId("remove_unused_imports", AssistKind::QuickFix),
+            "Remove all the unused imports",
+            selected_el.text_range(),
+            |builder| {
+                let unused: Vec<ast::UseTree> = unused.map(|x| builder.make_mut(x)).collect();
+                for node in unused {
+                    node.remove_recursive();
+                }
+            },
+        )
+    } else {
+        None
+    }
+}
+
+fn used_once_in_scope(ctx: &AssistContext<'_>, def: Definition, scopes: &Vec<SearchScope>) -> bool {
+    let mut found = false;
+
+    for scope in scopes {
+        let mut search_non_import = |_, r: FileReference| {
+            // The import itself is a use; we must skip that.
+            if r.category != Some(ReferenceCategory::Import) {
+                found = true;
+                true
+            } else {
+                false
+            }
+        };
+        def.usages(&ctx.sema).in_scope(scope).search(&mut search_non_import);
+        if found {
+            break;
+        }
+    }
+
+    found
+}
+
+/// Build a search scope spanning the given module but none of its submodules.
+fn module_search_scope(db: &RootDatabase, module: hir::Module) -> Vec<SearchScope> {
+    let (file_id, range) = {
+        let InFile { file_id, value } = module.definition_source(db);
+        if let Some((file_id, call_source)) = file_id.original_call_node(db) {
+            (file_id, Some(call_source.text_range()))
+        } else {
+            (
+                file_id.original_file(db),
+                match value {
+                    ModuleSource::SourceFile(_) => None,
+                    ModuleSource::Module(it) => Some(it.syntax().text_range()),
+                    ModuleSource::BlockExpr(it) => Some(it.syntax().text_range()),
+                },
+            )
+        }
+    };
+
+    fn split_at_subrange(first: TextRange, second: TextRange) -> (TextRange, Option<TextRange>) {
+        let intersect = first.intersect(second);
+        if let Some(intersect) = intersect {
+            let start_range = TextRange::new(first.start(), intersect.start());
+
+            if intersect.end() < first.end() {
+                (start_range, Some(TextRange::new(intersect.end(), first.end())))
+            } else {
+                (start_range, None)
+            }
+        } else {
+            (first, None)
+        }
+    }
+
+    let mut scopes = Vec::new();
+    if let Some(range) = range {
+        let mut ranges = vec![range];
+
+        for child in module.children(db) {
+            let rng = match child.definition_source(db).value {
+                ModuleSource::SourceFile(_) => continue,
+                ModuleSource::Module(it) => it.syntax().text_range(),
+                ModuleSource::BlockExpr(_) => continue,
+            };
+            let mut new_ranges = Vec::new();
+            for old_range in ranges.iter_mut() {
+                let split = split_at_subrange(old_range.clone(), rng);
+                *old_range = split.0;
+                new_ranges.extend(split.1);
+            }
+
+            ranges.append(&mut new_ranges);
+        }
+
+        for range in ranges {
+            scopes.push(SearchScope::file_range(FileRange { file_id, range }));
+        }
+    } else {
+        scopes.push(SearchScope::single_file(file_id));
+    }
+
+    scopes
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    #[test]
+    fn remove_unused() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    $0use super::X;
+    use super::Y;$0
+}
+"#,
+            r#"
+struct X();
+struct Y();
+mod z {
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_unused_is_precise() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod z {
+$0use super::X;$0
+
+fn w() {
+    struct X();
+    let x = X();
+}
+}
+"#,
+            r#"
+struct X();
+mod z {
+
+fn w() {
+    struct X();
+    let x = X();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn trait_name_use_is_use() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+trait Y {
+    fn f();
+}
+
+impl Y for X {
+    fn f() {}
+}
+mod z {
+$0use super::X;
+use super::Y;$0
+
+fn w() {
+    X::f();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn trait_item_use_is_use() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+trait Y {
+    fn f(self);
+}
+
+impl Y for X {
+    fn f(self) {}
+}
+mod z {
+$0use super::X;
+use super::Y;$0
+
+fn w() {
+    let x = X();
+    x.f();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn ranamed_trait_item_use_is_use() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+trait Y {
+    fn f(self);
+}
+
+impl Y for X {
+    fn f(self) {}
+}
+mod z {
+$0use super::X;
+use super::Y as Z;$0
+
+fn w() {
+    let x = X();
+    x.f();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn ranamed_underscore_trait_item_use_is_use() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+trait Y {
+    fn f(self);
+}
+
+impl Y for X {
+    fn f(self) {}
+}
+mod z {
+$0use super::X;
+use super::Y as _;$0
+
+fn w() {
+    let x = X();
+    x.f();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn dont_remove_used() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+$0use super::X;
+use super::Y;$0
+
+fn w() {
+    let x = X();
+    let y = Y();
+}
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_unused_in_braces() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    $0use super::{X, Y};$0
+
+    fn w() {
+        let x = X();
+    }
+}
+"#,
+            r#"
+struct X();
+struct Y();
+mod z {
+    use super::{X};
+
+    fn w() {
+        let x = X();
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_unused_under_cursor() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod z {
+    use super::X$0;
+}
+"#,
+            r#"
+struct X();
+mod z {
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_multi_use_block() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+$0mod y {
+    use super::X;
+}
+mod z {
+    use super::X;
+}$0
+"#,
+            r#"
+struct X();
+mod y {
+}
+mod z {
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_nested() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+        use crate::{X, y::Y}$0;
+        fn f() {
+            let x = X();
+        }
+    }
+}
+"#,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+        use crate::{X};
+        fn f() {
+            let x = X();
+        }
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_nested_first_item() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+        use crate::{X, y::Y}$0;
+        fn f() {
+            let y = Y();
+        }
+    }
+}
+"#,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+        use crate::{y::Y};
+        fn f() {
+            let y = Y();
+        }
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_nested_all_unused() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+        use crate::{X, y::Y}$0;
+    }
+}
+"#,
+            r#"
+struct X();
+mod y {
+    struct Y();
+    mod z {
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_unused_glob() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    use super::*$0;
+}
+"#,
+            r#"
+struct X();
+struct Y();
+mod z {
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn remove_unused_braced_glob() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    use super::{*}$0;
+}
+"#,
+            r#"
+struct X();
+struct Y();
+mod z {
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn dont_remove_used_glob() {
+        check_assist_not_applicable(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    use super::*$0;
+
+    fn f() {
+        let x = X();
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn only_remove_from_selection() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+struct Y();
+mod z {
+    $0use super::X;$0
+    use super::Y;
+}
+mod w {
+    use super::Y;
+}
+"#,
+            r#"
+struct X();
+struct Y();
+mod z {
+    use super::Y;
+}
+mod w {
+    use super::Y;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn test_several_files() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+//- /foo.rs
+pub struct X();
+pub struct Y();
+
+//- /main.rs
+$0use foo::X;
+use foo::Y;
+$0
+mod foo;
+mod z {
+    use crate::foo::X;
+}
+"#,
+            r#"
+
+mod foo;
+mod z {
+    use crate::foo::X;
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn use_in_submodule_doesnt_count() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+struct X();
+mod z {
+    use super::X$0;
+
+    mod w {
+        use crate::X;
+
+        fn f() {
+            let x = X();
+        }
+    }
+}
+"#,
+            r#"
+struct X();
+mod z {
+
+    mod w {
+        use crate::X;
+
+        fn f() {
+            let x = X();
+        }
+    }
+}
+"#,
+        );
+    }
+
+    #[test]
+    fn use_in_submodule_file_doesnt_count() {
+        check_assist(
+            remove_unused_imports,
+            r#"
+//- /z/foo.rs
+use crate::X;
+fn f() {
+    let x = X();
+}
+
+//- /main.rs
+pub struct X();
+
+mod z {
+    use crate::X$0;
+    mod foo;
+}
+"#,
+            r#"
+pub struct X();
+
+mod z {
+    mod foo;
+}
+"#,
+        );
+    }
+}
diff --git a/crates/ide-assists/src/lib.rs b/crates/ide-assists/src/lib.rs
index a82f1f9dd8b..2ebb5ef9b19 100644
--- a/crates/ide-assists/src/lib.rs
+++ b/crates/ide-assists/src/lib.rs
@@ -184,6 +184,7 @@ mod handlers {
     mod raw_string;
     mod remove_dbg;
     mod remove_mut;
+    mod remove_unused_imports;
     mod remove_unused_param;
     mod remove_parentheses;
     mod reorder_fields;
@@ -294,6 +295,7 @@ mod handlers {
             raw_string::make_usual_string,
             raw_string::remove_hash,
             remove_mut::remove_mut,
+            remove_unused_imports::remove_unused_imports,
             remove_unused_param::remove_unused_param,
             remove_parentheses::remove_parentheses,
             reorder_fields::reorder_fields,
diff --git a/crates/ide-assists/src/tests/generated.rs b/crates/ide-assists/src/tests/generated.rs
index 4d47a199b7c..ec3822c3d11 100644
--- a/crates/ide-assists/src/tests/generated.rs
+++ b/crates/ide-assists/src/tests/generated.rs
@@ -2234,6 +2234,24 @@ fn main() {
 }
 
 #[test]
+fn doctest_remove_unused_imports() {
+    check_doc_test(
+        "remove_unused_imports",
+        r#####"
+struct X();
+mod foo {
+    use super::X$0;
+}
+"#####,
+        r#####"
+struct X();
+mod foo {
+}
+"#####,
+    )
+}
+
+#[test]
 fn doctest_remove_unused_param() {
     check_doc_test(
         "remove_unused_param",
diff --git a/crates/syntax/src/ast/edit_in_place.rs b/crates/syntax/src/ast/edit_in_place.rs
index 5919609d91e..96b438f1a0d 100644
--- a/crates/syntax/src/ast/edit_in_place.rs
+++ b/crates/syntax/src/ast/edit_in_place.rs
@@ -358,6 +358,26 @@ impl Removable for ast::UseTree {
 }
 
 impl ast::UseTree {
+    /// Deletes the usetree node represented by the input. Recursively removes parents, including use nodes that become empty.
+    pub fn remove_recursive(self) {
+        let parent = self.syntax().parent();
+
+        self.remove();
+
+        if let Some(u) = parent.clone().and_then(ast::Use::cast) {
+            if u.use_tree().is_none() {
+                u.remove();
+            }
+        } else if let Some(u) = parent.and_then(ast::UseTreeList::cast) {
+            if u.use_trees().next().is_none() {
+                let parent = u.syntax().parent().and_then(ast::UseTree::cast);
+                if let Some(u) = parent {
+                    u.remove_recursive();
+                }
+            }
+        }
+    }
+
     pub fn get_or_create_use_tree_list(&self) -> ast::UseTreeList {
         match self.use_tree_list() {
             Some(it) => it,
@@ -465,6 +485,22 @@ impl Removable for ast::Use {
                 }
             }
         }
+        let prev_ws = self
+            .syntax()
+            .prev_sibling_or_token()
+            .and_then(|it| it.into_token())
+            .and_then(ast::Whitespace::cast);
+        if let Some(prev_ws) = prev_ws {
+            let ws_text = prev_ws.syntax().text();
+            let prev_newline = ws_text.rfind('\n').map(|x| x + 1).unwrap_or(0);
+            let rest = &ws_text[0..prev_newline];
+            if rest.is_empty() {
+                ted::remove(prev_ws.syntax());
+            } else {
+                ted::replace(prev_ws.syntax(), make::tokens::whitespace(rest));
+            }
+        }
+
         ted::remove(self.syntax());
     }
 }