summaryrefslogtreecommitdiffstats
path: root/src/tools/rust-analyzer/crates/ide-assists/src/handlers/promote_local_to_const.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/tools/rust-analyzer/crates/ide-assists/src/handlers/promote_local_to_const.rs')
-rw-r--r--src/tools/rust-analyzer/crates/ide-assists/src/handlers/promote_local_to_const.rs221
1 files changed, 221 insertions, 0 deletions
diff --git a/src/tools/rust-analyzer/crates/ide-assists/src/handlers/promote_local_to_const.rs b/src/tools/rust-analyzer/crates/ide-assists/src/handlers/promote_local_to_const.rs
new file mode 100644
index 000000000..cbbea6c1e
--- /dev/null
+++ b/src/tools/rust-analyzer/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,
+ syntax_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 = ();
+}
+",
+ );
+ }
+}