diff options
Diffstat (limited to 'crates/ra_analysis/src/call_info.rs')
-rw-r--r-- | crates/ra_analysis/src/call_info.rs | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/crates/ra_analysis/src/call_info.rs b/crates/ra_analysis/src/call_info.rs new file mode 100644 index 000000000..1dac95584 --- /dev/null +++ b/crates/ra_analysis/src/call_info.rs | |||
@@ -0,0 +1,451 @@ | |||
1 | use std::cmp::{max, min}; | ||
2 | |||
3 | use ra_db::{SyntaxDatabase, Cancelable}; | ||
4 | use ra_syntax::{ | ||
5 | AstNode, SyntaxNode, TextUnit, TextRange, | ||
6 | SyntaxKind::FN_DEF, | ||
7 | ast::{self, ArgListOwner, DocCommentsOwner}, | ||
8 | }; | ||
9 | use ra_editor::find_node_at_offset; | ||
10 | |||
11 | use crate::{FilePosition, CallInfo, db::RootDatabase}; | ||
12 | |||
13 | /// Computes parameter information for the given call expression. | ||
14 | pub(crate) fn call_info(db: &RootDatabase, position: FilePosition) -> Cancelable<Option<CallInfo>> { | ||
15 | let file = db.source_file(position.file_id); | ||
16 | let syntax = file.syntax(); | ||
17 | |||
18 | // Find the calling expression and it's NameRef | ||
19 | let calling_node = ctry!(FnCallNode::with_node(syntax, position.offset)); | ||
20 | let name_ref = ctry!(calling_node.name_ref()); | ||
21 | |||
22 | // Resolve the function's NameRef (NOTE: this isn't entirely accurate). | ||
23 | let file_symbols = db.index_resolve(name_ref)?; | ||
24 | let symbol = ctry!(file_symbols.into_iter().find(|it| it.ptr.kind() == FN_DEF)); | ||
25 | let fn_file = db.source_file(symbol.file_id); | ||
26 | let fn_def = symbol.ptr.resolve(&fn_file); | ||
27 | let fn_def = ast::FnDef::cast(&fn_def).unwrap(); | ||
28 | let mut call_info = ctry!(CallInfo::new(fn_def)); | ||
29 | // If we have a calling expression let's find which argument we are on | ||
30 | let num_params = call_info.parameters.len(); | ||
31 | let has_self = fn_def.param_list().and_then(|l| l.self_param()).is_some(); | ||
32 | |||
33 | if num_params == 1 { | ||
34 | if !has_self { | ||
35 | call_info.active_parameter = Some(0); | ||
36 | } | ||
37 | } else if num_params > 1 { | ||
38 | // Count how many parameters into the call we are. | ||
39 | // TODO: This is best effort for now and should be fixed at some point. | ||
40 | // It may be better to see where we are in the arg_list and then check | ||
41 | // where offset is in that list (or beyond). | ||
42 | // Revisit this after we get documentation comments in. | ||
43 | if let Some(ref arg_list) = calling_node.arg_list() { | ||
44 | let start = arg_list.syntax().range().start(); | ||
45 | |||
46 | let range_search = TextRange::from_to(start, position.offset); | ||
47 | let mut commas: usize = arg_list | ||
48 | .syntax() | ||
49 | .text() | ||
50 | .slice(range_search) | ||
51 | .to_string() | ||
52 | .matches(',') | ||
53 | .count(); | ||
54 | |||
55 | // If we have a method call eat the first param since it's just self. | ||
56 | if has_self { | ||
57 | commas += 1; | ||
58 | } | ||
59 | |||
60 | call_info.active_parameter = Some(commas); | ||
61 | } | ||
62 | } | ||
63 | |||
64 | Ok(Some(call_info)) | ||
65 | } | ||
66 | |||
67 | enum FnCallNode<'a> { | ||
68 | CallExpr(&'a ast::CallExpr), | ||
69 | MethodCallExpr(&'a ast::MethodCallExpr), | ||
70 | } | ||
71 | |||
72 | impl<'a> FnCallNode<'a> { | ||
73 | pub fn with_node(syntax: &'a SyntaxNode, offset: TextUnit) -> Option<FnCallNode<'a>> { | ||
74 | if let Some(expr) = find_node_at_offset::<ast::CallExpr>(syntax, offset) { | ||
75 | return Some(FnCallNode::CallExpr(expr)); | ||
76 | } | ||
77 | if let Some(expr) = find_node_at_offset::<ast::MethodCallExpr>(syntax, offset) { | ||
78 | return Some(FnCallNode::MethodCallExpr(expr)); | ||
79 | } | ||
80 | None | ||
81 | } | ||
82 | |||
83 | pub fn name_ref(&self) -> Option<&'a ast::NameRef> { | ||
84 | match *self { | ||
85 | FnCallNode::CallExpr(call_expr) => Some(match call_expr.expr()?.kind() { | ||
86 | ast::ExprKind::PathExpr(path_expr) => path_expr.path()?.segment()?.name_ref()?, | ||
87 | _ => return None, | ||
88 | }), | ||
89 | |||
90 | FnCallNode::MethodCallExpr(call_expr) => call_expr | ||
91 | .syntax() | ||
92 | .children() | ||
93 | .filter_map(ast::NameRef::cast) | ||
94 | .nth(0), | ||
95 | } | ||
96 | } | ||
97 | |||
98 | pub fn arg_list(&self) -> Option<&'a ast::ArgList> { | ||
99 | match *self { | ||
100 | FnCallNode::CallExpr(expr) => expr.arg_list(), | ||
101 | FnCallNode::MethodCallExpr(expr) => expr.arg_list(), | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | |||
106 | impl CallInfo { | ||
107 | fn new(node: &ast::FnDef) -> Option<Self> { | ||
108 | let mut doc = None; | ||
109 | |||
110 | // Strip the body out for the label. | ||
111 | let mut label: String = if let Some(body) = node.body() { | ||
112 | let body_range = body.syntax().range(); | ||
113 | let label: String = node | ||
114 | .syntax() | ||
115 | .children() | ||
116 | .filter(|child| !child.range().is_subrange(&body_range)) | ||
117 | .map(|node| node.text().to_string()) | ||
118 | .collect(); | ||
119 | label | ||
120 | } else { | ||
121 | node.syntax().text().to_string() | ||
122 | }; | ||
123 | |||
124 | if let Some((comment_range, docs)) = extract_doc_comments(node) { | ||
125 | let comment_range = comment_range | ||
126 | .checked_sub(node.syntax().range().start()) | ||
127 | .unwrap(); | ||
128 | let start = comment_range.start().to_usize(); | ||
129 | let end = comment_range.end().to_usize(); | ||
130 | |||
131 | // Remove the comment from the label | ||
132 | label.replace_range(start..end, ""); | ||
133 | |||
134 | // Massage markdown | ||
135 | let mut processed_lines = Vec::new(); | ||
136 | let mut in_code_block = false; | ||
137 | for line in docs.lines() { | ||
138 | if line.starts_with("```") { | ||
139 | in_code_block = !in_code_block; | ||
140 | } | ||
141 | |||
142 | let line = if in_code_block && line.starts_with("```") && !line.contains("rust") { | ||
143 | "```rust".into() | ||
144 | } else { | ||
145 | line.to_string() | ||
146 | }; | ||
147 | |||
148 | processed_lines.push(line); | ||
149 | } | ||
150 | |||
151 | if !processed_lines.is_empty() { | ||
152 | doc = Some(processed_lines.join("\n")); | ||
153 | } | ||
154 | } | ||
155 | |||
156 | Some(CallInfo { | ||
157 | parameters: param_list(node), | ||
158 | label: label.trim().to_owned(), | ||
159 | doc, | ||
160 | active_parameter: None, | ||
161 | }) | ||
162 | } | ||
163 | } | ||
164 | |||
165 | fn extract_doc_comments(node: &ast::FnDef) -> Option<(TextRange, String)> { | ||
166 | if node.doc_comments().count() == 0 { | ||
167 | return None; | ||
168 | } | ||
169 | |||
170 | let comment_text = node.doc_comment_text(); | ||
171 | |||
172 | let (begin, end) = node | ||
173 | .doc_comments() | ||
174 | .map(|comment| comment.syntax().range()) | ||
175 | .map(|range| (range.start().to_usize(), range.end().to_usize())) | ||
176 | .fold((std::usize::MAX, std::usize::MIN), |acc, range| { | ||
177 | (min(acc.0, range.0), max(acc.1, range.1)) | ||
178 | }); | ||
179 | |||
180 | let range = TextRange::from_to(TextUnit::from_usize(begin), TextUnit::from_usize(end)); | ||
181 | |||
182 | Some((range, comment_text)) | ||
183 | } | ||
184 | |||
185 | fn param_list(node: &ast::FnDef) -> Vec<String> { | ||
186 | let mut res = vec![]; | ||
187 | if let Some(param_list) = node.param_list() { | ||
188 | if let Some(self_param) = param_list.self_param() { | ||
189 | res.push(self_param.syntax().text().to_string()) | ||
190 | } | ||
191 | |||
192 | // Maybe use param.pat here? See if we can just extract the name? | ||
193 | //res.extend(param_list.params().map(|p| p.syntax().text().to_string())); | ||
194 | res.extend( | ||
195 | param_list | ||
196 | .params() | ||
197 | .filter_map(|p| p.pat()) | ||
198 | .map(|pat| pat.syntax().text().to_string()), | ||
199 | ); | ||
200 | } | ||
201 | res | ||
202 | } | ||
203 | |||
204 | #[cfg(test)] | ||
205 | mod tests { | ||
206 | use super::*; | ||
207 | |||
208 | use crate::mock_analysis::single_file_with_position; | ||
209 | |||
210 | fn call_info(text: &str) -> CallInfo { | ||
211 | let (analysis, position) = single_file_with_position(text); | ||
212 | analysis.call_info(position).unwrap().unwrap() | ||
213 | } | ||
214 | |||
215 | #[test] | ||
216 | fn test_fn_signature_two_args_first() { | ||
217 | let info = call_info( | ||
218 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
219 | fn bar() { foo(<|>3, ); }"#, | ||
220 | ); | ||
221 | |||
222 | assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); | ||
223 | assert_eq!(info.active_parameter, Some(0)); | ||
224 | } | ||
225 | |||
226 | #[test] | ||
227 | fn test_fn_signature_two_args_second() { | ||
228 | let info = call_info( | ||
229 | r#"fn foo(x: u32, y: u32) -> u32 {x + y} | ||
230 | fn bar() { foo(3, <|>); }"#, | ||
231 | ); | ||
232 | |||
233 | assert_eq!(info.parameters, vec!("x".to_string(), "y".to_string())); | ||
234 | assert_eq!(info.active_parameter, Some(1)); | ||
235 | } | ||
236 | |||
237 | #[test] | ||
238 | fn test_fn_signature_for_impl() { | ||
239 | let info = call_info( | ||
240 | r#"struct F; impl F { pub fn new() { F{}} } | ||
241 | fn bar() {let _ : F = F::new(<|>);}"#, | ||
242 | ); | ||
243 | |||
244 | assert_eq!(info.parameters, Vec::<String>::new()); | ||
245 | assert_eq!(info.active_parameter, None); | ||
246 | } | ||
247 | |||
248 | #[test] | ||
249 | fn test_fn_signature_for_method_self() { | ||
250 | let info = call_info( | ||
251 | r#"struct F; | ||
252 | impl F { | ||
253 | pub fn new() -> F{ | ||
254 | F{} | ||
255 | } | ||
256 | |||
257 | pub fn do_it(&self) {} | ||
258 | } | ||
259 | |||
260 | fn bar() { | ||
261 | let f : F = F::new(); | ||
262 | f.do_it(<|>); | ||
263 | }"#, | ||
264 | ); | ||
265 | |||
266 | assert_eq!(info.parameters, vec!["&self".to_string()]); | ||
267 | assert_eq!(info.active_parameter, None); | ||
268 | } | ||
269 | |||
270 | #[test] | ||
271 | fn test_fn_signature_for_method_with_arg() { | ||
272 | let info = call_info( | ||
273 | r#"struct F; | ||
274 | impl F { | ||
275 | pub fn new() -> F{ | ||
276 | F{} | ||
277 | } | ||
278 | |||
279 | pub fn do_it(&self, x: i32) {} | ||
280 | } | ||
281 | |||
282 | fn bar() { | ||
283 | let f : F = F::new(); | ||
284 | f.do_it(<|>); | ||
285 | }"#, | ||
286 | ); | ||
287 | |||
288 | assert_eq!(info.parameters, vec!["&self".to_string(), "x".to_string()]); | ||
289 | assert_eq!(info.active_parameter, Some(1)); | ||
290 | } | ||
291 | |||
292 | #[test] | ||
293 | fn test_fn_signature_with_docs_simple() { | ||
294 | let info = call_info( | ||
295 | r#" | ||
296 | /// test | ||
297 | // non-doc-comment | ||
298 | fn foo(j: u32) -> u32 { | ||
299 | j | ||
300 | } | ||
301 | |||
302 | fn bar() { | ||
303 | let _ = foo(<|>); | ||
304 | } | ||
305 | "#, | ||
306 | ); | ||
307 | |||
308 | assert_eq!(info.parameters, vec!["j".to_string()]); | ||
309 | assert_eq!(info.active_parameter, Some(0)); | ||
310 | assert_eq!(info.label, "fn foo(j: u32) -> u32".to_string()); | ||
311 | assert_eq!(info.doc, Some("test".into())); | ||
312 | } | ||
313 | |||
314 | #[test] | ||
315 | fn test_fn_signature_with_docs() { | ||
316 | let info = call_info( | ||
317 | r#" | ||
318 | /// Adds one to the number given. | ||
319 | /// | ||
320 | /// # Examples | ||
321 | /// | ||
322 | /// ``` | ||
323 | /// let five = 5; | ||
324 | /// | ||
325 | /// assert_eq!(6, my_crate::add_one(5)); | ||
326 | /// ``` | ||
327 | pub fn add_one(x: i32) -> i32 { | ||
328 | x + 1 | ||
329 | } | ||
330 | |||
331 | pub fn do() { | ||
332 | add_one(<|> | ||
333 | }"#, | ||
334 | ); | ||
335 | |||
336 | assert_eq!(info.parameters, vec!["x".to_string()]); | ||
337 | assert_eq!(info.active_parameter, Some(0)); | ||
338 | assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); | ||
339 | assert_eq!( | ||
340 | info.doc, | ||
341 | Some( | ||
342 | r#"Adds one to the number given. | ||
343 | |||
344 | # Examples | ||
345 | |||
346 | ```rust | ||
347 | let five = 5; | ||
348 | |||
349 | assert_eq!(6, my_crate::add_one(5)); | ||
350 | ```"# | ||
351 | .into() | ||
352 | ) | ||
353 | ); | ||
354 | } | ||
355 | |||
356 | #[test] | ||
357 | fn test_fn_signature_with_docs_impl() { | ||
358 | let info = call_info( | ||
359 | r#" | ||
360 | struct addr; | ||
361 | impl addr { | ||
362 | /// Adds one to the number given. | ||
363 | /// | ||
364 | /// # Examples | ||
365 | /// | ||
366 | /// ``` | ||
367 | /// let five = 5; | ||
368 | /// | ||
369 | /// assert_eq!(6, my_crate::add_one(5)); | ||
370 | /// ``` | ||
371 | pub fn add_one(x: i32) -> i32 { | ||
372 | x + 1 | ||
373 | } | ||
374 | } | ||
375 | |||
376 | pub fn do_it() { | ||
377 | addr {}; | ||
378 | addr::add_one(<|>); | ||
379 | }"#, | ||
380 | ); | ||
381 | |||
382 | assert_eq!(info.parameters, vec!["x".to_string()]); | ||
383 | assert_eq!(info.active_parameter, Some(0)); | ||
384 | assert_eq!(info.label, "pub fn add_one(x: i32) -> i32".to_string()); | ||
385 | assert_eq!( | ||
386 | info.doc, | ||
387 | Some( | ||
388 | r#"Adds one to the number given. | ||
389 | |||
390 | # Examples | ||
391 | |||
392 | ```rust | ||
393 | let five = 5; | ||
394 | |||
395 | assert_eq!(6, my_crate::add_one(5)); | ||
396 | ```"# | ||
397 | .into() | ||
398 | ) | ||
399 | ); | ||
400 | } | ||
401 | |||
402 | #[test] | ||
403 | fn test_fn_signature_with_docs_from_actix() { | ||
404 | let info = call_info( | ||
405 | r#" | ||
406 | pub trait WriteHandler<E> | ||
407 | where | ||
408 | Self: Actor, | ||
409 | Self::Context: ActorContext, | ||
410 | { | ||
411 | /// Method is called when writer emits error. | ||
412 | /// | ||
413 | /// If this method returns `ErrorAction::Continue` writer processing | ||
414 | /// continues otherwise stream processing stops. | ||
415 | fn error(&mut self, err: E, ctx: &mut Self::Context) -> Running { | ||
416 | Running::Stop | ||
417 | } | ||
418 | |||
419 | /// Method is called when writer finishes. | ||
420 | /// | ||
421 | /// By default this method stops actor's `Context`. | ||
422 | fn finished(&mut self, ctx: &mut Self::Context) { | ||
423 | ctx.stop() | ||
424 | } | ||
425 | } | ||
426 | |||
427 | pub fn foo() { | ||
428 | WriteHandler r; | ||
429 | r.finished(<|>); | ||
430 | } | ||
431 | |||
432 | "#, | ||
433 | ); | ||
434 | |||
435 | assert_eq!( | ||
436 | info.parameters, | ||
437 | vec!["&mut self".to_string(), "ctx".to_string()] | ||
438 | ); | ||
439 | assert_eq!(info.active_parameter, Some(1)); | ||
440 | assert_eq!( | ||
441 | info.doc, | ||
442 | Some( | ||
443 | r#"Method is called when writer finishes. | ||
444 | |||
445 | By default this method stops actor's `Context`."# | ||
446 | .into() | ||
447 | ) | ||
448 | ); | ||
449 | } | ||
450 | |||
451 | } | ||