]> git.lizzy.rs Git - rust.git/blob - crates/ide_assists/src/handlers/replace_qualified_name_with_use.rs
3121e2298178068fbf5d041f986c81cf1d237c01
[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.clone_for_update());
88             // stick the found import in front of the to be replaced path
89             let path = match path_to_qualifier.and_then(|it| mod_path_to_ast(&it).qualifier()) {
90                 Some(qualifier) => make::path_concat(qualifier, path),
91                 None => path,
92             };
93             insert_use(&scope, path, &ctx.config.insert_use);
94         },
95     )
96 }
97
98 /// Adds replacements to `re` that shorten `path` in all descendants of `node`.
99 fn shorten_paths(node: &SyntaxNode, path: &ast::Path) {
100     for child in node.children() {
101         match_ast! {
102             match child {
103                 // Don't modify `use` items, as this can break the `use` item when injecting a new
104                 // import into the use tree.
105                 ast::Use(_) => continue,
106                 // Don't descend into submodules, they don't have the same `use` items in scope.
107                 // FIXME: This isn't true due to `super::*` imports?
108                 ast::Module(_) => continue,
109                 ast::Path(p) => if maybe_replace_path(p.clone(), path.clone()).is_none() {
110                     shorten_paths(p.syntax(), path);
111                 },
112                 _ => shorten_paths(&child, path),
113             }
114         }
115     }
116 }
117
118 fn maybe_replace_path(path: ast::Path, target: ast::Path) -> Option<()> {
119     if !path_eq_no_generics(path.clone(), target) {
120         return None;
121     }
122
123     // Shorten `path`, leaving only its last segment.
124     if let Some(parent) = path.qualifier() {
125         ted::remove(parent.syntax());
126     }
127     if let Some(double_colon) = path.coloncolon_token() {
128         ted::remove(&double_colon);
129     }
130
131     Some(())
132 }
133
134 fn path_eq_no_generics(lhs: ast::Path, rhs: ast::Path) -> bool {
135     let mut lhs_curr = lhs;
136     let mut rhs_curr = rhs;
137     loop {
138         match lhs_curr.segment().zip(rhs_curr.segment()) {
139             Some((lhs, rhs))
140                 if lhs.coloncolon_token().is_some() == rhs.coloncolon_token().is_some()
141                     && lhs
142                         .name_ref()
143                         .zip(rhs.name_ref())
144                         .map_or(false, |(lhs, rhs)| lhs.text() == rhs.text()) => {}
145             _ => return false,
146         }
147
148         match (lhs_curr.qualifier(), rhs_curr.qualifier()) {
149             (Some(lhs), Some(rhs)) => {
150                 lhs_curr = lhs;
151                 rhs_curr = rhs;
152             }
153             (None, None) => return true,
154             _ => return false,
155         }
156     }
157 }
158
159 #[cfg(test)]
160 mod tests {
161     use crate::tests::{check_assist, check_assist_not_applicable};
162
163     use super::*;
164
165     #[test]
166     fn test_replace_already_imported() {
167         check_assist(
168             replace_qualified_name_with_use,
169             r"
170 mod std { pub mod fs { pub struct Path; } }
171 use std::fs;
172
173 fn main() {
174     std::f$0s::Path
175 }",
176             r"
177 mod std { pub mod fs { pub struct Path; } }
178 use std::fs;
179
180 fn main() {
181     fs::Path
182 }",
183         )
184     }
185
186     #[test]
187     fn test_replace_add_use_no_anchor() {
188         check_assist(
189             replace_qualified_name_with_use,
190             r"
191 mod std { pub mod fs { pub struct Path; } }
192 std::fs::Path$0
193     ",
194             r"
195 use std::fs::Path;
196
197 mod std { pub mod fs { pub struct Path; } }
198 Path
199     ",
200         );
201     }
202
203     #[test]
204     fn test_replace_add_use_no_anchor_middle_segment() {
205         check_assist(
206             replace_qualified_name_with_use,
207             r"
208 mod std { pub mod fs { pub struct Path; } }
209 std::fs$0::Path
210     ",
211             r"
212 use std::fs;
213
214 mod std { pub mod fs { pub struct Path; } }
215 fs::Path
216     ",
217         );
218     }
219
220     #[test]
221     fn dont_import_trivial_paths() {
222         cov_mark::check!(dont_import_trivial_paths);
223         check_assist_not_applicable(replace_qualified_name_with_use, r"impl foo$0 for () {}");
224     }
225
226     #[test]
227     fn test_replace_not_applicable_in_use() {
228         cov_mark::check!(not_applicable_in_use);
229         check_assist_not_applicable(replace_qualified_name_with_use, r"use std::fmt$0;");
230     }
231
232     #[test]
233     fn replaces_all_affected_paths() {
234         check_assist(
235             replace_qualified_name_with_use,
236             r"
237 mod std { pub mod fmt { pub trait Debug {} } }
238 fn main() {
239     std::fmt::Debug$0;
240     let x: std::fmt::Debug = std::fmt::Debug;
241 }
242     ",
243             r"
244 use std::fmt::Debug;
245
246 mod std { pub mod fmt { pub trait Debug {} } }
247 fn main() {
248     Debug;
249     let x: Debug = Debug;
250 }
251     ",
252         );
253     }
254
255     #[test]
256     fn does_not_replace_in_submodules() {
257         check_assist(
258             replace_qualified_name_with_use,
259             r"
260 mod std { pub mod fmt { pub trait Debug {} } }
261 fn main() {
262     std::fmt::Debug$0;
263 }
264
265 mod sub {
266     fn f() {
267         std::fmt::Debug;
268     }
269 }
270     ",
271             r"
272 use std::fmt::Debug;
273
274 mod std { pub mod fmt { pub trait Debug {} } }
275 fn main() {
276     Debug;
277 }
278
279 mod sub {
280     fn f() {
281         std::fmt::Debug;
282     }
283 }
284     ",
285         );
286     }
287
288     #[test]
289     fn does_not_replace_in_use() {
290         check_assist(
291             replace_qualified_name_with_use,
292             r"
293 mod std { pub mod fmt { pub trait Display {} } }
294 use std::fmt::Display;
295
296 fn main() {
297     std::fmt$0;
298 }
299     ",
300             r"
301 mod std { pub mod fmt { pub trait Display {} } }
302 use std::fmt::{Display, self};
303
304 fn main() {
305     fmt;
306 }
307     ",
308         );
309     }
310
311     #[test]
312     fn does_not_replace_assoc_item_path() {
313         check_assist_not_applicable(
314             replace_qualified_name_with_use,
315             r"
316 pub struct Foo;
317 impl Foo {
318     pub fn foo() {}
319 }
320
321 fn main() {
322     Foo::foo$0();
323 }
324 ",
325         );
326     }
327
328     #[test]
329     fn replace_reuses_path_qualifier() {
330         check_assist(
331             replace_qualified_name_with_use,
332             r"
333 pub mod foo {
334     pub struct Foo;
335 }
336
337 mod bar {
338     pub use super::foo::Foo as Bar;
339 }
340
341 fn main() {
342     foo::Foo$0;
343 }
344 ",
345             r"
346 use foo::Foo;
347
348 pub mod foo {
349     pub struct Foo;
350 }
351
352 mod bar {
353     pub use super::foo::Foo as Bar;
354 }
355
356 fn main() {
357     Foo;
358 }
359 ",
360         );
361     }
362
363     #[test]
364     fn replace_does_not_always_try_to_replace_by_full_item_path() {
365         check_assist(
366             replace_qualified_name_with_use,
367             r"
368 use std::mem;
369
370 mod std {
371     pub mod mem {
372         pub fn drop<T>(_: T) {}
373     }
374 }
375
376 fn main() {
377     mem::drop$0(0);
378 }
379 ",
380             r"
381 use std::mem::{self, drop};
382
383 mod std {
384     pub mod mem {
385         pub fn drop<T>(_: T) {}
386     }
387 }
388
389 fn main() {
390     drop(0);
391 }
392 ",
393         );
394     }
395 }