Skip to main content

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},
7};
8
9use crate::{
10    assist_context::{AssistContext, Assists},
11    utils,
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 editor = edit.make_editor(let_stmt.syntax());
72            let make = editor.make();
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            edit.add_file_edits(ctx.vfs_file_id(), editor);
101        },
102    )
103}
104
105#[cfg(test)]
106mod tests {
107    use crate::tests::{check_assist, check_assist_not_applicable};
108
109    use super::*;
110
111    #[test]
112    fn simple() {
113        check_assist(
114            promote_local_to_const,
115            r"
116fn foo() {
117    let x$0 = 0;
118    let y = x;
119}
120",
121            r"
122fn foo() {
123    const $0X: i32 = 0;
124    let y = X;
125}
126",
127        );
128    }
129
130    #[test]
131    fn multiple_uses() {
132        check_assist(
133            promote_local_to_const,
134            r"
135fn foo() {
136    let x$0 = 0;
137    let y = x;
138    let z = (x, x, x, x);
139}
140",
141            r"
142fn foo() {
143    const $0X: i32 = 0;
144    let y = X;
145    let z = (X, X, X, X);
146}
147",
148        );
149    }
150
151    #[test]
152    fn usage_in_field_shorthand() {
153        check_assist(
154            promote_local_to_const,
155            r"
156struct Foo {
157    bar: usize,
158}
159
160fn main() {
161    let $0bar = 0;
162    let foo = Foo { bar };
163}
164",
165            r"
166struct Foo {
167    bar: usize,
168}
169
170fn main() {
171    const $0BAR: usize = 0;
172    let foo = Foo { bar: BAR };
173}
174",
175        )
176    }
177
178    #[test]
179    fn usage_in_macro() {
180        check_assist(
181            promote_local_to_const,
182            r"
183macro_rules! identity {
184    ($body:expr) => {
185        $body
186    }
187}
188
189fn baz() -> usize {
190    let $0foo = 2;
191    identity![foo]
192}
193",
194            r"
195macro_rules! identity {
196    ($body:expr) => {
197        $body
198    }
199}
200
201fn baz() -> usize {
202    const $0FOO: usize = 2;
203    identity![FOO]
204}
205",
206        )
207    }
208
209    #[test]
210    fn usage_shorthand_in_macro() {
211        check_assist(
212            promote_local_to_const,
213            r"
214struct Foo {
215    foo: usize,
216}
217
218macro_rules! identity {
219    ($body:expr) => {
220        $body
221    };
222}
223
224fn baz() -> Foo {
225    let $0foo = 2;
226    identity![Foo { foo }]
227}
228",
229            r"
230struct Foo {
231    foo: usize,
232}
233
234macro_rules! identity {
235    ($body:expr) => {
236        $body
237    };
238}
239
240fn baz() -> Foo {
241    const $0FOO: usize = 2;
242    identity![Foo { foo: FOO }]
243}
244",
245        )
246    }
247
248    #[test]
249    fn not_applicable_non_const_meth_call() {
250        cov_mark::check!(promote_local_non_const);
251        check_assist_not_applicable(
252            promote_local_to_const,
253            r"
254struct Foo;
255impl Foo {
256    fn foo(self) {}
257}
258fn foo() {
259    let x$0 = Foo.foo();
260}
261",
262        );
263    }
264
265    #[test]
266    fn not_applicable_non_const_call() {
267        check_assist_not_applicable(
268            promote_local_to_const,
269            r"
270fn bar(self) {}
271fn foo() {
272    let x$0 = bar();
273}
274",
275        );
276    }
277
278    #[test]
279    fn not_applicable_unknown_ty() {
280        check_assist(
281            promote_local_to_const,
282            r"
283fn foo() {
284    let x$0 = bar();
285}
286",
287            r"
288fn foo() {
289    const $0X: _ = bar();
290}
291",
292        );
293    }
294
295    #[test]
296    fn not_applicable_non_simple_ident() {
297        cov_mark::check!(promote_local_non_simple_ident);
298        check_assist_not_applicable(
299            promote_local_to_const,
300            r"
301fn foo() {
302    let ref x$0 = ();
303}
304",
305        );
306        check_assist_not_applicable(
307            promote_local_to_const,
308            r"
309fn foo() {
310    let mut x$0 = ();
311}
312",
313        );
314    }
315}