Skip to content

Commit 0af9d1f

Browse files
bors[bot]Veykril
andauthored
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 <[email protected]>
2 parents e52d47a + 06286ee commit 0af9d1f

File tree

6 files changed

+280
-11
lines changed

6 files changed

+280
-11
lines changed

crates/hir/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,10 @@ impl Function {
13071307
db.function_data(self.id).is_unsafe()
13081308
}
13091309

1310+
pub fn is_const(self, db: &dyn HirDatabase) -> bool {
1311+
db.function_data(self.id).is_const()
1312+
}
1313+
13101314
pub fn is_async(self, db: &dyn HirDatabase) -> bool {
13111315
db.function_data(self.id).is_async()
13121316
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
use hir::{HirDisplay, ModuleDef, PathResolution, Semantics};
2+
use ide_db::{
3+
assists::{AssistId, AssistKind},
4+
defs::Definition,
5+
helpers::node_ext::preorder_expr,
6+
RootDatabase,
7+
};
8+
use stdx::to_upper_snake_case;
9+
use syntax::{
10+
ast::{self, make, HasName},
11+
AstNode, WalkEvent,
12+
};
13+
14+
use crate::{
15+
assist_context::{AssistContext, Assists},
16+
utils::{render_snippet, Cursor},
17+
};
18+
19+
// Assist: promote_local_to_const
20+
//
21+
// Promotes a local variable to a const item changing its name to a `SCREAMING_SNAKE_CASE` variant
22+
// if the local uses no non-const expressions.
23+
//
24+
// ```
25+
// fn main() {
26+
// let foo$0 = true;
27+
//
28+
// if foo {
29+
// println!("It's true");
30+
// } else {
31+
// println!("It's false");
32+
// }
33+
// }
34+
// ```
35+
// ->
36+
// ```
37+
// fn main() {
38+
// const $0FOO: bool = true;
39+
//
40+
// if FOO {
41+
// println!("It's true");
42+
// } else {
43+
// println!("It's false");
44+
// }
45+
// }
46+
// ```
47+
pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext) -> Option<()> {
48+
let pat = ctx.find_node_at_offset::<ast::IdentPat>()?;
49+
let name = pat.name()?;
50+
if !pat.is_simple_ident() {
51+
cov_mark::hit!(promote_local_non_simple_ident);
52+
return None;
53+
}
54+
let let_stmt = pat.syntax().parent().and_then(ast::LetStmt::cast)?;
55+
56+
let module = ctx.sema.scope(pat.syntax()).module()?;
57+
let local = ctx.sema.to_def(&pat)?;
58+
let ty = ctx.sema.type_of_pat(&pat.into())?.original;
59+
60+
if ty.contains_unknown() || ty.is_closure() {
61+
cov_mark::hit!(promote_lcoal_not_applicable_if_ty_not_inferred);
62+
return None;
63+
}
64+
let ty = ty.display_source_code(ctx.db(), module.into()).ok()?;
65+
66+
let initializer = let_stmt.initializer()?;
67+
if !is_body_const(&ctx.sema, &initializer) {
68+
cov_mark::hit!(promote_local_non_const);
69+
return None;
70+
}
71+
let target = let_stmt.syntax().text_range();
72+
acc.add(
73+
AssistId("promote_local_to_const", AssistKind::Refactor),
74+
"Promote local to constant",
75+
target,
76+
|builder| {
77+
let name = to_upper_snake_case(&name.to_string());
78+
let usages = Definition::Local(local).usages(&ctx.sema).all();
79+
if let Some(usages) = usages.references.get(&ctx.file_id()) {
80+
for usage in usages {
81+
builder.replace(usage.range, &name);
82+
}
83+
}
84+
85+
let item = make::item_const(None, make::name(&name), make::ty(&ty), initializer);
86+
match ctx.config.snippet_cap.zip(item.name()) {
87+
Some((cap, name)) => builder.replace_snippet(
88+
cap,
89+
target,
90+
render_snippet(cap, item.syntax(), Cursor::Before(name.syntax())),
91+
),
92+
None => builder.replace(target, item.to_string()),
93+
}
94+
},
95+
)
96+
}
97+
98+
fn is_body_const(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> bool {
99+
let mut is_const = true;
100+
preorder_expr(expr, &mut |ev| {
101+
let expr = match ev {
102+
WalkEvent::Enter(_) if !is_const => return true,
103+
WalkEvent::Enter(expr) => expr,
104+
WalkEvent::Leave(_) => return false,
105+
};
106+
match expr {
107+
ast::Expr::CallExpr(call) => {
108+
if let Some(ast::Expr::PathExpr(path_expr)) = call.expr() {
109+
if let Some(PathResolution::Def(ModuleDef::Function(func))) =
110+
path_expr.path().and_then(|path| sema.resolve_path(&path))
111+
{
112+
is_const &= func.is_const(sema.db);
113+
}
114+
}
115+
}
116+
ast::Expr::MethodCallExpr(call) => {
117+
is_const &=
118+
sema.resolve_method_call(&call).map(|it| it.is_const(sema.db)).unwrap_or(true)
119+
}
120+
ast::Expr::BoxExpr(_)
121+
| ast::Expr::ForExpr(_)
122+
| ast::Expr::ReturnExpr(_)
123+
| ast::Expr::TryExpr(_)
124+
| ast::Expr::YieldExpr(_)
125+
| ast::Expr::AwaitExpr(_) => is_const = false,
126+
_ => (),
127+
}
128+
!is_const
129+
});
130+
is_const
131+
}
132+
133+
#[cfg(test)]
134+
mod tests {
135+
use crate::tests::{check_assist, check_assist_not_applicable};
136+
137+
use super::*;
138+
139+
#[test]
140+
fn simple() {
141+
check_assist(
142+
promote_local_to_const,
143+
r"
144+
fn foo() {
145+
let x$0 = 0;
146+
let y = x;
147+
}
148+
",
149+
r"
150+
fn foo() {
151+
const $0X: i32 = 0;
152+
let y = X;
153+
}
154+
",
155+
);
156+
}
157+
158+
#[test]
159+
fn not_applicable_non_const_meth_call() {
160+
cov_mark::check!(promote_local_non_const);
161+
check_assist_not_applicable(
162+
promote_local_to_const,
163+
r"
164+
struct Foo;
165+
impl Foo {
166+
fn foo(self) {}
167+
}
168+
fn foo() {
169+
let x$0 = Foo.foo();
170+
}
171+
",
172+
);
173+
}
174+
175+
#[test]
176+
fn not_applicable_non_const_call() {
177+
check_assist_not_applicable(
178+
promote_local_to_const,
179+
r"
180+
fn bar(self) {}
181+
fn foo() {
182+
let x$0 = bar();
183+
}
184+
",
185+
);
186+
}
187+
188+
#[test]
189+
fn not_applicable_unknown_ty() {
190+
cov_mark::check!(promote_lcoal_not_applicable_if_ty_not_inferred);
191+
check_assist_not_applicable(
192+
promote_local_to_const,
193+
r"
194+
fn foo() {
195+
let x$0 = bar();
196+
}
197+
",
198+
);
199+
}
200+
201+
#[test]
202+
fn not_applicable_non_simple_ident() {
203+
cov_mark::check!(promote_local_non_simple_ident);
204+
check_assist_not_applicable(
205+
promote_local_to_const,
206+
r"
207+
fn foo() {
208+
let ref x$0 = ();
209+
}
210+
",
211+
);
212+
check_assist_not_applicable(
213+
promote_local_to_const,
214+
r"
215+
fn foo() {
216+
let mut x$0 = ();
217+
}
218+
",
219+
);
220+
}
221+
}

crates/ide_assists/src/handlers/raw_string.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -168,23 +168,23 @@ fn required_hashes(s: &str) -> usize {
168168
res
169169
}
170170

171-
#[test]
172-
fn test_required_hashes() {
173-
assert_eq!(0, required_hashes("abc"));
174-
assert_eq!(0, required_hashes("###"));
175-
assert_eq!(1, required_hashes("\""));
176-
assert_eq!(2, required_hashes("\"#abc"));
177-
assert_eq!(0, required_hashes("#abc"));
178-
assert_eq!(3, required_hashes("#ab\"##c"));
179-
assert_eq!(5, required_hashes("#ab\"##\"####c"));
180-
}
181-
182171
#[cfg(test)]
183172
mod tests {
184173
use crate::tests::{check_assist, check_assist_not_applicable, check_assist_target};
185174

186175
use super::*;
187176

177+
#[test]
178+
fn test_required_hashes() {
179+
assert_eq!(0, required_hashes("abc"));
180+
assert_eq!(0, required_hashes("###"));
181+
assert_eq!(1, required_hashes("\""));
182+
assert_eq!(2, required_hashes("\"#abc"));
183+
assert_eq!(0, required_hashes("#abc"));
184+
assert_eq!(3, required_hashes("#ab\"##c"));
185+
assert_eq!(5, required_hashes("#ab\"##\"####c"));
186+
}
187+
188188
#[test]
189189
fn make_raw_string_target() {
190190
check_assist_target(

crates/ide_assists/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ mod handlers {
157157
mod move_module_to_file;
158158
mod move_to_mod_rs;
159159
mod move_from_mod_rs;
160+
mod promote_local_to_const;
160161
mod pull_assignment_up;
161162
mod qualify_path;
162163
mod raw_string;
@@ -237,6 +238,7 @@ mod handlers {
237238
move_to_mod_rs::move_to_mod_rs,
238239
move_from_mod_rs::move_from_mod_rs,
239240
pull_assignment_up::pull_assignment_up,
241+
promote_local_to_const::promote_local_to_const,
240242
qualify_path::qualify_path,
241243
raw_string::add_hash,
242244
raw_string::make_usual_string,

crates/ide_assists/src/tests/generated.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1431,6 +1431,35 @@ fn t() {}
14311431
)
14321432
}
14331433

1434+
#[test]
1435+
fn doctest_promote_local_to_const() {
1436+
check_doc_test(
1437+
"promote_local_to_const",
1438+
r#####"
1439+
fn main() {
1440+
let foo$0 = true;
1441+
1442+
if foo {
1443+
println!("It's true");
1444+
} else {
1445+
println!("It's false");
1446+
}
1447+
}
1448+
"#####,
1449+
r#####"
1450+
fn main() {
1451+
const $0FOO: bool = true;
1452+
1453+
if FOO {
1454+
println!("It's true");
1455+
} else {
1456+
println!("It's false");
1457+
}
1458+
}
1459+
"#####,
1460+
)
1461+
}
1462+
14341463
#[test]
14351464
fn doctest_pull_assignment_up() {
14361465
check_doc_test(

crates/syntax/src/ast/make.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,19 @@ pub fn expr_stmt(expr: ast::Expr) -> ast::ExprStmt {
580580
ast_from_text(&format!("fn f() {{ {}{} (); }}", expr, semi))
581581
}
582582

583+
pub fn item_const(
584+
visibility: Option<ast::Visibility>,
585+
name: ast::Name,
586+
ty: ast::Type,
587+
expr: ast::Expr,
588+
) -> ast::Const {
589+
let visibility = match visibility {
590+
None => String::new(),
591+
Some(it) => format!("{} ", it),
592+
};
593+
ast_from_text(&format!("{} const {}: {} = {};", visibility, name, ty, expr))
594+
}
595+
583596
pub fn param(pat: ast::Pat, ty: ast::Type) -> ast::Param {
584597
ast_from_text(&format!("fn f({}: {}) {{ }}", pat, ty))
585598
}

0 commit comments

Comments
 (0)