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},
7};
8
9use crate::{
10 assist_context::{AssistContext, Assists},
11 utils,
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 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}