about summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2021-10-14 19:51:34 +0000
committerGitHub <noreply@github.com>2021-10-14 19:51:34 +0000
commit0af9d1fc8aaf07bdcd7ab1e7ccad757b69e5c76f (patch)
treeee121eaa18b33adf73a1b704fd1116367fa4a022
parente52d47a3b8211c46de9321f76a59ce5e9811a8f8 (diff)
parent06286ee90b24dcd999513fac86fd1db760f85b15 (diff)
downloadrust-0af9d1fc8aaf07bdcd7ab1e7ccad757b69e5c76f.tar.gz
rust-0af9d1fc8aaf07bdcd7ab1e7ccad757b69e5c76f.zip
Merge #10546
10546: feat: Implement promote_local_to_const assist r=Veykril a=Veykril

Fixes #7692, that is now one can invoke the `extract_variable` assist on something and then follow that up with this assist to turn it into a const.
bors r+

Co-authored-by: Lukas Wirth <lukastw97@gmail.com>
-rw-r--r--crates/hir/src/lib.rs4
-rw-r--r--crates/ide_assists/src/handlers/promote_local_to_const.rs221
-rw-r--r--crates/ide_assists/src/handlers/raw_string.rs22
-rw-r--r--crates/ide_assists/src/lib.rs2
-rw-r--r--crates/ide_assists/src/tests/generated.rs29
-rw-r--r--crates/syntax/src/ast/make.rs13
6 files changed, 280 insertions, 11 deletions
diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs
index de59cb96130..d79e93406c3 100644
--- a/crates/hir/src/lib.rs
+++ b/crates/hir/src/lib.rs
@@ -1307,6 +1307,10 @@ impl Function {
         db.function_data(self.id).is_unsafe()
     }
 
+    pub fn is_const(self, db: &dyn HirDatabase) -> bool {
+        db.function_data(self.id).is_const()
+    }
+
     pub fn is_async(self, db: &dyn HirDatabase) -> bool {
         db.function_data(self.id).is_async()
     }
