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