diff options
Diffstat (limited to 'crates/ra_ide')
-rw-r--r-- | crates/ra_ide/Cargo.toml | 1 | ||||
-rw-r--r-- | crates/ra_ide/src/lib.rs | 2 | ||||
-rw-r--r-- | crates/ra_ide/src/ssr.rs | 563 |
3 files changed, 11 insertions, 555 deletions
diff --git a/crates/ra_ide/Cargo.toml b/crates/ra_ide/Cargo.toml index 05c940605..bbc6a5c9b 100644 --- a/crates/ra_ide/Cargo.toml +++ b/crates/ra_ide/Cargo.toml | |||
@@ -29,6 +29,7 @@ ra_fmt = { path = "../ra_fmt" } | |||
29 | ra_prof = { path = "../ra_prof" } | 29 | ra_prof = { path = "../ra_prof" } |
30 | test_utils = { path = "../test_utils" } | 30 | test_utils = { path = "../test_utils" } |
31 | ra_assists = { path = "../ra_assists" } | 31 | ra_assists = { path = "../ra_assists" } |
32 | ra_ssr = { path = "../ra_ssr" } | ||
32 | 33 | ||
33 | # ra_ide should depend only on the top-level `hir` package. if you need | 34 | # ra_ide should depend only on the top-level `hir` package. if you need |
34 | # something from some `hir_xxx` subpackage, reexport the API via `hir`. | 35 | # something from some `hir_xxx` subpackage, reexport the API via `hir`. |
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs index be9ab62c0..47823718f 100644 --- a/crates/ra_ide/src/lib.rs +++ b/crates/ra_ide/src/lib.rs | |||
@@ -70,7 +70,6 @@ pub use crate::{ | |||
70 | inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, | 70 | inlay_hints::{InlayHint, InlayHintsConfig, InlayKind}, |
71 | references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult}, | 71 | references::{Declaration, Reference, ReferenceAccess, ReferenceKind, ReferenceSearchResult}, |
72 | runnables::{Runnable, RunnableKind, TestId}, | 72 | runnables::{Runnable, RunnableKind, TestId}, |
73 | ssr::SsrError, | ||
74 | syntax_highlighting::{ | 73 | syntax_highlighting::{ |
75 | Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange, | 74 | Highlight, HighlightModifier, HighlightModifiers, HighlightTag, HighlightedRange, |
76 | }, | 75 | }, |
@@ -89,6 +88,7 @@ pub use ra_ide_db::{ | |||
89 | symbol_index::Query, | 88 | symbol_index::Query, |
90 | RootDatabase, | 89 | RootDatabase, |
91 | }; | 90 | }; |
91 | pub use ra_ssr::SsrError; | ||
92 | pub use ra_text_edit::{Indel, TextEdit}; | 92 | pub use ra_text_edit::{Indel, TextEdit}; |
93 | 93 | ||
94 | pub type Cancelable<T> = Result<T, Canceled>; | 94 | pub type Cancelable<T> = Result<T, Canceled>; |
diff --git a/crates/ra_ide/src/ssr.rs b/crates/ra_ide/src/ssr.rs index 762aab962..59c230f6c 100644 --- a/crates/ra_ide/src/ssr.rs +++ b/crates/ra_ide/src/ssr.rs | |||
@@ -1,31 +1,12 @@ | |||
1 | use std::{collections::HashMap, iter::once, str::FromStr}; | 1 | use ra_db::SourceDatabaseExt; |
2 | |||
3 | use ra_db::{SourceDatabase, SourceDatabaseExt}; | ||
4 | use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase}; | 2 | use ra_ide_db::{symbol_index::SymbolsDatabase, RootDatabase}; |
5 | use ra_syntax::ast::{ | ||
6 | make::try_expr_from_text, ArgList, AstToken, CallExpr, Comment, Expr, MethodCallExpr, | ||
7 | RecordField, RecordLit, | ||
8 | }; | ||
9 | use ra_syntax::{AstNode, SyntaxElement, SyntaxKind, SyntaxNode}; | ||
10 | use ra_text_edit::{TextEdit, TextEditBuilder}; | ||
11 | use rustc_hash::FxHashMap; | ||
12 | 3 | ||
13 | use crate::SourceFileEdit; | 4 | use crate::SourceFileEdit; |
14 | 5 | use ra_ssr::{MatchFinder, SsrError, SsrRule}; | |
15 | #[derive(Debug, PartialEq)] | ||
16 | pub struct SsrError(String); | ||
17 | |||
18 | impl std::fmt::Display for SsrError { | ||
19 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
20 | write!(f, "Parse error: {}", self.0) | ||
21 | } | ||
22 | } | ||
23 | |||
24 | impl std::error::Error for SsrError {} | ||
25 | 6 | ||
26 | // Feature: Structural Seach and Replace | 7 | // Feature: Structural Seach and Replace |
27 | // | 8 | // |
28 | // Search and replace with named wildcards that will match any expression. | 9 | // Search and replace with named wildcards that will match any expression, type, path, pattern or item. |
29 | // The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`. | 10 | // The syntax for a structural search replace command is `<search_pattern> ==>> <replace_pattern>`. |
30 | // A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement. | 11 | // A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement. |
31 | // Available via the command `rust-analyzer.ssr`. | 12 | // Available via the command `rust-analyzer.ssr`. |
@@ -46,550 +27,24 @@ impl std::error::Error for SsrError {} | |||
46 | // | VS Code | **Rust Analyzer: Structural Search Replace** | 27 | // | VS Code | **Rust Analyzer: Structural Search Replace** |
47 | // |=== | 28 | // |=== |
48 | pub fn parse_search_replace( | 29 | pub fn parse_search_replace( |
49 | query: &str, | 30 | rule: &str, |
50 | parse_only: bool, | 31 | parse_only: bool, |
51 | db: &RootDatabase, | 32 | db: &RootDatabase, |
52 | ) -> Result<Vec<SourceFileEdit>, SsrError> { | 33 | ) -> Result<Vec<SourceFileEdit>, SsrError> { |
53 | let mut edits = vec![]; | 34 | let mut edits = vec![]; |
54 | let query: SsrQuery = query.parse()?; | 35 | let rule: SsrRule = rule.parse()?; |
55 | if parse_only { | 36 | if parse_only { |
56 | return Ok(edits); | 37 | return Ok(edits); |
57 | } | 38 | } |
39 | let mut match_finder = MatchFinder::new(db); | ||
40 | match_finder.add_rule(rule); | ||
58 | for &root in db.local_roots().iter() { | 41 | for &root in db.local_roots().iter() { |
59 | let sr = db.source_root(root); | 42 | let sr = db.source_root(root); |
60 | for file_id in sr.walk() { | 43 | for file_id in sr.walk() { |
61 | let matches = find(&query.pattern, db.parse(file_id).tree().syntax()); | 44 | if let Some(edit) = match_finder.edits_for_file(file_id) { |
62 | if !matches.matches.is_empty() { | 45 | edits.push(SourceFileEdit { file_id, edit }); |
63 | edits.push(SourceFileEdit { file_id, edit: replace(&matches, &query.template) }); | ||
64 | } | 46 | } |
65 | } | 47 | } |
66 | } | 48 | } |
67 | Ok(edits) | 49 | Ok(edits) |
68 | } | 50 | } |
69 | |||
70 | #[derive(Debug)] | ||
71 | struct SsrQuery { | ||
72 | pattern: SsrPattern, | ||
73 | template: SsrTemplate, | ||
74 | } | ||
75 | |||
76 | #[derive(Debug)] | ||
77 | struct SsrPattern { | ||
78 | pattern: SyntaxNode, | ||
79 | vars: Vec<Var>, | ||
80 | } | ||
81 | |||
82 | /// Represents a `$var` in an SSR query. | ||
83 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] | ||
84 | struct Var(String); | ||
85 | |||
86 | #[derive(Debug)] | ||
87 | struct SsrTemplate { | ||
88 | template: SyntaxNode, | ||
89 | placeholders: FxHashMap<SyntaxNode, Var>, | ||
90 | } | ||
91 | |||
92 | type Binding = HashMap<Var, SyntaxNode>; | ||
93 | |||
94 | #[derive(Debug)] | ||
95 | struct Match { | ||
96 | place: SyntaxNode, | ||
97 | binding: Binding, | ||
98 | ignored_comments: Vec<Comment>, | ||
99 | } | ||
100 | |||
101 | #[derive(Debug)] | ||
102 | struct SsrMatches { | ||
103 | matches: Vec<Match>, | ||
104 | } | ||
105 | |||
106 | impl FromStr for SsrQuery { | ||
107 | type Err = SsrError; | ||
108 | |||
109 | fn from_str(query: &str) -> Result<SsrQuery, SsrError> { | ||
110 | let mut it = query.split("==>>"); | ||
111 | let pattern = it.next().expect("at least empty string").trim(); | ||
112 | let mut template = it | ||
113 | .next() | ||
114 | .ok_or_else(|| SsrError("Cannot find delemiter `==>>`".into()))? | ||
115 | .trim() | ||
116 | .to_string(); | ||
117 | if it.next().is_some() { | ||
118 | return Err(SsrError("More than one delimiter found".into())); | ||
119 | } | ||
120 | let mut vars = vec![]; | ||
121 | let mut it = pattern.split('$'); | ||
122 | let mut pattern = it.next().expect("something").to_string(); | ||
123 | |||
124 | for part in it.map(split_by_var) { | ||
125 | let (var, remainder) = part?; | ||
126 | let new_var = create_name(var, &mut vars)?; | ||
127 | pattern.push_str(new_var); | ||
128 | pattern.push_str(remainder); | ||
129 | template = replace_in_template(template, var, new_var); | ||
130 | } | ||
131 | |||
132 | let template = try_expr_from_text(&template) | ||
133 | .ok_or(SsrError("Template is not an expression".into()))? | ||
134 | .syntax() | ||
135 | .clone(); | ||
136 | let mut placeholders = FxHashMap::default(); | ||
137 | |||
138 | traverse(&template, &mut |n| { | ||
139 | if let Some(v) = vars.iter().find(|v| v.0.as_str() == n.text()) { | ||
140 | placeholders.insert(n.clone(), v.clone()); | ||
141 | false | ||
142 | } else { | ||
143 | true | ||
144 | } | ||
145 | }); | ||
146 | |||
147 | let pattern = SsrPattern { | ||
148 | pattern: try_expr_from_text(&pattern) | ||
149 | .ok_or(SsrError("Pattern is not an expression".into()))? | ||
150 | .syntax() | ||
151 | .clone(), | ||
152 | vars, | ||
153 | }; | ||
154 | let template = SsrTemplate { template, placeholders }; | ||
155 | Ok(SsrQuery { pattern, template }) | ||
156 | } | ||
157 | } | ||
158 | |||
159 | fn traverse(node: &SyntaxNode, go: &mut impl FnMut(&SyntaxNode) -> bool) { | ||
160 | if !go(node) { | ||
161 | return; | ||
162 | } | ||
163 | for ref child in node.children() { | ||
164 | traverse(child, go); | ||
165 | } | ||
166 | } | ||
167 | |||
168 | fn split_by_var(s: &str) -> Result<(&str, &str), SsrError> { | ||
169 | let end_of_name = s.find(|c| !char::is_ascii_alphanumeric(&c)).unwrap_or_else(|| s.len()); | ||
170 | let name = &s[..end_of_name]; | ||
171 | is_name(name)?; | ||
172 | Ok((name, &s[end_of_name..])) | ||
173 | } | ||
174 | |||
175 | fn is_name(s: &str) -> Result<(), SsrError> { | ||
176 | if s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { | ||
177 | Ok(()) | ||
178 | } else { | ||
179 | Err(SsrError("Name can contain only alphanumerics and _".into())) | ||
180 | } | ||
181 | } | ||
182 | |||
183 | fn replace_in_template(template: String, var: &str, new_var: &str) -> String { | ||
184 | let name = format!("${}", var); | ||
185 | template.replace(&name, new_var) | ||
186 | } | ||
187 | |||
188 | fn create_name<'a>(name: &str, vars: &'a mut Vec<Var>) -> Result<&'a str, SsrError> { | ||
189 | let sanitized_name = format!("__search_pattern_{}", name); | ||
190 | if vars.iter().any(|a| a.0 == sanitized_name) { | ||
191 | return Err(SsrError(format!("Name `{}` repeats more than once", name))); | ||
192 | } | ||
193 | vars.push(Var(sanitized_name)); | ||
194 | Ok(&vars.last().unwrap().0) | ||
195 | } | ||
196 | |||
197 | fn find(pattern: &SsrPattern, code: &SyntaxNode) -> SsrMatches { | ||
198 | fn check_record_lit( | ||
199 | pattern: RecordLit, | ||
200 | code: RecordLit, | ||
201 | placeholders: &[Var], | ||
202 | match_: Match, | ||
203 | ) -> Option<Match> { | ||
204 | let match_ = check_opt_nodes(pattern.path(), code.path(), placeholders, match_)?; | ||
205 | |||
206 | let mut pattern_fields: Vec<RecordField> = | ||
207 | pattern.record_field_list().map(|x| x.fields().collect()).unwrap_or_default(); | ||
208 | let mut code_fields: Vec<RecordField> = | ||
209 | code.record_field_list().map(|x| x.fields().collect()).unwrap_or_default(); | ||
210 | |||
211 | if pattern_fields.len() != code_fields.len() { | ||
212 | return None; | ||
213 | } | ||
214 | |||
215 | let by_name = |a: &RecordField, b: &RecordField| { | ||
216 | a.name_ref() | ||
217 | .map(|x| x.syntax().text().to_string()) | ||
218 | .cmp(&b.name_ref().map(|x| x.syntax().text().to_string())) | ||
219 | }; | ||
220 | pattern_fields.sort_by(by_name); | ||
221 | code_fields.sort_by(by_name); | ||
222 | |||
223 | pattern_fields.into_iter().zip(code_fields.into_iter()).fold( | ||
224 | Some(match_), | ||
225 | |accum, (a, b)| { | ||
226 | accum.and_then(|match_| check_opt_nodes(Some(a), Some(b), placeholders, match_)) | ||
227 | }, | ||
228 | ) | ||
229 | } | ||
230 | |||
231 | fn check_call_and_method_call( | ||
232 | pattern: CallExpr, | ||
233 | code: MethodCallExpr, | ||
234 | placeholders: &[Var], | ||
235 | match_: Match, | ||
236 | ) -> Option<Match> { | ||
237 | let (pattern_name, pattern_type_args) = if let Some(Expr::PathExpr(path_exr)) = | ||
238 | pattern.expr() | ||
239 | { | ||
240 | let segment = path_exr.path().and_then(|p| p.segment()); | ||
241 | (segment.as_ref().and_then(|s| s.name_ref()), segment.and_then(|s| s.type_arg_list())) | ||
242 | } else { | ||
243 | (None, None) | ||
244 | }; | ||
245 | let match_ = check_opt_nodes(pattern_name, code.name_ref(), placeholders, match_)?; | ||
246 | let match_ = | ||
247 | check_opt_nodes(pattern_type_args, code.type_arg_list(), placeholders, match_)?; | ||
248 | let pattern_args = pattern.syntax().children().find_map(ArgList::cast)?.args(); | ||
249 | let code_args = code.syntax().children().find_map(ArgList::cast)?.args(); | ||
250 | let code_args = once(code.expr()?).chain(code_args); | ||
251 | check_iter(pattern_args, code_args, placeholders, match_) | ||
252 | } | ||
253 | |||
254 | fn check_method_call_and_call( | ||
255 | pattern: MethodCallExpr, | ||
256 | code: CallExpr, | ||
257 | placeholders: &[Var], | ||
258 | match_: Match, | ||
259 | ) -> Option<Match> { | ||
260 | let (code_name, code_type_args) = if let Some(Expr::PathExpr(path_exr)) = code.expr() { | ||
261 | let segment = path_exr.path().and_then(|p| p.segment()); | ||
262 | (segment.as_ref().and_then(|s| s.name_ref()), segment.and_then(|s| s.type_arg_list())) | ||
263 | } else { | ||
264 | (None, None) | ||
265 | }; | ||
266 | let match_ = check_opt_nodes(pattern.name_ref(), code_name, placeholders, match_)?; | ||
267 | let match_ = | ||
268 | check_opt_nodes(pattern.type_arg_list(), code_type_args, placeholders, match_)?; | ||
269 | let code_args = code.syntax().children().find_map(ArgList::cast)?.args(); | ||
270 | let pattern_args = pattern.syntax().children().find_map(ArgList::cast)?.args(); | ||
271 | let pattern_args = once(pattern.expr()?).chain(pattern_args); | ||
272 | check_iter(pattern_args, code_args, placeholders, match_) | ||
273 | } | ||
274 | |||
275 | fn check_opt_nodes( | ||
276 | pattern: Option<impl AstNode>, | ||
277 | code: Option<impl AstNode>, | ||
278 | placeholders: &[Var], | ||
279 | match_: Match, | ||
280 | ) -> Option<Match> { | ||
281 | match (pattern, code) { | ||
282 | (Some(pattern), Some(code)) => check( | ||
283 | &pattern.syntax().clone().into(), | ||
284 | &code.syntax().clone().into(), | ||
285 | placeholders, | ||
286 | match_, | ||
287 | ), | ||
288 | (None, None) => Some(match_), | ||
289 | _ => None, | ||
290 | } | ||
291 | } | ||
292 | |||
293 | fn check_iter<T, I1, I2>( | ||
294 | mut pattern: I1, | ||
295 | mut code: I2, | ||
296 | placeholders: &[Var], | ||
297 | match_: Match, | ||
298 | ) -> Option<Match> | ||
299 | where | ||
300 | T: AstNode, | ||
301 | I1: Iterator<Item = T>, | ||
302 | I2: Iterator<Item = T>, | ||
303 | { | ||
304 | pattern | ||
305 | .by_ref() | ||
306 | .zip(code.by_ref()) | ||
307 | .fold(Some(match_), |accum, (a, b)| { | ||
308 | accum.and_then(|match_| { | ||
309 | check( | ||
310 | &a.syntax().clone().into(), | ||
311 | &b.syntax().clone().into(), | ||
312 | placeholders, | ||
313 | match_, | ||
314 | ) | ||
315 | }) | ||
316 | }) | ||
317 | .filter(|_| pattern.next().is_none() && code.next().is_none()) | ||
318 | } | ||
319 | |||
320 | fn check( | ||
321 | pattern: &SyntaxElement, | ||
322 | code: &SyntaxElement, | ||
323 | placeholders: &[Var], | ||
324 | mut match_: Match, | ||
325 | ) -> Option<Match> { | ||
326 | match (&pattern, &code) { | ||
327 | (SyntaxElement::Token(pattern), SyntaxElement::Token(code)) => { | ||
328 | if pattern.text() == code.text() { | ||
329 | Some(match_) | ||
330 | } else { | ||
331 | None | ||
332 | } | ||
333 | } | ||
334 | (SyntaxElement::Node(pattern), SyntaxElement::Node(code)) => { | ||
335 | if placeholders.iter().any(|n| n.0.as_str() == pattern.text()) { | ||
336 | match_.binding.insert(Var(pattern.text().to_string()), code.clone()); | ||
337 | Some(match_) | ||
338 | } else { | ||
339 | if let (Some(pattern), Some(code)) = | ||
340 | (RecordLit::cast(pattern.clone()), RecordLit::cast(code.clone())) | ||
341 | { | ||
342 | check_record_lit(pattern, code, placeholders, match_) | ||
343 | } else if let (Some(pattern), Some(code)) = | ||
344 | (CallExpr::cast(pattern.clone()), MethodCallExpr::cast(code.clone())) | ||
345 | { | ||
346 | check_call_and_method_call(pattern, code, placeholders, match_) | ||
347 | } else if let (Some(pattern), Some(code)) = | ||
348 | (MethodCallExpr::cast(pattern.clone()), CallExpr::cast(code.clone())) | ||
349 | { | ||
350 | check_method_call_and_call(pattern, code, placeholders, match_) | ||
351 | } else { | ||
352 | let mut pattern_children = pattern | ||
353 | .children_with_tokens() | ||
354 | .filter(|element| !element.kind().is_trivia()); | ||
355 | let mut code_children = code | ||
356 | .children_with_tokens() | ||
357 | .filter(|element| !element.kind().is_trivia()); | ||
358 | let new_ignored_comments = | ||
359 | code.children_with_tokens().filter_map(|element| { | ||
360 | element.as_token().and_then(|token| Comment::cast(token.clone())) | ||
361 | }); | ||
362 | match_.ignored_comments.extend(new_ignored_comments); | ||
363 | pattern_children | ||
364 | .by_ref() | ||
365 | .zip(code_children.by_ref()) | ||
366 | .fold(Some(match_), |accum, (a, b)| { | ||
367 | accum.and_then(|match_| check(&a, &b, placeholders, match_)) | ||
368 | }) | ||
369 | .filter(|_| { | ||
370 | pattern_children.next().is_none() && code_children.next().is_none() | ||
371 | }) | ||
372 | } | ||
373 | } | ||
374 | } | ||
375 | _ => None, | ||
376 | } | ||
377 | } | ||
378 | let kind = pattern.pattern.kind(); | ||
379 | let matches = code | ||
380 | .descendants() | ||
381 | .filter(|n| { | ||
382 | n.kind() == kind | ||
383 | || (kind == SyntaxKind::CALL_EXPR && n.kind() == SyntaxKind::METHOD_CALL_EXPR) | ||
384 | || (kind == SyntaxKind::METHOD_CALL_EXPR && n.kind() == SyntaxKind::CALL_EXPR) | ||
385 | }) | ||
386 | .filter_map(|code| { | ||
387 | let match_ = | ||
388 | Match { place: code.clone(), binding: HashMap::new(), ignored_comments: vec![] }; | ||
389 | check(&pattern.pattern.clone().into(), &code.into(), &pattern.vars, match_) | ||
390 | }) | ||
391 | .collect(); | ||
392 | SsrMatches { matches } | ||
393 | } | ||
394 | |||
395 | fn replace(matches: &SsrMatches, template: &SsrTemplate) -> TextEdit { | ||
396 | let mut builder = TextEditBuilder::default(); | ||
397 | for match_ in &matches.matches { | ||
398 | builder.replace( | ||
399 | match_.place.text_range(), | ||
400 | render_replace(&match_.binding, &match_.ignored_comments, template), | ||
401 | ); | ||
402 | } | ||
403 | builder.finish() | ||
404 | } | ||
405 | |||
406 | fn render_replace( | ||
407 | binding: &Binding, | ||
408 | ignored_comments: &Vec<Comment>, | ||
409 | template: &SsrTemplate, | ||
410 | ) -> String { | ||
411 | let edit = { | ||
412 | let mut builder = TextEditBuilder::default(); | ||
413 | for element in template.template.descendants() { | ||
414 | if let Some(var) = template.placeholders.get(&element) { | ||
415 | builder.replace(element.text_range(), binding[var].to_string()) | ||
416 | } | ||
417 | } | ||
418 | for comment in ignored_comments { | ||
419 | builder.insert(template.template.text_range().end(), comment.syntax().to_string()) | ||
420 | } | ||
421 | builder.finish() | ||
422 | }; | ||
423 | |||
424 | let mut text = template.template.text().to_string(); | ||
425 | edit.apply(&mut text); | ||
426 | text | ||
427 | } | ||
428 | |||
429 | #[cfg(test)] | ||
430 | mod tests { | ||
431 | use super::*; | ||
432 | use ra_syntax::SourceFile; | ||
433 | |||
434 | fn parse_error_text(query: &str) -> String { | ||
435 | format!("{}", query.parse::<SsrQuery>().unwrap_err()) | ||
436 | } | ||
437 | |||
438 | #[test] | ||
439 | fn parser_happy_case() { | ||
440 | let result: SsrQuery = "foo($a, $b) ==>> bar($b, $a)".parse().unwrap(); | ||
441 | assert_eq!(&result.pattern.pattern.text(), "foo(__search_pattern_a, __search_pattern_b)"); | ||
442 | assert_eq!(result.pattern.vars.len(), 2); | ||
443 | assert_eq!(result.pattern.vars[0].0, "__search_pattern_a"); | ||
444 | assert_eq!(result.pattern.vars[1].0, "__search_pattern_b"); | ||
445 | assert_eq!(&result.template.template.text(), "bar(__search_pattern_b, __search_pattern_a)"); | ||
446 | } | ||
447 | |||
448 | #[test] | ||
449 | fn parser_empty_query() { | ||
450 | assert_eq!(parse_error_text(""), "Parse error: Cannot find delemiter `==>>`"); | ||
451 | } | ||
452 | |||
453 | #[test] | ||
454 | fn parser_no_delimiter() { | ||
455 | assert_eq!(parse_error_text("foo()"), "Parse error: Cannot find delemiter `==>>`"); | ||
456 | } | ||
457 | |||
458 | #[test] | ||
459 | fn parser_two_delimiters() { | ||
460 | assert_eq!( | ||
461 | parse_error_text("foo() ==>> a ==>> b "), | ||
462 | "Parse error: More than one delimiter found" | ||
463 | ); | ||
464 | } | ||
465 | |||
466 | #[test] | ||
467 | fn parser_repeated_name() { | ||
468 | assert_eq!( | ||
469 | parse_error_text("foo($a, $a) ==>>"), | ||
470 | "Parse error: Name `a` repeats more than once" | ||
471 | ); | ||
472 | } | ||
473 | |||
474 | #[test] | ||
475 | fn parser_invlid_pattern() { | ||
476 | assert_eq!(parse_error_text(" ==>> ()"), "Parse error: Pattern is not an expression"); | ||
477 | } | ||
478 | |||
479 | #[test] | ||
480 | fn parser_invlid_template() { | ||
481 | assert_eq!(parse_error_text("() ==>> )"), "Parse error: Template is not an expression"); | ||
482 | } | ||
483 | |||
484 | #[test] | ||
485 | fn parse_match_replace() { | ||
486 | let query: SsrQuery = "foo($x) ==>> bar($x)".parse().unwrap(); | ||
487 | let input = "fn main() { foo(1+2); }"; | ||
488 | |||
489 | let code = SourceFile::parse(input).tree(); | ||
490 | let matches = find(&query.pattern, code.syntax()); | ||
491 | assert_eq!(matches.matches.len(), 1); | ||
492 | assert_eq!(matches.matches[0].place.text(), "foo(1+2)"); | ||
493 | assert_eq!(matches.matches[0].binding.len(), 1); | ||
494 | assert_eq!( | ||
495 | matches.matches[0].binding[&Var("__search_pattern_x".to_string())].text(), | ||
496 | "1+2" | ||
497 | ); | ||
498 | |||
499 | let edit = replace(&matches, &query.template); | ||
500 | let mut after = input.to_string(); | ||
501 | edit.apply(&mut after); | ||
502 | assert_eq!(after, "fn main() { bar(1+2); }"); | ||
503 | } | ||
504 | |||
505 | fn assert_ssr_transform(query: &str, input: &str, result: &str) { | ||
506 | let query: SsrQuery = query.parse().unwrap(); | ||
507 | let code = SourceFile::parse(input).tree(); | ||
508 | let matches = find(&query.pattern, code.syntax()); | ||
509 | let edit = replace(&matches, &query.template); | ||
510 | let mut after = input.to_string(); | ||
511 | edit.apply(&mut after); | ||
512 | assert_eq!(after, result); | ||
513 | } | ||
514 | |||
515 | #[test] | ||
516 | fn ssr_function_to_method() { | ||
517 | assert_ssr_transform( | ||
518 | "my_function($a, $b) ==>> ($a).my_method($b)", | ||
519 | "loop { my_function( other_func(x, y), z + w) }", | ||
520 | "loop { (other_func(x, y)).my_method(z + w) }", | ||
521 | ) | ||
522 | } | ||
523 | |||
524 | #[test] | ||
525 | fn ssr_nested_function() { | ||
526 | assert_ssr_transform( | ||
527 | "foo($a, $b, $c) ==>> bar($c, baz($a, $b))", | ||
528 | "fn main { foo (x + value.method(b), x+y-z, true && false) }", | ||
529 | "fn main { bar(true && false, baz(x + value.method(b), x+y-z)) }", | ||
530 | ) | ||
531 | } | ||
532 | |||
533 | #[test] | ||
534 | fn ssr_expected_spacing() { | ||
535 | assert_ssr_transform( | ||
536 | "foo($x) + bar() ==>> bar($x)", | ||
537 | "fn main() { foo(5) + bar() }", | ||
538 | "fn main() { bar(5) }", | ||
539 | ); | ||
540 | } | ||
541 | |||
542 | #[test] | ||
543 | fn ssr_with_extra_space() { | ||
544 | assert_ssr_transform( | ||
545 | "foo($x ) + bar() ==>> bar($x)", | ||
546 | "fn main() { foo( 5 ) +bar( ) }", | ||
547 | "fn main() { bar(5) }", | ||
548 | ); | ||
549 | } | ||
550 | |||
551 | #[test] | ||
552 | fn ssr_keeps_nested_comment() { | ||
553 | assert_ssr_transform( | ||
554 | "foo($x) ==>> bar($x)", | ||
555 | "fn main() { foo(other(5 /* using 5 */)) }", | ||
556 | "fn main() { bar(other(5 /* using 5 */)) }", | ||
557 | ) | ||
558 | } | ||
559 | |||
560 | #[test] | ||
561 | fn ssr_keeps_comment() { | ||
562 | assert_ssr_transform( | ||
563 | "foo($x) ==>> bar($x)", | ||
564 | "fn main() { foo(5 /* using 5 */) }", | ||
565 | "fn main() { bar(5)/* using 5 */ }", | ||
566 | ) | ||
567 | } | ||
568 | |||
569 | #[test] | ||
570 | fn ssr_struct_lit() { | ||
571 | assert_ssr_transform( | ||
572 | "foo{a: $a, b: $b} ==>> foo::new($a, $b)", | ||
573 | "fn main() { foo{b:2, a:1} }", | ||
574 | "fn main() { foo::new(1, 2) }", | ||
575 | ) | ||
576 | } | ||
577 | |||
578 | #[test] | ||
579 | fn ssr_call_and_method_call() { | ||
580 | assert_ssr_transform( | ||
581 | "foo::<'a>($a, $b)) ==>> foo2($a, $b)", | ||
582 | "fn main() { get().bar.foo::<'a>(1); }", | ||
583 | "fn main() { foo2(get().bar, 1); }", | ||
584 | ) | ||
585 | } | ||
586 | |||
587 | #[test] | ||
588 | fn ssr_method_call_and_call() { | ||
589 | assert_ssr_transform( | ||
590 | "$o.foo::<i32>($a)) ==>> $o.foo2($a)", | ||
591 | "fn main() { X::foo::<i32>(x, 1); }", | ||
592 | "fn main() { x.foo2(1); }", | ||
593 | ) | ||
594 | } | ||
595 | } | ||