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 // ```
promote_local_to_const(acc: &mut Assists, ctx: &AssistContext) -> Option<()>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 
is_body_const(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> bool98 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]
simple()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]
not_applicable_non_const_meth_call()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]
not_applicable_non_const_call()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]
not_applicable_unknown_ty()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]
not_applicable_non_simple_ident()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 }
222