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