ide_assists/handlers/
promote_local_to_const.rs1use 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
14pub(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}