diff options
Diffstat (limited to 'crates/ide_assists/src/utils.rs')
-rw-r--r-- | crates/ide_assists/src/utils.rs | 490 |
1 files changed, 490 insertions, 0 deletions
diff --git a/crates/ide_assists/src/utils.rs b/crates/ide_assists/src/utils.rs new file mode 100644 index 000000000..62f959082 --- /dev/null +++ b/crates/ide_assists/src/utils.rs | |||
@@ -0,0 +1,490 @@ | |||
1 | //! Assorted functions shared by several assists. | ||
2 | |||
3 | pub(crate) mod suggest_name; | ||
4 | |||
5 | use std::ops; | ||
6 | |||
7 | use ast::TypeBoundsOwner; | ||
8 | use hir::{Adt, HasSource, Semantics}; | ||
9 | use ide_db::{ | ||
10 | helpers::{FamousDefs, SnippetCap}, | ||
11 | RootDatabase, | ||
12 | }; | ||
13 | use itertools::Itertools; | ||
14 | use stdx::format_to; | ||
15 | use syntax::{ | ||
16 | ast::edit::AstNodeEdit, | ||
17 | ast::AttrsOwner, | ||
18 | ast::NameOwner, | ||
19 | ast::{self, edit, make, ArgListOwner, GenericParamsOwner}, | ||
20 | AstNode, Direction, SmolStr, | ||
21 | SyntaxKind::*, | ||
22 | SyntaxNode, TextSize, T, | ||
23 | }; | ||
24 | |||
25 | use crate::{ | ||
26 | assist_context::{AssistBuilder, AssistContext}, | ||
27 | ast_transform::{self, AstTransform, QualifyPaths, SubstituteTypeParams}, | ||
28 | }; | ||
29 | |||
30 | pub(crate) fn unwrap_trivial_block(block: ast::BlockExpr) -> ast::Expr { | ||
31 | extract_trivial_expression(&block) | ||
32 | .filter(|expr| !expr.syntax().text().contains_char('\n')) | ||
33 | .unwrap_or_else(|| block.into()) | ||
34 | } | ||
35 | |||
36 | pub fn extract_trivial_expression(block: &ast::BlockExpr) -> Option<ast::Expr> { | ||
37 | let has_anything_else = |thing: &SyntaxNode| -> bool { | ||
38 | let mut non_trivial_children = | ||
39 | block.syntax().children_with_tokens().filter(|it| match it.kind() { | ||
40 | WHITESPACE | T!['{'] | T!['}'] => false, | ||
41 | _ => it.as_node() != Some(thing), | ||
42 | }); | ||
43 | non_trivial_children.next().is_some() | ||
44 | }; | ||
45 | |||
46 | if let Some(expr) = block.tail_expr() { | ||
47 | if has_anything_else(expr.syntax()) { | ||
48 | return None; | ||
49 | } | ||
50 | return Some(expr); | ||
51 | } | ||
52 | // Unwrap `{ continue; }` | ||
53 | let (stmt,) = block.statements().next_tuple()?; | ||
54 | if let ast::Stmt::ExprStmt(expr_stmt) = stmt { | ||
55 | if has_anything_else(expr_stmt.syntax()) { | ||
56 | return None; | ||
57 | } | ||
58 | let expr = expr_stmt.expr()?; | ||
59 | match expr.syntax().kind() { | ||
60 | CONTINUE_EXPR | BREAK_EXPR | RETURN_EXPR => return Some(expr), | ||
61 | _ => (), | ||
62 | } | ||
63 | } | ||
64 | None | ||
65 | } | ||
66 | |||
67 | /// This is a method with a heuristics to support test methods annotated with custom test annotations, such as | ||
68 | /// `#[test_case(...)]`, `#[tokio::test]` and similar. | ||
69 | /// Also a regular `#[test]` annotation is supported. | ||
70 | /// | ||
71 | /// It may produce false positives, for example, `#[wasm_bindgen_test]` requires a different command to run the test, | ||
72 | /// but it's better than not to have the runnables for the tests at all. | ||
73 | pub fn test_related_attribute(fn_def: &ast::Fn) -> Option<ast::Attr> { | ||
74 | fn_def.attrs().find_map(|attr| { | ||
75 | let path = attr.path()?; | ||
76 | if path.syntax().text().to_string().contains("test") { | ||
77 | Some(attr) | ||
78 | } else { | ||
79 | None | ||
80 | } | ||
81 | }) | ||
82 | } | ||
83 | |||
84 | #[derive(Copy, Clone, PartialEq)] | ||
85 | pub enum DefaultMethods { | ||
86 | Only, | ||
87 | No, | ||
88 | } | ||
89 | |||
90 | pub fn filter_assoc_items( | ||
91 | db: &RootDatabase, | ||
92 | items: &[hir::AssocItem], | ||
93 | default_methods: DefaultMethods, | ||
94 | ) -> Vec<ast::AssocItem> { | ||
95 | fn has_def_name(item: &ast::AssocItem) -> bool { | ||
96 | match item { | ||
97 | ast::AssocItem::Fn(def) => def.name(), | ||
98 | ast::AssocItem::TypeAlias(def) => def.name(), | ||
99 | ast::AssocItem::Const(def) => def.name(), | ||
100 | ast::AssocItem::MacroCall(_) => None, | ||
101 | } | ||
102 | .is_some() | ||
103 | } | ||
104 | |||
105 | items | ||
106 | .iter() | ||
107 | // Note: This throws away items with no source. | ||
108 | .filter_map(|i| { | ||
109 | let item = match i { | ||
110 | hir::AssocItem::Function(i) => ast::AssocItem::Fn(i.source(db)?.value), | ||
111 | hir::AssocItem::TypeAlias(i) => ast::AssocItem::TypeAlias(i.source(db)?.value), | ||
112 | hir::AssocItem::Const(i) => ast::AssocItem::Const(i.source(db)?.value), | ||
113 | }; | ||
114 | Some(item) | ||
115 | }) | ||
116 | .filter(has_def_name) | ||
117 | .filter(|it| match it { | ||
118 | ast::AssocItem::Fn(def) => matches!( | ||
119 | (default_methods, def.body()), | ||
120 | (DefaultMethods::Only, Some(_)) | (DefaultMethods::No, None) | ||
121 | ), | ||
122 | _ => default_methods == DefaultMethods::No, | ||
123 | }) | ||
124 | .collect::<Vec<_>>() | ||
125 | } | ||
126 | |||
127 | pub fn add_trait_assoc_items_to_impl( | ||
128 | sema: &hir::Semantics<ide_db::RootDatabase>, | ||
129 | items: Vec<ast::AssocItem>, | ||
130 | trait_: hir::Trait, | ||
131 | impl_def: ast::Impl, | ||
132 | target_scope: hir::SemanticsScope, | ||
133 | ) -> (ast::Impl, ast::AssocItem) { | ||
134 | let impl_item_list = impl_def.assoc_item_list().unwrap_or_else(make::assoc_item_list); | ||
135 | |||
136 | let n_existing_items = impl_item_list.assoc_items().count(); | ||
137 | let source_scope = sema.scope_for_def(trait_); | ||
138 | let ast_transform = QualifyPaths::new(&target_scope, &source_scope) | ||
139 | .or(SubstituteTypeParams::for_trait_impl(&source_scope, trait_, impl_def.clone())); | ||
140 | |||
141 | let items = items | ||
142 | .into_iter() | ||
143 | .map(|it| ast_transform::apply(&*ast_transform, it)) | ||
144 | .map(|it| match it { | ||
145 | ast::AssocItem::Fn(def) => ast::AssocItem::Fn(add_body(def)), | ||
146 | ast::AssocItem::TypeAlias(def) => ast::AssocItem::TypeAlias(def.remove_bounds()), | ||
147 | _ => it, | ||
148 | }) | ||
149 | .map(|it| edit::remove_attrs_and_docs(&it)); | ||
150 | |||
151 | let new_impl_item_list = impl_item_list.append_items(items); | ||
152 | let new_impl_def = impl_def.with_assoc_item_list(new_impl_item_list); | ||
153 | let first_new_item = | ||
154 | new_impl_def.assoc_item_list().unwrap().assoc_items().nth(n_existing_items).unwrap(); | ||
155 | return (new_impl_def, first_new_item); | ||
156 | |||
157 | fn add_body(fn_def: ast::Fn) -> ast::Fn { | ||
158 | match fn_def.body() { | ||
159 | Some(_) => fn_def, | ||
160 | None => { | ||
161 | let body = | ||
162 | make::block_expr(None, Some(make::expr_todo())).indent(edit::IndentLevel(1)); | ||
163 | fn_def.with_body(body) | ||
164 | } | ||
165 | } | ||
166 | } | ||
167 | } | ||
168 | |||
169 | #[derive(Clone, Copy, Debug)] | ||
170 | pub(crate) enum Cursor<'a> { | ||
171 | Replace(&'a SyntaxNode), | ||
172 | Before(&'a SyntaxNode), | ||
173 | } | ||
174 | |||
175 | impl<'a> Cursor<'a> { | ||
176 | fn node(self) -> &'a SyntaxNode { | ||
177 | match self { | ||
178 | Cursor::Replace(node) | Cursor::Before(node) => node, | ||
179 | } | ||
180 | } | ||
181 | } | ||
182 | |||
183 | pub(crate) fn render_snippet(_cap: SnippetCap, node: &SyntaxNode, cursor: Cursor) -> String { | ||
184 | assert!(cursor.node().ancestors().any(|it| it == *node)); | ||
185 | let range = cursor.node().text_range() - node.text_range().start(); | ||
186 | let range: ops::Range<usize> = range.into(); | ||
187 | |||
188 | let mut placeholder = cursor.node().to_string(); | ||
189 | escape(&mut placeholder); | ||
190 | let tab_stop = match cursor { | ||
191 | Cursor::Replace(placeholder) => format!("${{0:{}}}", placeholder), | ||
192 | Cursor::Before(placeholder) => format!("$0{}", placeholder), | ||
193 | }; | ||
194 | |||
195 | let mut buf = node.to_string(); | ||
196 | buf.replace_range(range, &tab_stop); | ||
197 | return buf; | ||
198 | |||
199 | fn escape(buf: &mut String) { | ||
200 | stdx::replace(buf, '{', r"\{"); | ||
201 | stdx::replace(buf, '}', r"\}"); | ||
202 | stdx::replace(buf, '$', r"\$"); | ||
203 | } | ||
204 | } | ||
205 | |||
206 | pub(crate) fn vis_offset(node: &SyntaxNode) -> TextSize { | ||
207 | node.children_with_tokens() | ||
208 | .find(|it| !matches!(it.kind(), WHITESPACE | COMMENT | ATTR)) | ||
209 | .map(|it| it.text_range().start()) | ||
210 | .unwrap_or_else(|| node.text_range().start()) | ||
211 | } | ||
212 | |||
213 | pub(crate) fn invert_boolean_expression( | ||
214 | sema: &Semantics<RootDatabase>, | ||
215 | expr: ast::Expr, | ||
216 | ) -> ast::Expr { | ||
217 | if let Some(expr) = invert_special_case(sema, &expr) { | ||
218 | return expr; | ||
219 | } | ||
220 | make::expr_prefix(T![!], expr) | ||
221 | } | ||
222 | |||
223 | fn invert_special_case(sema: &Semantics<RootDatabase>, expr: &ast::Expr) -> Option<ast::Expr> { | ||
224 | match expr { | ||
225 | ast::Expr::BinExpr(bin) => match bin.op_kind()? { | ||
226 | ast::BinOp::NegatedEqualityTest => bin.replace_op(T![==]).map(|it| it.into()), | ||
227 | ast::BinOp::EqualityTest => bin.replace_op(T![!=]).map(|it| it.into()), | ||
228 | // Swap `<` with `>=`, `<=` with `>`, ... if operands `impl Ord` | ||
229 | ast::BinOp::LesserTest if bin_impls_ord(sema, bin) => { | ||
230 | bin.replace_op(T![>=]).map(|it| it.into()) | ||
231 | } | ||
232 | ast::BinOp::LesserEqualTest if bin_impls_ord(sema, bin) => { | ||
233 | bin.replace_op(T![>]).map(|it| it.into()) | ||
234 | } | ||
235 | ast::BinOp::GreaterTest if bin_impls_ord(sema, bin) => { | ||
236 | bin.replace_op(T![<=]).map(|it| it.into()) | ||
237 | } | ||
238 | ast::BinOp::GreaterEqualTest if bin_impls_ord(sema, bin) => { | ||
239 | bin.replace_op(T![<]).map(|it| it.into()) | ||
240 | } | ||
241 | // Parenthesize other expressions before prefixing `!` | ||
242 | _ => Some(make::expr_prefix(T![!], make::expr_paren(expr.clone()))), | ||
243 | }, | ||
244 | ast::Expr::MethodCallExpr(mce) => { | ||
245 | let receiver = mce.receiver()?; | ||
246 | let method = mce.name_ref()?; | ||
247 | let arg_list = mce.arg_list()?; | ||
248 | |||
249 | let method = match method.text() { | ||
250 | "is_some" => "is_none", | ||
251 | "is_none" => "is_some", | ||
252 | "is_ok" => "is_err", | ||
253 | "is_err" => "is_ok", | ||
254 | _ => return None, | ||
255 | }; | ||
256 | Some(make::expr_method_call(receiver, method, arg_list)) | ||
257 | } | ||
258 | ast::Expr::PrefixExpr(pe) if pe.op_kind()? == ast::PrefixOp::Not => { | ||
259 | if let ast::Expr::ParenExpr(parexpr) = pe.expr()? { | ||
260 | parexpr.expr() | ||
261 | } else { | ||
262 | pe.expr() | ||
263 | } | ||
264 | } | ||
265 | // FIXME: | ||
266 | // ast::Expr::Literal(true | false ) | ||
267 | _ => None, | ||
268 | } | ||
269 | } | ||
270 | |||
271 | fn bin_impls_ord(sema: &Semantics<RootDatabase>, bin: &ast::BinExpr) -> bool { | ||
272 | match ( | ||
273 | bin.lhs().and_then(|lhs| sema.type_of_expr(&lhs)), | ||
274 | bin.rhs().and_then(|rhs| sema.type_of_expr(&rhs)), | ||
275 | ) { | ||
276 | (Some(lhs_ty), Some(rhs_ty)) if lhs_ty == rhs_ty => { | ||
277 | let krate = sema.scope(bin.syntax()).module().map(|it| it.krate()); | ||
278 | let ord_trait = FamousDefs(sema, krate).core_cmp_Ord(); | ||
279 | ord_trait.map_or(false, |ord_trait| { | ||
280 | lhs_ty.autoderef(sema.db).any(|ty| ty.impls_trait(sema.db, ord_trait, &[])) | ||
281 | }) | ||
282 | } | ||
283 | _ => false, | ||
284 | } | ||
285 | } | ||
286 | |||
287 | pub(crate) fn next_prev() -> impl Iterator<Item = Direction> { | ||
288 | [Direction::Next, Direction::Prev].iter().copied() | ||
289 | } | ||
290 | |||
291 | pub(crate) fn does_pat_match_variant(pat: &ast::Pat, var: &ast::Pat) -> bool { | ||
292 | let first_node_text = |pat: &ast::Pat| pat.syntax().first_child().map(|node| node.text()); | ||
293 | |||
294 | let pat_head = match pat { | ||
295 | ast::Pat::IdentPat(bind_pat) => { | ||
296 | if let Some(p) = bind_pat.pat() { | ||
297 | first_node_text(&p) | ||
298 | } else { | ||
299 | return pat.syntax().text() == var.syntax().text(); | ||
300 | } | ||
301 | } | ||
302 | pat => first_node_text(pat), | ||
303 | }; | ||
304 | |||
305 | let var_head = first_node_text(var); | ||
306 | |||
307 | pat_head == var_head | ||
308 | } | ||
309 | |||
310 | // Uses a syntax-driven approach to find any impl blocks for the struct that | ||
311 | // exist within the module/file | ||
312 | // | ||
313 | // Returns `None` if we've found an existing fn | ||
314 | // | ||
315 | // FIXME: change the new fn checking to a more semantic approach when that's more | ||
316 | // viable (e.g. we process proc macros, etc) | ||
317 | // FIXME: this partially overlaps with `find_impl_block_*` | ||
318 | pub(crate) fn find_struct_impl( | ||
319 | ctx: &AssistContext, | ||
320 | strukt: &ast::Adt, | ||
321 | name: &str, | ||
322 | ) -> Option<Option<ast::Impl>> { | ||
323 | let db = ctx.db(); | ||
324 | let module = strukt.syntax().ancestors().find(|node| { | ||
325 | ast::Module::can_cast(node.kind()) || ast::SourceFile::can_cast(node.kind()) | ||
326 | })?; | ||
327 | |||
328 | let struct_def = match strukt { | ||
329 | ast::Adt::Enum(e) => Adt::Enum(ctx.sema.to_def(e)?), | ||
330 | ast::Adt::Struct(s) => Adt::Struct(ctx.sema.to_def(s)?), | ||
331 | ast::Adt::Union(u) => Adt::Union(ctx.sema.to_def(u)?), | ||
332 | }; | ||
333 | |||
334 | let block = module.descendants().filter_map(ast::Impl::cast).find_map(|impl_blk| { | ||
335 | let blk = ctx.sema.to_def(&impl_blk)?; | ||
336 | |||
337 | // FIXME: handle e.g. `struct S<T>; impl<U> S<U> {}` | ||
338 | // (we currently use the wrong type parameter) | ||
339 | // also we wouldn't want to use e.g. `impl S<u32>` | ||
340 | |||
341 | let same_ty = match blk.target_ty(db).as_adt() { | ||
342 | Some(def) => def == struct_def, | ||
343 | None => false, | ||
344 | }; | ||
345 | let not_trait_impl = blk.target_trait(db).is_none(); | ||
346 | |||
347 | if !(same_ty && not_trait_impl) { | ||
348 | None | ||
349 | } else { | ||
350 | Some(impl_blk) | ||
351 | } | ||
352 | }); | ||
353 | |||
354 | if let Some(ref impl_blk) = block { | ||
355 | if has_fn(impl_blk, name) { | ||
356 | return None; | ||
357 | } | ||
358 | } | ||
359 | |||
360 | Some(block) | ||
361 | } | ||
362 | |||
363 | fn has_fn(imp: &ast::Impl, rhs_name: &str) -> bool { | ||
364 | if let Some(il) = imp.assoc_item_list() { | ||
365 | for item in il.assoc_items() { | ||
366 | if let ast::AssocItem::Fn(f) = item { | ||
367 | if let Some(name) = f.name() { | ||
368 | if name.text().eq_ignore_ascii_case(rhs_name) { | ||
369 | return true; | ||
370 | } | ||
371 | } | ||
372 | } | ||
373 | } | ||
374 | } | ||
375 | |||
376 | false | ||
377 | } | ||
378 | |||
379 | /// Find the start of the `impl` block for the given `ast::Impl`. | ||
380 | // | ||
381 | // FIXME: this partially overlaps with `find_struct_impl` | ||
382 | pub(crate) fn find_impl_block_start(impl_def: ast::Impl, buf: &mut String) -> Option<TextSize> { | ||
383 | buf.push('\n'); | ||
384 | let start = impl_def.assoc_item_list().and_then(|it| it.l_curly_token())?.text_range().end(); | ||
385 | Some(start) | ||
386 | } | ||
387 | |||
388 | /// Find the end of the `impl` block for the given `ast::Impl`. | ||
389 | // | ||
390 | // FIXME: this partially overlaps with `find_struct_impl` | ||
391 | pub(crate) fn find_impl_block_end(impl_def: ast::Impl, buf: &mut String) -> Option<TextSize> { | ||
392 | buf.push('\n'); | ||
393 | let end = impl_def | ||
394 | .assoc_item_list() | ||
395 | .and_then(|it| it.r_curly_token())? | ||
396 | .prev_sibling_or_token()? | ||
397 | .text_range() | ||
398 | .end(); | ||
399 | Some(end) | ||
400 | } | ||
401 | |||
402 | // Generates the surrounding `impl Type { <code> }` including type and lifetime | ||
403 | // parameters | ||
404 | pub(crate) fn generate_impl_text(adt: &ast::Adt, code: &str) -> String { | ||
405 | generate_impl_text_inner(adt, None, code) | ||
406 | } | ||
407 | |||
408 | // Generates the surrounding `impl <trait> for Type { <code> }` including type | ||
409 | // and lifetime parameters | ||
410 | pub(crate) fn generate_trait_impl_text(adt: &ast::Adt, trait_text: &str, code: &str) -> String { | ||
411 | generate_impl_text_inner(adt, Some(trait_text), code) | ||
412 | } | ||
413 | |||
414 | fn generate_impl_text_inner(adt: &ast::Adt, trait_text: Option<&str>, code: &str) -> String { | ||
415 | let generic_params = adt.generic_param_list(); | ||
416 | let mut buf = String::with_capacity(code.len()); | ||
417 | buf.push_str("\n\n"); | ||
418 | adt.attrs() | ||
419 | .filter(|attr| attr.as_simple_call().map(|(name, _arg)| name == "cfg").unwrap_or(false)) | ||
420 | .for_each(|attr| buf.push_str(format!("{}\n", attr.to_string()).as_str())); | ||
421 | buf.push_str("impl"); | ||
422 | if let Some(generic_params) = &generic_params { | ||
423 | let lifetimes = generic_params.lifetime_params().map(|lt| format!("{}", lt.syntax())); | ||
424 | let type_params = generic_params.type_params().map(|type_param| { | ||
425 | let mut buf = String::new(); | ||
426 | if let Some(it) = type_param.name() { | ||
427 | format_to!(buf, "{}", it.syntax()); | ||
428 | } | ||
429 | if let Some(it) = type_param.colon_token() { | ||
430 | format_to!(buf, "{} ", it); | ||
431 | } | ||
432 | if let Some(it) = type_param.type_bound_list() { | ||
433 | format_to!(buf, "{}", it.syntax()); | ||
434 | } | ||
435 | buf | ||
436 | }); | ||
437 | let generics = lifetimes.chain(type_params).format(", "); | ||
438 | format_to!(buf, "<{}>", generics); | ||
439 | } | ||
440 | buf.push(' '); | ||
441 | if let Some(trait_text) = trait_text { | ||
442 | buf.push_str(trait_text); | ||
443 | buf.push_str(" for "); | ||
444 | } | ||
445 | buf.push_str(adt.name().unwrap().text()); | ||
446 | if let Some(generic_params) = generic_params { | ||
447 | let lifetime_params = generic_params | ||
448 | .lifetime_params() | ||
449 | .filter_map(|it| it.lifetime()) | ||
450 | .map(|it| SmolStr::from(it.text())); | ||
451 | let type_params = generic_params | ||
452 | .type_params() | ||
453 | .filter_map(|it| it.name()) | ||
454 | .map(|it| SmolStr::from(it.text())); | ||
455 | format_to!(buf, "<{}>", lifetime_params.chain(type_params).format(", ")) | ||
456 | } | ||
457 | |||
458 | match adt.where_clause() { | ||
459 | Some(where_clause) => { | ||
460 | format_to!(buf, "\n{}\n{{\n{}\n}}", where_clause, code); | ||
461 | } | ||
462 | None => { | ||
463 | format_to!(buf, " {{\n{}\n}}", code); | ||
464 | } | ||
465 | } | ||
466 | |||
467 | buf | ||
468 | } | ||
469 | |||
470 | pub(crate) fn add_method_to_adt( | ||
471 | builder: &mut AssistBuilder, | ||
472 | adt: &ast::Adt, | ||
473 | impl_def: Option<ast::Impl>, | ||
474 | method: &str, | ||
475 | ) { | ||
476 | let mut buf = String::with_capacity(method.len() + 2); | ||
477 | if impl_def.is_some() { | ||
478 | buf.push('\n'); | ||
479 | } | ||
480 | buf.push_str(method); | ||
481 | |||
482 | let start_offset = impl_def | ||
483 | .and_then(|impl_def| find_impl_block_end(impl_def, &mut buf)) | ||
484 | .unwrap_or_else(|| { | ||
485 | buf = generate_impl_text(&adt, &buf); | ||
486 | adt.syntax().text_range().end() | ||
487 | }); | ||
488 | |||
489 | builder.insert(start_offset, buf); | ||
490 | } | ||