Skip to main content

ide_assists/handlers/
move_const_to_impl.rs

1use hir::{AsAssocItem, AssocItemContainer, FileRange, HasSource};
2use ide_db::{assists::AssistId, defs::Definition, search::SearchScope};
3use syntax::{
4    SyntaxKind,
5    ast::{
6        self, AstNode,
7        edit::{AstNodeEdit, IndentLevel},
8    },
9};
10
11use crate::assist_context::{AssistContext, Assists};
12
13// NOTE: Code may break if the self type implements a trait that has associated const with the same
14// name, but it's pretty expensive to check that (`hir::Impl::all_for_type()`) and we assume that's
15// pretty rare case.
16
17// Assist: move_const_to_impl
18//
19// Move a local constant item in a method to impl's associated constant. All the references will be
20// qualified with `Self::`.
21//
22// ```
23// struct S;
24// impl S {
25//     fn foo() -> usize {
26//         /// The answer.
27//         const C$0: usize = 42;
28//
29//         C * C
30//     }
31// }
32// ```
33// ->
34// ```
35// struct S;
36// impl S {
37//     /// The answer.
38//     const C: usize = 42;
39//
40//     fn foo() -> usize {
41//         Self::C * Self::C
42//     }
43// }
44// ```
45pub(crate) fn move_const_to_impl(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
46    let db = ctx.db();
47    let const_: ast::Const = ctx.find_node_at_offset()?;
48    // Don't show the assist when the cursor is at the const's body.
49    if let Some(body) = const_.body()
50        && body.syntax().text_range().contains(ctx.offset())
51    {
52        return None;
53    }
54
55    let parent_fn = const_.syntax().ancestors().find_map(ast::Fn::cast)?;
56
57    // NOTE: We can technically provide this assist for default methods in trait definitions, but
58    // it's somewhat complex to handle it correctly when the const's name conflicts with
59    // supertrait's item. We may want to consider implementing it in the future.
60    let AssocItemContainer::Impl(impl_) =
61        ctx.sema.to_def(&parent_fn)?.as_assoc_item(db)?.container(db)
62    else {
63        return None;
64    };
65    if impl_.trait_(db).is_some() {
66        return None;
67    }
68
69    let def = ctx.sema.to_def(&const_)?;
70    let name = def.name(db)?;
71    let items = impl_.source(db)?.value.assoc_item_list()?;
72
73    let ty = impl_.self_ty(db);
74    // If there exists another associated item with the same name, skip the assist.
75    if ty
76        .iterate_assoc_items(db, |assoc| {
77            // Type aliases wouldn't conflict due to different namespaces, but we're only checking
78            // the items in inherent impls, so we assume `assoc` is never type alias for the sake
79            // of brevity (inherent associated types exist in nightly Rust, but it's *very*
80            // unstable and we don't support them either).
81            assoc.name(db).filter(|it| it == &name)
82        })
83        .is_some()
84    {
85        return None;
86    }
87
88    acc.add(
89        AssistId::refactor_rewrite("move_const_to_impl"),
90        "Move const to impl block",
91        const_.syntax().text_range(),
92        |builder| {
93            let usages = Definition::Const(def)
94                .usages(&ctx.sema)
95                .in_scope(&SearchScope::file_range(FileRange {
96                    file_id: ctx.file_id(),
97                    range: parent_fn.syntax().text_range(),
98                }))
99                .all();
100
101            let range_to_delete = match const_.syntax().next_sibling_or_token() {
102                Some(s) if matches!(s.kind(), SyntaxKind::WHITESPACE) => {
103                    // Remove following whitespaces too.
104                    const_.syntax().text_range().cover(s.text_range())
105                }
106                _ => const_.syntax().text_range(),
107            };
108            builder.delete(range_to_delete);
109
110            let usages = usages.iter().flat_map(|(file_id, usages)| {
111                let edition = file_id.edition(ctx.db());
112                usages.iter().map(move |usage| (edition, usage.range))
113            });
114            for (edition, range) in usages {
115                let const_ref = format!("Self::{}", name.display(ctx.db(), edition));
116                builder.replace(range, const_ref);
117            }
118
119            // Heuristically inserting the extracted const after the consecutive existing consts
120            // from the beginning of assoc items. We assume there are no inherent assoc type as
121            // above.
122            let last_const =
123                items.assoc_items().take_while(|it| matches!(it, ast::AssocItem::Const(_))).last();
124            let insert_offset = match &last_const {
125                Some(it) => it.syntax().text_range().end(),
126                None => match items.l_curly_token() {
127                    Some(l_curly) => l_curly.text_range().end(),
128                    // Not sure if this branch is ever reachable, but it wouldn't hurt to have a
129                    // fallback.
130                    None => items.syntax().text_range().start(),
131                },
132            };
133
134            // If the moved const will be the first item of the impl, add a new line after that.
135            //
136            // We're assuming the code is formatted according to Rust's standard style guidelines
137            // (i.e. no empty lines between impl's `{` token and its first assoc item).
138            let fixup = if last_const.is_none() { "\n" } else { "" };
139            let indent = IndentLevel::from_node(parent_fn.syntax());
140
141            let const_ = const_.reset_indent();
142            let const_ = const_.indent(indent);
143            builder.insert(insert_offset, format!("\n{indent}{const_}{fixup}"));
144        },
145    )
146}
147
148#[cfg(test)]
149mod tests {
150    use crate::tests::{check_assist, check_assist_not_applicable};
151
152    use super::*;
153
154    #[test]
155    fn not_applicable_to_top_level_const() {
156        check_assist_not_applicable(
157            move_const_to_impl,
158            r#"
159const C$0: () = ();
160"#,
161        );
162    }
163
164    #[test]
165    fn not_applicable_to_free_fn() {
166        check_assist_not_applicable(
167            move_const_to_impl,
168            r#"
169fn f() {
170    const C$0: () = ();
171}
172"#,
173        );
174    }
175
176    #[test]
177    fn not_applicable_when_at_const_body() {
178        check_assist_not_applicable(
179            move_const_to_impl,
180            r#"
181struct S;
182impl S {
183    fn f() {
184        const C: () = ($0);
185    }
186}
187            "#,
188        );
189    }
190
191    #[test]
192    fn not_applicable_when_inside_const_body_block() {
193        check_assist_not_applicable(
194            move_const_to_impl,
195            r#"
196struct S;
197impl S {
198    fn f() {
199        const C: () = {
200            ($0)
201        };
202    }
203}
204            "#,
205        );
206    }
207
208    #[test]
209    fn not_applicable_to_trait_impl_fn() {
210        check_assist_not_applicable(
211            move_const_to_impl,
212            r#"
213trait Trait {
214    fn f();
215}
216impl Trait for () {
217    fn f() {
218        const C$0: () = ();
219    }
220}
221"#,
222        );
223    }
224
225    #[test]
226    fn not_applicable_to_non_assoc_fn_inside_impl() {
227        check_assist_not_applicable(
228            move_const_to_impl,
229            r#"
230struct S;
231impl S {
232    fn f() {
233        fn g() {
234            const C$0: () = ();
235        }
236    }
237}
238"#,
239        );
240    }
241
242    #[test]
243    fn not_applicable_when_const_with_same_name_exists() {
244        check_assist_not_applicable(
245            move_const_to_impl,
246            r#"
247struct S;
248impl S {
249    const C: usize = 42;
250    fn f() {
251        const C$0: () = ();
252    }
253"#,
254        );
255
256        check_assist_not_applicable(
257            move_const_to_impl,
258            r#"
259struct S;
260impl S {
261    const C: usize = 42;
262}
263impl S {
264    fn f() {
265        const C$0: () = ();
266    }
267"#,
268        );
269    }
270
271    #[test]
272    fn move_const_simple_body() {
273        check_assist(
274            move_const_to_impl,
275            r#"
276struct S;
277impl S {
278    fn f() -> usize {
279        /// doc comment
280        const C$0: usize = 42;
281
282        C * C
283    }
284}
285"#,
286            r#"
287struct S;
288impl S {
289    /// doc comment
290    const C: usize = 42;
291
292    fn f() -> usize {
293        Self::C * Self::C
294    }
295}
296"#,
297        );
298    }
299
300    #[test]
301    fn move_const_simple_body_existing_const() {
302        check_assist(
303            move_const_to_impl,
304            r#"
305struct S;
306impl S {
307    const X: () = ();
308    const Y: () = ();
309
310    fn f() -> usize {
311        /// doc comment
312        const C$0: usize = 42;
313
314        C * C
315    }
316}
317"#,
318            r#"
319struct S;
320impl S {
321    const X: () = ();
322    const Y: () = ();
323    /// doc comment
324    const C: usize = 42;
325
326    fn f() -> usize {
327        Self::C * Self::C
328    }
329}
330"#,
331        );
332    }
333
334    #[test]
335    fn move_const_block_body() {
336        check_assist(
337            move_const_to_impl,
338            r#"
339struct S;
340impl S {
341    fn f() -> usize {
342        /// doc comment
343        const C$0: usize = {
344            let a = 3;
345            let b = 4;
346            a * b
347        };
348
349        C * C
350    }
351}
352"#,
353            r#"
354struct S;
355impl S {
356    /// doc comment
357    const C: usize = {
358        let a = 3;
359        let b = 4;
360        a * b
361    };
362
363    fn f() -> usize {
364        Self::C * Self::C
365    }
366}
367"#,
368        );
369    }
370
371    #[test]
372    fn correct_indent_when_nested() {
373        check_assist(
374            move_const_to_impl,
375            r#"
376fn main() {
377    struct S;
378    impl S {
379        fn f() -> usize {
380            /// doc comment
381            const C$0: usize = 42;
382
383            C * C
384        }
385    }
386}
387"#,
388            r#"
389fn main() {
390    struct S;
391    impl S {
392        /// doc comment
393        const C: usize = 42;
394
395        fn f() -> usize {
396            Self::C * Self::C
397        }
398    }
399}
400"#,
401        )
402    }
403
404    #[test]
405    fn move_const_in_nested_scope_with_same_name_in_other_scope() {
406        check_assist(
407            move_const_to_impl,
408            r#"
409struct S;
410impl S {
411    fn f() -> usize {
412        const C: &str = "outer";
413
414        let n = {
415            /// doc comment
416            const C$0: usize = 42;
417
418            let m = {
419                const C: &str = "inner";
420                C.len()
421            };
422
423            C * m
424        };
425
426        n + C.len()
427    }
428}
429"#,
430            r#"
431struct S;
432impl S {
433    /// doc comment
434    const C: usize = 42;
435
436    fn f() -> usize {
437        const C: &str = "outer";
438
439        let n = {
440            let m = {
441                const C: &str = "inner";
442                C.len()
443            };
444
445            Self::C * m
446        };
447
448        n + C.len()
449    }
450}
451"#,
452        );
453    }
454}