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>
This commit is contained in:
commit
0af9d1fc8a
@ -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()
|
||||
}
|
||||
|
221
crates/ide_assists/src/handlers/promote_local_to_const.rs
Normal file
221
crates/ide_assists/src/handlers/promote_local_to_const.rs
Normal file
@ -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 = ();
|
||||
}
|
||||
",
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user