ide_assists/handlers/
promote_local_to_const.rs

1use hir::HirDisplay;
2use ide_db::{assists::AssistId, defs::Definition};
3use stdx::to_upper_snake_case;
4use syntax::{
5    AstNode,
6    ast::{self, HasName, syntax_factory::SyntaxFactory},
7};
8
9use crate::{
10    assist_context::{AssistContext, Assists},
11    utils::{self},
12};
13
14// Assist: promote_local_to_const
15//
16// Promotes a local variable to a const item changing its name to a `SCREAMING_SNAKE_CASE` variant
17// if the local uses no non-const expressions.
18//
19// ```
20// fn main() {
21//     let foo$0 = true;
22//
23//     if foo {
24//         println!("It's true");
25//     } else {
26//         println!("It's false");
27//     }
28// }
29// ```
30// ->
31// ```
32// fn main() {
33//     const $0FOO: bool = true;
34//
35//     if FOO {
36//         println!("It's true");
37//     } else {
38//         println!("It's false");
39//     }
40// }
41// ```
42pub(crate) fn promote_local_to_const(acc: &mut Assists, ctx: &AssistContext<'_>) -> Option<()> {
43    let pat = ctx.find_node_at_offset::<ast::IdentPat>()?;
44    let name = pat.name()?;
45    if !pat.is_simple_ident() {
46        cov_mark::hit!(promote_local_non_simple_ident);
47        return None;
48    }
49    let let_stmt = pat.syntax().parent().and_then(ast::LetStmt::cast)?;
50
51    let module = ctx.sema.scope(pat.syntax())?.module();
52    let local = ctx.sema.to_def(&pat)?;
53    let ty = ctx.sema.type_of_pat(&pat.into())?.original;
54
55    let ty = match ty.display_source_code(ctx.db(), module.into(), false) {
56        Ok(ty) => ty,
57        Err(_) => return None,
58    };
59
60    let initializer = let_stmt.initializer()?;
61    if !utils::is_body_const(&ctx.sema, &initializer) {
62        cov_mark::hit!(promote_local_non_const);
63        return None;
64    }
65
66    acc.add(
67        AssistId::refactor("promote_local_to_const"),
68        "Promote local to constant",
69        let_stmt.syntax().text_range(),
70        |edit| {
71            let make = SyntaxFactory::with_mappings();
72            let mut editor = edit.make_editor(let_stmt.syntax());
73            let name = to_upper_snake_case(&name.to_string());
74            let usages = Definition::Local(local).usages(&ctx.sema).all();
75            if let Some(usages) = usages.references.get(&ctx.file_id()) {
76                let name_ref = make.name_ref(&name);
77
78                for usage in usages {
79                    let Some(usage_name) = usage.name.as_name_ref().cloned() else { continue };
80                    if let Some(record_field) = ast::RecordExprField::for_name_ref(&usage_name) {
81                        let path = make.ident_path(&name);
82                        let name_expr = make.expr_path(path);
83                        utils::replace_record_field_expr(ctx, edit, record_field, name_expr);
84                    } else {
85                        let usage_range = usage.range;
86                        edit.replace(usage_range, name_ref.syntax().text());
87                    }
88                }
89            }
90
91            let item = make.item_const(None, None, make.name(&name), make.ty(&ty), initializer);
92
93            if let Some((cap, name)) = ctx.config.snippet_cap.zip(item.name()) {
94                let tabstop = edit.make_tabstop_before(cap);
95                editor.add_annotation(name.syntax().clone(), tabstop);
96            }
97
98            editor.replace(let_stmt.syntax(), item.syntax());
99
100            editor.add_mappings(make.finish_with_mappings());
101            edit.add_file_edits(ctx.vfs_file_id(), editor);
102        },
103    )
104}
105
106#[cfg(test)]
107mod tests {
108    use crate::tests::{check_assist, check_assist_not_applicable};
109
110    use super::*;
111
112    #[test]
113    fn simple() {
114        check_assist(
115            promote_local_to_const,
116            r"
117fn foo() {
118    let x$0 = 0;
119    let y = x;
120}
121",
122            r"
123fn foo() {
124    const $0X: i32 = 0;
125    let y = X;
126}
127",
128        );
129    }
130
131    #[test]
132    fn multiple_uses() {
133        check_assist(
134            promote_local_to_const,
135            r"
136fn foo() {
137    let x$0 = 0;
138    let y = x;
139    let z = (x, x, x, x);
140}
141",
142            r"
143fn foo() {
144    const $0X: i32 = 0;
145    let y = X;
146    let z = (X, X, X, X);
147}
148",
149        );
150    }
151
152    #[test]
153    fn usage_in_field_shorthand() {
154        check_assist(
155            promote_local_to_const,
156            r"
157struct Foo {
158    bar: usize,
159}
160
161fn main() {
162    let $0bar = 0;
163    let foo = Foo { bar };
164}
165",
166            r"
167struct Foo {
168    bar: usize,
169}
170
171fn main() {
172    const $0BAR: usize = 0;
173    let foo = Foo { bar: BAR };
174}
175",
176        )
177    }
178
179    #[test]
180    fn usage_in_macro() {
181        check_assist(
182            promote_local_to_const,
183            r"
184macro_rules! identity {
185    ($body:expr) => {
186        $body
187    }
188}
189
190fn baz() -> usize {
191    let $0foo = 2;
192    identity![foo]
193}
194",
195            r"
196macro_rules! identity {
197    ($body:expr) => {
198        $body
199    }
200}
201
202fn baz() -> usize {
203    const $0FOO: usize = 2;
204    identity![FOO]
205}
206",
207        )
208    }
209
210    #[test]
211    fn usage_shorthand_in_macro() {
212        check_assist(
213            promote_local_to_const,
214            r"
215struct Foo {
216    foo: usize,
217}
218
219macro_rules! identity {
220    ($body:expr) => {
221        $body
222    };
223}
224
225fn baz() -> Foo {
226    let $0foo = 2;
227    identity![Foo { foo }]
228}
229",
230            r"
231struct Foo {
232    foo: usize,
233}
234
235macro_rules! identity {
236    ($body:expr) => {
237        $body
238    };
239}
240
241fn baz() -> Foo {
242    const $0FOO: usize = 2;
243    identity![Foo { foo: FOO }]
244}
245",
246        )
247    }
248
249    #[test]
250    fn not_applicable_non_const_meth_call() {
251        cov_mark::check!(promote_local_non_const);
252        check_assist_not_applicable(
253            promote_local_to_const,
254            r"
255struct Foo;
256impl Foo {
257    fn foo(self) {}
258}
259fn foo() {
260    let x$0 = Foo.foo();
261}
262",
263        );
264    }
265
266    #[test]
267    fn not_applicable_non_const_call() {
268        check_assist_not_applicable(
269            promote_local_to_const,
270            r"
271fn bar(self) {}
272fn foo() {
273    let x$0 = bar();
274}
275",
276        );
277    }
278
279    #[test]
280    fn not_applicable_unknown_ty() {
281        check_assist(
282            promote_local_to_const,
283            r"
284fn foo() {
285    let x$0 = bar();
286}
287",
288            r"
289fn foo() {
290    const $0X: _ = bar();
291}
292",
293        );
294    }
295
296    #[test]
297    fn not_applicable_non_simple_ident() {
298        cov_mark::check!(promote_local_non_simple_ident);
299        check_assist_not_applicable(
300            promote_local_to_const,
301            r"
302fn foo() {
303    let ref x$0 = ();
304}
305",
306        );
307        check_assist_not_applicable(
308            promote_local_to_const,
309            r"
310fn foo() {
311    let mut x$0 = ();
312}
313",
314        );
315    }
316}