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 @@ pub fn is_unsafe(self, db: &dyn HirDatabase) -> bool { 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::()?; + 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, 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,23 +168,23 @@ 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}; 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( 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 @@ pub(crate) fn all() -> &'static [Handler] { 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 @@ -1431,6 +1431,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( 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, + 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)) }