diff --git a/crates/ide_assists/src/handlers/promote_local_to_const.rs b/crates/ide_assists/src/handlers/promote_local_to_const.rs
new file mode 100644
index 00000000000..879247d37ee
--- /dev/null
+++ b/crates/ide_assists/src/handlers/promote_local_to_const.rs
@@ -0,0 +1,221 @@
+use hir::{HirDisplay, ModuleDef, PathResolution, Semantics};
+use ide_db::{
+    assists::{AssistId, AssistKind},
+    defs::Definition,
+    helpers::node_ext::preorder_expr,
+    RootDatabase,
+};
+use stdx::to_upper_snake_case;
+use syntax::{
+    ast::{self, make, HasName},
+    AstNode, WalkEvent,
+};
+
+use crate::{
+    assist_context::{AssistContext, Assists},
+    utils::{render_snippet, Cursor},
+};
+
+// Assist: promote_local_to_const
+//
+// Promotes a local variable to a const item changing its name to a `SCREAMING_SNAKE_CASE` variant
+// if the local uses no non-const expressions.
+//
+// ```
+// fn main() {
+//     let foo$0 = true;
+//
+//     if foo {
+//         println!("It's true");
+//     } else {
+//         println!("It's false");
+//     }
+// }
+// ```
+// ->
+// ```
+// fn main() {
+//     const $0FOO: bool = true;
+//
+//     if FOO {
+//         println!("It's true");
+//     } else {
+//         println!("It's false");
+//     }
+// }
+// ```
+pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
+    let pat = ctx.find_node_at_offset::<ast::IdentPat>()?;
+    let name = pat.name()?;
+    if !pat.is_simple_ident() {
+        cov_mark::hit!(promote_local_non_simple_ident);
+        return None;
+    }
+    let let_stmt = pat.syntax().parent().and_then(ast::LetStmt::cast)?;
+
+    let module = ctx.sema.scope(pat.syntax()).module()?;
+    let local = ctx.sema.to_def(&pat)?;
+    let ty = ctx.sema.type_of_pat(&pat.into())?.original;
+
+    if ty.contains_unknown() || ty.is_closure() {
+        cov_mark::hit!(promote_lcoal_not_applicable_if_ty_not_inferred);
+        return None;
+    }
+    let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
+
+    let initializer = let_stmt.initializer()?;
+    if !is_body_const(&ctx.sema, &initializer) {
+        cov_mark::hit!(promote_local_non_const);
+        return None;
+    }
+    let target = let_stmt.syntax().text_range();
+    acc.add(
+        AssistId("promote_local_to_const", AssistKind::Refactor),
+        "Promote local to constant",
+        target,
+        |builder| {
+            let name = to_upper_snake_case(&name.to_string());
+            let usages = Definition::Local(local).usages(&ctx.sema).all();
+            if let Some(usages) = usages.references.get(&ctx.file_id()) {
+                for usage in usages {
+                    builder.replace(usage.range, &name);
+                }
+            }
+
+            let item = make::item_const(None, make::name(&name), make::ty(&ty), initializer);
+            match ctx.config.snippet_cap.zip(item.name()) {
+                Some((cap, name)) => builder.replace_snippet(
+                    cap,
+                    target,
+                    render_snippet(cap, item.syntax(), Cursor::Before(name.syntax())),
+                ),
+                None => builder.replace(target, item.to_string()),
+            }
+        },
+    )
+}
+
+fn is_body_const(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> bool {
+    let mut is_const = true;
+    preorder_expr(expr, &mut |ev| {
+        let expr = match ev {
+            WalkEvent::Enter(_) if !is_const => return true,
+            WalkEvent::Enter(expr) => expr,
+            WalkEvent::Leave(_) => return false,
+        };
+        match expr {
+            ast::Expr::CallExpr(call) => {
+                if let Some(ast::Expr::PathExpr(path_expr)) = call.expr() {
+                    if let Some(PathResolution::Def(ModuleDef::Function(func))) =
+                        path_expr.path().and_then(|path| sema.resolve_path(&path))
+                    {
+                        is_const &= func.is_const(sema.db);
+                    }
+                }
+            }
+            ast::Expr::MethodCallExpr(call) => {
+                is_const &=
+                    sema.resolve_method_call(&call).map(|it| it.is_const(sema.db)).unwrap_or(true)
+            }
+            ast::Expr::BoxExpr(_)
+            | ast::Expr::ForExpr(_)
+            | ast::Expr::ReturnExpr(_)
+            | ast::Expr::TryExpr(_)
+            | ast::Expr::YieldExpr(_)
+            | ast::Expr::AwaitExpr(_) => is_const = false,
+            _ => (),
+        }
+        !is_const
+    });
+    is_const
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::tests::{check_assist, check_assist_not_applicable};
+
+    use super::*;
+
+    #[test]
+    fn simple() {
+        check_assist(
+            promote_local_to_const,
+            r"
+fn foo() {
+    let x$0 = 0;
+    let y = x;
+}
+",
+            r"
+fn foo() {
+    const $0X: i32 = 0;
+    let y = X;
+}
+",
+        );
+    }
+
+    #[test]
+    fn not_applicable_non_const_meth_call() {
+        cov_mark::check!(promote_local_non_const);
+        check_assist_not_applicable(
+            promote_local_to_const,
+            r"
+struct Foo;
+impl Foo {
+    fn foo(self) {}
+}
+fn foo() {
+    let x$0 = Foo.foo();
+}
+",
+        );
+    }
+
+    #[test]
+    fn not_applicable_non_const_call() {
+        check_assist_not_applicable(
+            promote_local_to_const,
+            r"
+fn bar(self) {}
+fn foo() {
+    let x$0 = bar();
+}
+",
+        );
+    }
+
+    #[test]
+    fn not_applicable_unknown_ty() {
+        cov_mark::check!(promote_lcoal_not_applicable_if_ty_not_inferred);
+        check_assist_not_applicable(
+            promote_local_to_const,
+            r"
+fn foo() {
+    let x$0 = bar();
+}
+",
+        );
+    }
+
+    #[test]
+    fn not_applicable_non_simple_ident() {
+        cov_mark::check!(promote_local_non_simple_ident);
+        check_assist_not_applicable(
+            promote_local_to_const,
+            r"
+fn foo() {
+    let ref x$0 = ();
+}
+",
+        );
+        check_assist_not_applicable(
+            promote_local_to_const,
+            r"
+fn foo() {
+    let mut x$0 = ();
+}
+",
+        );
+    }
+}
diff --git a/crates/ide_assists/src/handlers/raw_string.rs b/crates/ide_assists/src/handlers/raw_string.rs
index acd0829570c..41f768c3175 100644
--- a/crates/ide_assists/src/handlers/raw_string.rs
+++ b/crates/ide_assists/src/handlers/raw_string.rs
@@ -168,17 +168,6 @@ fn required_hashes(s: &str) -> usize {
     res
 }
 
-#[test]
-fn test_required_hashes() {
-    assert_eq!(0, required_hashes("abc"));
-    assert_eq!(0, required_hashes("###"));
-    assert_eq!(1, required_hashes("\""));
-    assert_eq!(2, required_hashes("\"#abc"));
-    assert_eq!(0, required_hashes("#abc"));
-    assert_eq!(3, required_hashes("#ab\"##c"));
-    assert_eq!(5, required_hashes("#ab\"##\"####c"));
-}
-
 #[cfg(test)]
 mod tests {
     use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
@@ -186,6 +175,17 @@ mod tests {
     use super::*;
 
     #[test]
+    fn test_required_hashes() {
+        assert_eq!(0, required_hashes("abc"));
+        assert_eq!(0, required_hashes("###"));
+        assert_eq!(1, required_hashes("\""));
+        assert_eq!(2, required_hashes("\"#abc"));
+        assert_eq!(0, required_hashes("#abc"));
+        assert_eq!(3, required_hashes("#ab\"##c"));
+        assert_eq!(5, required_hashes("#ab\"##\"####c"));
+    }
+
+    #[test]
     fn make_raw_string_target() {
         check_assist_target(
             make_raw_string,
diff --git a/crates/ide_assists/src/lib.rs b/crates/ide_assists/src/lib.rs
index bd543ff3e47..ea2c19b5087 100644
--- a/crates/ide_assists/src/lib.rs
+++ b/crates/ide_assists/src/lib.rs
@@ -157,6 +157,7 @@ mod handlers {
     mod move_module_to_file;
     mod move_to_mod_rs;
     mod move_from_mod_rs;
+    mod promote_local_to_const;
     mod pull_assignment_up;
     mod qualify_path;
     mod raw_string;
@@ -237,6 +238,7 @@ mod handlers {
             move_to_mod_rs::move_to_mod_rs,
             move_from_mod_rs::move_from_mod_rs,
             pull_assignment_up::pull_assignment_up,
+            promote_local_to_const::promote_local_to_const,
             qualify_path::qualify_path,
             raw_string::add_hash,
             raw_string::make_usual_string,
diff --git a/crates/ide_assists/src/tests/generated.rs b/crates/ide_assists/src/tests/generated.rs
index fba7736633a..25acd534824 100644
--- a/crates/ide_assists/src/tests/generated.rs
+++ b/crates/ide_assists/src/tests/generated.rs
@@ -1432,6 +1432,35 @@ fn t() {}
 }
 
 #[test]
+fn doctest_promote_local_to_const() {
+    check_doc_test(
+        "promote_local_to_const",
+        r#####"
+fn main() {
+    let foo$0 = true;
+
+    if foo {
+        println!("It's true");
+    } else {
+        println!("It's false");
+    }
+}
+"#####,
+        r#####"
+fn main() {
+    const $0FOO: bool = true;
+
+    if FOO {
+        println!("It's true");
+    } else {
+        println!("It's false");
+    }
+}
+"#####,
+    )
+}
+
+#[test]
 fn doctest_pull_assignment_up() {
     check_doc_test(
         "pull_assignment_up",
diff --git a/crates/syntax/src/ast/make.rs b/crates/syntax/src/ast/make.rs
index e67ac69073e..14faf9182dc 100644
--- a/crates/syntax/src/ast/make.rs
+++ b/crates/syntax/src/ast/make.rs
@@ -580,6 +580,19 @@ pub fn expr_stmt(expr: ast::Expr) -> ast::ExprStmt {
     ast_from_text(&format!("fn f() {{ {}{} (); }}", expr, semi))
 }
 
+pub fn item_const(
+    visibility: Option<ast::Visibility>,
+    name: ast::Name,
+    ty: ast::Type,
+    expr: ast::Expr,
+) -> ast::Const {
+    let visibility = match visibility {
+        None => String::new(),
+        Some(it) => format!("{} ", it),
+    };
+    ast_from_text(&format!("{} const {}: {} = {};", visibility, name, ty, expr))
+}
+
 pub fn param(pat: ast::Pat, ty: ast::Type) -> ast::Param {
     ast_from_text(&format!("fn f({}: {}) {{ }}", pat, ty))
 }