]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/ide-assists/src/handlers/replace_qualified_name_with_use.rs
Auto merge of #103913 - Neutron3529:patch-1, r=thomcc
[rust.git] / src / tools / rust-analyzer / crates / ide-assists / src / handlers / replace_qualified_name_with_use.rs
1 use hir::AsAssocItem;
2 use ide_db::{
3     helpers::mod_path_to_ast,
4     imports::insert_use::{insert_use, ImportScope},
5 };
6 use syntax::{
7     ast::{self, make},
8     match_ast, ted, AstNode, SyntaxNode,
9 };
10
11 use crate::{AssistContext, AssistId, AssistKind, Assists};
12
13 // Assist: replace_qualified_name_with_use
14 //
15 // Adds a use statement for a given fully-qualified name.
16 //
17 // ```
18 // # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
19 // fn process(map: std::collections::$0HashMap<String, String>) {}
20 // ```
21 // ->
22 // ```
23 // use std::collections::HashMap;
24 //
25 // # mod std { pub mod collections { pub struct HashMap<T, U>(T, U); } }
26 // fn process(map: HashMap<String, String>) {}
27 // ```
28 pub(crate) fn replace_qualified_name_with_use(
29     acc: &mut Assists,
30     ctx: &AssistContext<'_>,
31 ) -> Option<()> {
32     let path: ast::Path = ctx.find_node_at_offset()?;
33     // We don't want to mess with use statements
34     if path.syntax().ancestors().find_map(ast::UseTree::cast).is_some() {
35         cov_mark::hit!(not_applicable_in_use);
36         return None;
37     }
38
39     if path.qualifier().is_none() {
40         cov_mark::hit!(dont_import_trivial_paths);
41         return None;
42     }
43
44     // only offer replacement for non assoc items
45     match ctx.sema.resolve_path(&path)? {
46         hir::PathResolution::Def(def) if def.as_assoc_item(ctx.sema.db).is_none() => (),
47         _ => return None,
48     }
49     // then search for an import for the first path segment of what we want to replace
50     // that way it is less likely that we import the item from a different location due re-exports
51     let module = match ctx.sema.resolve_path(&path.first_qualifier_or_self())? {
52         hir::PathResolution::Def(module @ hir::ModuleDef::Module(_)) => module,
53         _ => return None,
54     };
55
56     let starts_with_name_ref = !matches!(
57         path.first_segment().and_then(|it| it.kind()),
58         Some(
59             ast::PathSegmentKind::CrateKw
60                 | ast::PathSegmentKind::SuperKw
61                 | ast::PathSegmentKind::SelfKw
62         )
63     );
64     let path_to_qualifier = starts_with_name_ref
65         .then(|| {
66             ctx.sema.scope(path.syntax())?.module().find_use_path_prefixed(
67                 ctx.sema.db,
68                 module,
69                 ctx.config.insert_use.prefix_kind,
70                 ctx.config.prefer_no_std,
71             )
72         })
73         .flatten();
74
75     let scope = ImportScope::find_insert_use_container(path.syntax(), &ctx.sema)?;
76     let target = path.syntax().text_range();
77     acc.add(
78         AssistId("replace_qualified_name_with_use", AssistKind::RefactorRewrite),
79         "Replace qualified path with use",
80         target,
81         |builder| {
82             // Now that we've brought the name into scope, re-qualify all paths that could be
83             // affected (that is, all paths inside the node we added the `use` to).
84             let scope = match scope {
85                 ImportScope::File(it) => ImportScope::File(builder.make_mut(it)),
86                 ImportScope::Module(it) => ImportScope::Module(builder.make_mut(it)),
87                 ImportScope::Block(it) => ImportScope::Block(builder.make_mut(it)),
88             };
89             shorten_paths(scope.as_syntax_node(), &path);
90             let path = drop_generic_args(&path);
91             // stick the found import in front of the to be replaced path
92             let path = match path_to_qualifier.and_then(|it| mod_path_to_ast(&it).qualifier()) {
93                 Some(qualifier) => make::path_concat(qualifier, path),
94                 None => path,
95             };
96             insert_use(&scope, path, &ctx.config.insert_use);
97         },
98     )
99 }
100
101 fn drop_generic_args(path: &ast::Path) -> ast::Path {
102     let path = path.clone_for_update();
103     if let Some(segment) = path.segment() {
104         if let Some(generic_args) = segment.generic_arg_list() {
105             ted::remove(generic_args.syntax());
106         }
107     }
108     path
109 }
110
111 /// Mutates `node` to shorten `path` in all descendants of `node`.
112 fn shorten_paths(node: &SyntaxNode, path: &ast::Path) {
113     for child in node.children() {
114         match_ast! {
115             match child {
116                 // Don't modify `use` items, as this can break the `use` item when injecting a new
117                 // import into the use tree.
118                 ast::Use(_) => continue,
119                 // Don't descend into submodules, they don't have the same `use` items in scope.
120                 // FIXME: This isn't true due to `super::*` imports?
121                 ast::Module(_) => continue,
122                 ast::Path(p) => if maybe_replace_path(p.clone(), path.clone()).is_none() {
123                     shorten_paths(p.syntax(), path);
124                 },
125                 _ => shorten_paths(&child, path),
126             }
127         }
128     }
129 }
130
131 fn maybe_replace_path(path: ast::Path, target: ast::Path) -> Option<()> {
132     if !path_eq_no_generics(path.clone(), target) {
133         return None;
134     }
135
136     // Shorten `path`, leaving only its last segment.
137     if let Some(parent) = path.qualifier() {
138         ted::remove(parent.syntax());
139     }
140     if let Some(double_colon) = path.coloncolon_token() {
141         ted::remove(&double_colon);
142     }
143
144     Some(())
145 }
146
147 fn path_eq_no_generics(lhs: ast::Path, rhs: ast::Path) -> bool {
148     let mut lhs_curr = lhs;
149     let mut rhs_curr = rhs;
150     loop {
151         match lhs_curr.segment().zip(rhs_curr.segment()) {
152             Some((lhs, rhs))
153                 if lhs.coloncolon_token().is_some() == rhs.coloncolon_token().is_some()
154                     && lhs
155                         .name_ref()
156                         .zip(rhs.name_ref())
157                         .map_or(false, |(lhs, rhs)| lhs.text() == rhs.text()) => {}
158             _ => return false,
159         }
160
161         match (lhs_curr.qualifier(), rhs_curr.qualifier()) {
162             (Some(lhs), Some(rhs)) => {
163                 lhs_curr = lhs;
164                 rhs_curr = rhs;
165             }
166             (None, None) => return true,
167             _ => return false,
168         }
169     }
170 }
171
172 #[cfg(test)]
173 mod tests {
174     use crate::tests::{check_assist, check_assist_not_applicable};
175
176     use super::*;
177
178     #[test]
179     fn test_replace_already_imported() {
180         check_assist(
181             replace_qualified_name_with_use,
182             r"
183 mod std { pub mod fs { pub struct Path; } }
184 use std::fs;
185
186 fn main() {
187     std::f$0s::Path
188 }",
189             r"
190 mod std { pub mod fs { pub struct Path; } }
191 use std::fs;
192
193 fn main() {
194     fs::Path
195 }",
196         )
197     }
198
199     #[test]
200     fn test_replace_add_use_no_anchor() {
201         check_assist(
202             replace_qualified_name_with_use,
203             r"
204 mod std { pub mod fs { pub struct Path; } }
205 std::fs::Path$0
206     ",
207             r"
208 use std::fs::Path;
209
210 mod std { pub mod fs { pub struct Path; } }
211 Path
212     ",
213         );
214     }
215
216     #[test]
217     fn test_replace_add_use_no_anchor_middle_segment() {
218         check_assist(
219             replace_qualified_name_with_use,
220             r"
221 mod std { pub mod fs { pub struct Path; } }
222 std::fs$0::Path
223     ",
224             r"
225 use std::fs;
226
227 mod std { pub mod fs { pub struct Path; } }
228 fs::Path
229     ",
230         );
231     }
232
233     #[test]
234     fn dont_import_trivial_paths() {
235         cov_mark::check!(dont_import_trivial_paths);
236         check_assist_not_applicable(replace_qualified_name_with_use, r"impl foo$0 for () {}");
237     }
238
239     #[test]
240     fn test_replace_not_applicable_in_use() {
241         cov_mark::check!(not_applicable_in_use);
242         check_assist_not_applicable(replace_qualified_name_with_use, r"use std::fmt$0;");
243     }
244
245     #[test]
246     fn replaces_all_affected_paths() {
247         check_assist(
248             replace_qualified_name_with_use,
249             r"
250 mod std { pub mod fmt { pub trait Debug {} } }
251 fn main() {
252     std::fmt::Debug$0;
253     let x: std::fmt::Debug = std::fmt::Debug;
254 }
255     ",
256             r"
257 use std::fmt::Debug;
258
259 mod std { pub mod fmt { pub trait Debug {} } }
260 fn main() {
261     Debug;
262     let x: Debug = Debug;
263 }
264     ",
265         );
266     }
267
268     #[test]
269     fn does_not_replace_in_submodules() {
270         check_assist(
271             replace_qualified_name_with_use,
272             r"
273 mod std { pub mod fmt { pub trait Debug {} } }
274 fn main() {
275     std::fmt::Debug$0;
276 }
277
278 mod sub {
279     fn f() {
280         std::fmt::Debug;
281     }
282 }
283     ",
284             r"
285 use std::fmt::Debug;
286
287 mod std { pub mod fmt { pub trait Debug {} } }
288 fn main() {
289     Debug;
290 }
291
292 mod sub {
293     fn f() {
294         std::fmt::Debug;
295     }
296 }
297     ",
298         );
299     }
300
301     #[test]
302     fn does_not_replace_in_use() {
303         check_assist(
304             replace_qualified_name_with_use,
305             r"
306 mod std { pub mod fmt { pub trait Display {} } }
307 use std::fmt::Display;
308
309 fn main() {
310     std::fmt$0;
311 }
312     ",
313             r"
314 mod std { pub mod fmt { pub trait Display {} } }
315 use std::fmt::{Display, self};
316
317 fn main() {
318     fmt;
319 }
320     ",
321         );
322     }
323
324     #[test]
325     fn does_not_replace_assoc_item_path() {
326         check_assist_not_applicable(
327             replace_qualified_name_with_use,
328             r"
329 pub struct Foo;
330 impl Foo {
331     pub fn foo() {}
332 }
333
334 fn main() {
335     Foo::foo$0();
336 }
337 ",
338         );
339     }
340
341     #[test]
342     fn replace_reuses_path_qualifier() {
343         check_assist(
344             replace_qualified_name_with_use,
345             r"
346 pub mod foo {
347     pub struct Foo;
348 }
349
350 mod bar {
351     pub use super::foo::Foo as Bar;
352 }
353
354 fn main() {
355     foo::Foo$0;
356 }
357 ",
358             r"
359 use foo::Foo;
360
361 pub mod foo {
362     pub struct Foo;
363 }
364
365 mod bar {
366     pub use super::foo::Foo as Bar;
367 }
368
369 fn main() {
370     Foo;
371 }
372 ",
373         );
374     }
375
376     #[test]
377     fn replace_does_not_always_try_to_replace_by_full_item_path() {
378         check_assist(
379             replace_qualified_name_with_use,
380             r"
381 use std::mem;
382
383 mod std {
384     pub mod mem {
385         pub fn drop<T>(_: T) {}
386     }
387 }
388
389 fn main() {
390     mem::drop$0(0);
391 }
392 ",
393             r"
394 use std::mem::{self, drop};
395
396 mod std {
397     pub mod mem {
398         pub fn drop<T>(_: T) {}
399     }
400 }
401
402 fn main() {
403     drop(0);
404 }
405 ",
406         );
407     }
408
409     #[test]
410     fn replace_should_drop_generic_args_in_use() {
411         check_assist(
412             replace_qualified_name_with_use,
413             r"
414 mod std {
415     pub mod mem {
416         pub fn drop<T>(_: T) {}
417     }
418 }
419
420 fn main() {
421     std::mem::drop::<usize>$0(0);
422 }
423 ",
424             r"
425 use std::mem::drop;
426
427 mod std {
428     pub mod mem {
429         pub fn drop<T>(_: T) {}
430     }
431 }
432
433 fn main() {
434     drop::<usize>(0);
435 }
436 ",
437         );
438     }
439 }