aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ssr/src/matching.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ra_ssr/src/matching.rs')
-rw-r--r--crates/ra_ssr/src/matching.rs732
1 files changed, 0 insertions, 732 deletions
diff --git a/crates/ra_ssr/src/matching.rs b/crates/ra_ssr/src/matching.rs
deleted file mode 100644
index 0f72fea69..000000000
--- a/crates/ra_ssr/src/matching.rs
+++ /dev/null
@@ -1,732 +0,0 @@
1//! This module is responsible for matching a search pattern against a node in the AST. In the
2//! process of matching, placeholder values are recorded.
3
4use crate::{
5 parsing::{Constraint, NodeKind, Placeholder},
6 resolving::{ResolvedPattern, ResolvedRule},
7 SsrMatches,
8};
9use hir::Semantics;
10use ra_db::FileRange;
11use ra_syntax::ast::{AstNode, AstToken};
12use ra_syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
13use rustc_hash::FxHashMap;
14use std::{cell::Cell, iter::Peekable};
15use test_utils::mark;
16
17// Creates a match error. If we're currently attempting to match some code that we thought we were
18// going to match, as indicated by the --debug-snippet flag, then populate the reason field.
19macro_rules! match_error {
20 ($e:expr) => {{
21 MatchFailed {
22 reason: if recording_match_fail_reasons() {
23 Some(format!("{}", $e))
24 } else {
25 None
26 }
27 }
28 }};
29 ($fmt:expr, $($arg:tt)+) => {{
30 MatchFailed {
31 reason: if recording_match_fail_reasons() {
32 Some(format!($fmt, $($arg)+))
33 } else {
34 None
35 }
36 }
37 }};
38}
39
40// Fails the current match attempt, recording the supplied reason if we're recording match fail reasons.
41macro_rules! fail_match {
42 ($($args:tt)*) => {return Err(match_error!($($args)*))};
43}
44
45/// Information about a match that was found.
46#[derive(Debug)]
47pub struct Match {
48 pub(crate) range: FileRange,
49 pub(crate) matched_node: SyntaxNode,
50 pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>,
51 pub(crate) ignored_comments: Vec<ast::Comment>,
52 pub(crate) rule_index: usize,
53 /// The depth of matched_node.
54 pub(crate) depth: usize,
55 // Each path in the template rendered for the module in which the match was found.
56 pub(crate) rendered_template_paths: FxHashMap<SyntaxNode, hir::ModPath>,
57}
58
59/// Represents a `$var` in an SSR query.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub(crate) struct Var(pub String);
62
63/// Information about a placeholder bound in a match.
64#[derive(Debug)]
65pub(crate) struct PlaceholderMatch {
66 /// The node that the placeholder matched to. If set, then we'll search for further matches
67 /// within this node. It isn't set when we match tokens within a macro call's token tree.
68 pub(crate) node: Option<SyntaxNode>,
69 pub(crate) range: FileRange,
70 /// More matches, found within `node`.
71 pub(crate) inner_matches: SsrMatches,
72}
73
74#[derive(Debug)]
75pub(crate) struct MatchFailureReason {
76 pub(crate) reason: String,
77}
78
79/// An "error" indicating that matching failed. Use the fail_match! macro to create and return this.
80#[derive(Clone)]
81pub(crate) struct MatchFailed {
82 /// The reason why we failed to match. Only present when debug_active true in call to
83 /// `get_match`.
84 pub(crate) reason: Option<String>,
85}
86
87/// Checks if `code` matches the search pattern found in `search_scope`, returning information about
88/// the match, if it does. Since we only do matching in this module and searching is done by the
89/// parent module, we don't populate nested matches.
90pub(crate) fn get_match(
91 debug_active: bool,
92 rule: &ResolvedRule,
93 code: &SyntaxNode,
94 restrict_range: &Option<FileRange>,
95 sema: &Semantics<ra_ide_db::RootDatabase>,
96) -> Result<Match, MatchFailed> {
97 record_match_fails_reasons_scope(debug_active, || {
98 Matcher::try_match(rule, code, restrict_range, sema)
99 })
100}
101
102/// Checks if our search pattern matches a particular node of the AST.
103struct Matcher<'db, 'sema> {
104 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
105 /// If any placeholders come from anywhere outside of this range, then the match will be
106 /// rejected.
107 restrict_range: Option<FileRange>,
108 rule: &'sema ResolvedRule,
109}
110
111/// Which phase of matching we're currently performing. We do two phases because most attempted
112/// matches will fail and it means we can defer more expensive checks to the second phase.
113enum Phase<'a> {
114 /// On the first phase, we perform cheap checks. No state is mutated and nothing is recorded.
115 First,
116 /// On the second phase, we construct the `Match`. Things like what placeholders bind to is
117 /// recorded.
118 Second(&'a mut Match),
119}
120
121impl<'db, 'sema> Matcher<'db, 'sema> {
122 fn try_match(
123 rule: &ResolvedRule,
124 code: &SyntaxNode,
125 restrict_range: &Option<FileRange>,
126 sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
127 ) -> Result<Match, MatchFailed> {
128 let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule };
129 // First pass at matching, where we check that node types and idents match.
130 match_state.attempt_match_node(&mut Phase::First, &rule.pattern.node, code)?;
131 match_state.validate_range(&sema.original_range(code))?;
132 let mut the_match = Match {
133 range: sema.original_range(code),
134 matched_node: code.clone(),
135 placeholder_values: FxHashMap::default(),
136 ignored_comments: Vec::new(),
137 rule_index: rule.index,
138 depth: 0,
139 rendered_template_paths: FxHashMap::default(),
140 };
141 // Second matching pass, where we record placeholder matches, ignored comments and maybe do
142 // any other more expensive checks that we didn't want to do on the first pass.
143 match_state.attempt_match_node(
144 &mut Phase::Second(&mut the_match),
145 &rule.pattern.node,
146 code,
147 )?;
148 the_match.depth = sema.ancestors_with_macros(the_match.matched_node.clone()).count();
149 if let Some(template) = &rule.template {
150 the_match.render_template_paths(template, sema)?;
151 }
152 Ok(the_match)
153 }
154
155 /// Checks that `range` is within the permitted range if any. This is applicable when we're
156 /// processing a macro expansion and we want to fail the match if we're working with a node that
157 /// didn't originate from the token tree of the macro call.
158 fn validate_range(&self, range: &FileRange) -> Result<(), MatchFailed> {
159 if let Some(restrict_range) = &self.restrict_range {
160 if restrict_range.file_id != range.file_id
161 || !restrict_range.range.contains_range(range.range)
162 {
163 fail_match!("Node originated from a macro");
164 }
165 }
166 Ok(())
167 }
168
169 fn attempt_match_node(
170 &self,
171 phase: &mut Phase,
172 pattern: &SyntaxNode,
173 code: &SyntaxNode,
174 ) -> Result<(), MatchFailed> {
175 // Handle placeholders.
176 if let Some(placeholder) = self.get_placeholder(&SyntaxElement::Node(pattern.clone())) {
177 for constraint in &placeholder.constraints {
178 self.check_constraint(constraint, code)?;
179 }
180 if let Phase::Second(matches_out) = phase {
181 let original_range = self.sema.original_range(code);
182 // We validated the range for the node when we started the match, so the placeholder
183 // probably can't fail range validation, but just to be safe...
184 self.validate_range(&original_range)?;
185 matches_out.placeholder_values.insert(
186 Var(placeholder.ident.to_string()),
187 PlaceholderMatch::new(code, original_range),
188 );
189 }
190 return Ok(());
191 }
192 // We allow a UFCS call to match a method call, provided they resolve to the same function.
193 if let Some(pattern_function) = self.rule.pattern.ufcs_function_calls.get(pattern) {
194 if let (Some(pattern), Some(code)) =
195 (ast::CallExpr::cast(pattern.clone()), ast::MethodCallExpr::cast(code.clone()))
196 {
197 return self.attempt_match_ufcs(phase, &pattern, &code, *pattern_function);
198 }
199 }
200 if pattern.kind() != code.kind() {
201 fail_match!(
202 "Pattern had `{}` ({:?}), code had `{}` ({:?})",
203 pattern.text(),
204 pattern.kind(),
205 code.text(),
206 code.kind()
207 );
208 }
209 // Some kinds of nodes have special handling. For everything else, we fall back to default
210 // matching.
211 match code.kind() {
212 SyntaxKind::RECORD_EXPR_FIELD_LIST => {
213 self.attempt_match_record_field_list(phase, pattern, code)
214 }
215 SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code),
216 SyntaxKind::PATH => self.attempt_match_path(phase, pattern, code),
217 _ => self.attempt_match_node_children(phase, pattern, code),
218 }
219 }
220
221 fn attempt_match_node_children(
222 &self,
223 phase: &mut Phase,
224 pattern: &SyntaxNode,
225 code: &SyntaxNode,
226 ) -> Result<(), MatchFailed> {
227 self.attempt_match_sequences(
228 phase,
229 PatternIterator::new(pattern),
230 code.children_with_tokens(),
231 )
232 }
233
234 fn attempt_match_sequences(
235 &self,
236 phase: &mut Phase,
237 pattern_it: PatternIterator,
238 mut code_it: SyntaxElementChildren,
239 ) -> Result<(), MatchFailed> {
240 let mut pattern_it = pattern_it.peekable();
241 loop {
242 match phase.next_non_trivial(&mut code_it) {
243 None => {
244 if let Some(p) = pattern_it.next() {
245 fail_match!("Part of the pattern was unmatched: {:?}", p);
246 }
247 return Ok(());
248 }
249 Some(SyntaxElement::Token(c)) => {
250 self.attempt_match_token(phase, &mut pattern_it, &c)?;
251 }
252 Some(SyntaxElement::Node(c)) => match pattern_it.next() {
253 Some(SyntaxElement::Node(p)) => {
254 self.attempt_match_node(phase, &p, &c)?;
255 }
256 Some(p) => fail_match!("Pattern wanted '{}', code has {}", p, c.text()),
257 None => fail_match!("Pattern reached end, code has {}", c.text()),
258 },
259 }
260 }
261 }
262
263 fn attempt_match_token(
264 &self,
265 phase: &mut Phase,
266 pattern: &mut Peekable<PatternIterator>,
267 code: &ra_syntax::SyntaxToken,
268 ) -> Result<(), MatchFailed> {
269 phase.record_ignored_comments(code);
270 // Ignore whitespace and comments.
271 if code.kind().is_trivia() {
272 return Ok(());
273 }
274 if let Some(SyntaxElement::Token(p)) = pattern.peek() {
275 // If the code has a comma and the pattern is about to close something, then accept the
276 // comma without advancing the pattern. i.e. ignore trailing commas.
277 if code.kind() == SyntaxKind::COMMA && is_closing_token(p.kind()) {
278 return Ok(());
279 }
280 // Conversely, if the pattern has a comma and the code doesn't, skip that part of the
281 // pattern and continue to match the code.
282 if p.kind() == SyntaxKind::COMMA && is_closing_token(code.kind()) {
283 pattern.next();
284 }
285 }
286 // Consume an element from the pattern and make sure it matches.
287 match pattern.next() {
288 Some(SyntaxElement::Token(p)) => {
289 if p.kind() != code.kind() || p.text() != code.text() {
290 fail_match!(
291 "Pattern wanted token '{}' ({:?}), but code had token '{}' ({:?})",
292 p.text(),
293 p.kind(),
294 code.text(),
295 code.kind()
296 )
297 }
298 }
299 Some(SyntaxElement::Node(p)) => {
300 // Not sure if this is actually reachable.
301 fail_match!(
302 "Pattern wanted {:?}, but code had token '{}' ({:?})",
303 p,
304 code.text(),
305 code.kind()
306 );
307 }
308 None => {
309 fail_match!("Pattern exhausted, while code remains: `{}`", code.text());
310 }
311 }
312 Ok(())
313 }
314
315 fn check_constraint(
316 &self,
317 constraint: &Constraint,
318 code: &SyntaxNode,
319 ) -> Result<(), MatchFailed> {
320 match constraint {
321 Constraint::Kind(kind) => {
322 kind.matches(code)?;
323 }
324 Constraint::Not(sub) => {
325 if self.check_constraint(&*sub, code).is_ok() {
326 fail_match!("Constraint {:?} failed for '{}'", constraint, code.text());
327 }
328 }
329 }
330 Ok(())
331 }
332
333 /// Paths are matched based on whether they refer to the same thing, even if they're written
334 /// differently.
335 fn attempt_match_path(
336 &self,
337 phase: &mut Phase,
338 pattern: &SyntaxNode,
339 code: &SyntaxNode,
340 ) -> Result<(), MatchFailed> {
341 if let Some(pattern_resolved) = self.rule.pattern.resolved_paths.get(pattern) {
342 let pattern_path = ast::Path::cast(pattern.clone()).unwrap();
343 let code_path = ast::Path::cast(code.clone()).unwrap();
344 if let (Some(pattern_segment), Some(code_segment)) =
345 (pattern_path.segment(), code_path.segment())
346 {
347 // Match everything within the segment except for the name-ref, which is handled
348 // separately via comparing what the path resolves to below.
349 self.attempt_match_opt(
350 phase,
351 pattern_segment.generic_arg_list(),
352 code_segment.generic_arg_list(),
353 )?;
354 self.attempt_match_opt(
355 phase,
356 pattern_segment.param_list(),
357 code_segment.param_list(),
358 )?;
359 }
360 if matches!(phase, Phase::Second(_)) {
361 let resolution = self
362 .sema
363 .resolve_path(&code_path)
364 .ok_or_else(|| match_error!("Failed to resolve path `{}`", code.text()))?;
365 if pattern_resolved.resolution != resolution {
366 fail_match!("Pattern had path `{}` code had `{}`", pattern.text(), code.text());
367 }
368 }
369 } else {
370 return self.attempt_match_node_children(phase, pattern, code);
371 }
372 Ok(())
373 }
374
375 fn attempt_match_opt<T: AstNode>(
376 &self,
377 phase: &mut Phase,
378 pattern: Option<T>,
379 code: Option<T>,
380 ) -> Result<(), MatchFailed> {
381 match (pattern, code) {
382 (Some(p), Some(c)) => self.attempt_match_node(phase, &p.syntax(), &c.syntax()),
383 (None, None) => Ok(()),
384 (Some(p), None) => fail_match!("Pattern `{}` had nothing to match", p.syntax().text()),
385 (None, Some(c)) => {
386 fail_match!("Nothing in pattern to match code `{}`", c.syntax().text())
387 }
388 }
389 }
390
391 /// We want to allow the records to match in any order, so we have special matching logic for
392 /// them.
393 fn attempt_match_record_field_list(
394 &self,
395 phase: &mut Phase,
396 pattern: &SyntaxNode,
397 code: &SyntaxNode,
398 ) -> Result<(), MatchFailed> {
399 // Build a map keyed by field name.
400 let mut fields_by_name = FxHashMap::default();
401 for child in code.children() {
402 if let Some(record) = ast::RecordExprField::cast(child.clone()) {
403 if let Some(name) = record.field_name() {
404 fields_by_name.insert(name.text().clone(), child.clone());
405 }
406 }
407 }
408 for p in pattern.children_with_tokens() {
409 if let SyntaxElement::Node(p) = p {
410 if let Some(name_element) = p.first_child_or_token() {
411 if self.get_placeholder(&name_element).is_some() {
412 // If the pattern is using placeholders for field names then order
413 // independence doesn't make sense. Fall back to regular ordered
414 // matching.
415 return self.attempt_match_node_children(phase, pattern, code);
416 }
417 if let Some(ident) = only_ident(name_element) {
418 let code_record = fields_by_name.remove(ident.text()).ok_or_else(|| {
419 match_error!(
420 "Placeholder has record field '{}', but code doesn't",
421 ident
422 )
423 })?;
424 self.attempt_match_node(phase, &p, &code_record)?;
425 }
426 }
427 }
428 }
429 if let Some(unmatched_fields) = fields_by_name.keys().next() {
430 fail_match!(
431 "{} field(s) of a record literal failed to match, starting with {}",
432 fields_by_name.len(),
433 unmatched_fields
434 );
435 }
436 Ok(())
437 }
438
439 /// Outside of token trees, a placeholder can only match a single AST node, whereas in a token
440 /// tree it can match a sequence of tokens. Note, that this code will only be used when the
441 /// pattern matches the macro invocation. For matches within the macro call, we'll already have
442 /// expanded the macro.
443 fn attempt_match_token_tree(
444 &self,
445 phase: &mut Phase,
446 pattern: &SyntaxNode,
447 code: &ra_syntax::SyntaxNode,
448 ) -> Result<(), MatchFailed> {
449 let mut pattern = PatternIterator::new(pattern).peekable();
450 let mut children = code.children_with_tokens();
451 while let Some(child) = children.next() {
452 if let Some(placeholder) = pattern.peek().and_then(|p| self.get_placeholder(p)) {
453 pattern.next();
454 let next_pattern_token = pattern
455 .peek()
456 .and_then(|p| match p {
457 SyntaxElement::Token(t) => Some(t.clone()),
458 SyntaxElement::Node(n) => n.first_token(),
459 })
460 .map(|p| p.text().to_string());
461 let first_matched_token = child.clone();
462 let mut last_matched_token = child;
463 // Read code tokens util we reach one equal to the next token from our pattern
464 // or we reach the end of the token tree.
465 while let Some(next) = children.next() {
466 match &next {
467 SyntaxElement::Token(t) => {
468 if Some(t.to_string()) == next_pattern_token {
469 pattern.next();
470 break;
471 }
472 }
473 SyntaxElement::Node(n) => {
474 if let Some(first_token) = n.first_token() {
475 if Some(first_token.to_string()) == next_pattern_token {
476 if let Some(SyntaxElement::Node(p)) = pattern.next() {
477 // We have a subtree that starts with the next token in our pattern.
478 self.attempt_match_token_tree(phase, &p, &n)?;
479 break;
480 }
481 }
482 }
483 }
484 };
485 last_matched_token = next;
486 }
487 if let Phase::Second(match_out) = phase {
488 match_out.placeholder_values.insert(
489 Var(placeholder.ident.to_string()),
490 PlaceholderMatch::from_range(FileRange {
491 file_id: self.sema.original_range(code).file_id,
492 range: first_matched_token
493 .text_range()
494 .cover(last_matched_token.text_range()),
495 }),
496 );
497 }
498 continue;
499 }
500 // Match literal (non-placeholder) tokens.
501 match child {
502 SyntaxElement::Token(token) => {
503 self.attempt_match_token(phase, &mut pattern, &token)?;
504 }
505 SyntaxElement::Node(node) => match pattern.next() {
506 Some(SyntaxElement::Node(p)) => {
507 self.attempt_match_token_tree(phase, &p, &node)?;
508 }
509 Some(SyntaxElement::Token(p)) => fail_match!(
510 "Pattern has token '{}', code has subtree '{}'",
511 p.text(),
512 node.text()
513 ),
514 None => fail_match!("Pattern has nothing, code has '{}'", node.text()),
515 },
516 }
517 }
518 if let Some(p) = pattern.next() {
519 fail_match!("Reached end of token tree in code, but pattern still has {:?}", p);
520 }
521 Ok(())
522 }
523
524 fn attempt_match_ufcs(
525 &self,
526 phase: &mut Phase,
527 pattern: &ast::CallExpr,
528 code: &ast::MethodCallExpr,
529 pattern_function: hir::Function,
530 ) -> Result<(), MatchFailed> {
531 use ast::ArgListOwner;
532 let code_resolved_function = self
533 .sema
534 .resolve_method_call(code)
535 .ok_or_else(|| match_error!("Failed to resolve method call"))?;
536 if pattern_function != code_resolved_function {
537 fail_match!("Method call resolved to a different function");
538 }
539 // Check arguments.
540 let mut pattern_args = pattern
541 .arg_list()
542 .ok_or_else(|| match_error!("Pattern function call has no args"))?
543 .args();
544 self.attempt_match_opt(phase, pattern_args.next(), code.expr())?;
545 let mut code_args =
546 code.arg_list().ok_or_else(|| match_error!("Code method call has no args"))?.args();
547 loop {
548 match (pattern_args.next(), code_args.next()) {
549 (None, None) => return Ok(()),
550 (p, c) => self.attempt_match_opt(phase, p, c)?,
551 }
552 }
553 }
554
555 fn get_placeholder(&self, element: &SyntaxElement) -> Option<&Placeholder> {
556 only_ident(element.clone()).and_then(|ident| self.rule.get_placeholder(&ident))
557 }
558}
559
560impl Match {
561 fn render_template_paths(
562 &mut self,
563 template: &ResolvedPattern,
564 sema: &Semantics<ra_ide_db::RootDatabase>,
565 ) -> Result<(), MatchFailed> {
566 let module = sema
567 .scope(&self.matched_node)
568 .module()
569 .ok_or_else(|| match_error!("Matched node isn't in a module"))?;
570 for (path, resolved_path) in &template.resolved_paths {
571 if let hir::PathResolution::Def(module_def) = resolved_path.resolution {
572 let mod_path = module.find_use_path(sema.db, module_def).ok_or_else(|| {
573 match_error!("Failed to render template path `{}` at match location")
574 })?;
575 self.rendered_template_paths.insert(path.clone(), mod_path);
576 }
577 }
578 Ok(())
579 }
580}
581
582impl Phase<'_> {
583 fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
584 loop {
585 let c = code_it.next();
586 if let Some(SyntaxElement::Token(t)) = &c {
587 self.record_ignored_comments(t);
588 if t.kind().is_trivia() {
589 continue;
590 }
591 }
592 return c;
593 }
594 }
595
596 fn record_ignored_comments(&mut self, token: &SyntaxToken) {
597 if token.kind() == SyntaxKind::COMMENT {
598 if let Phase::Second(match_out) = self {
599 if let Some(comment) = ast::Comment::cast(token.clone()) {
600 match_out.ignored_comments.push(comment);
601 }
602 }
603 }
604 }
605}
606
607fn is_closing_token(kind: SyntaxKind) -> bool {
608 kind == SyntaxKind::R_PAREN || kind == SyntaxKind::R_CURLY || kind == SyntaxKind::R_BRACK
609}
610
611pub(crate) fn record_match_fails_reasons_scope<F, T>(debug_active: bool, f: F) -> T
612where
613 F: Fn() -> T,
614{
615 RECORDING_MATCH_FAIL_REASONS.with(|c| c.set(debug_active));
616 let res = f();
617 RECORDING_MATCH_FAIL_REASONS.with(|c| c.set(false));
618 res
619}
620
621// For performance reasons, we don't want to record the reason why every match fails, only the bit
622// of code that the user indicated they thought would match. We use a thread local to indicate when
623// we are trying to match that bit of code. This saves us having to pass a boolean into all the bits
624// of code that can make the decision to not match.
625thread_local! {
626 pub static RECORDING_MATCH_FAIL_REASONS: Cell<bool> = Cell::new(false);
627}
628
629fn recording_match_fail_reasons() -> bool {
630 RECORDING_MATCH_FAIL_REASONS.with(|c| c.get())
631}
632
633impl PlaceholderMatch {
634 fn new(node: &SyntaxNode, range: FileRange) -> Self {
635 Self { node: Some(node.clone()), range, inner_matches: SsrMatches::default() }
636 }
637
638 fn from_range(range: FileRange) -> Self {
639 Self { node: None, range, inner_matches: SsrMatches::default() }
640 }
641}
642
643impl NodeKind {
644 fn matches(&self, node: &SyntaxNode) -> Result<(), MatchFailed> {
645 let ok = match self {
646 Self::Literal => {
647 mark::hit!(literal_constraint);
648 ast::Literal::can_cast(node.kind())
649 }
650 };
651 if !ok {
652 fail_match!("Code '{}' isn't of kind {:?}", node.text(), self);
653 }
654 Ok(())
655 }
656}
657
658// If `node` contains nothing but an ident then return it, otherwise return None.
659fn only_ident(element: SyntaxElement) -> Option<SyntaxToken> {
660 match element {
661 SyntaxElement::Token(t) => {
662 if t.kind() == SyntaxKind::IDENT {
663 return Some(t);
664 }
665 }
666 SyntaxElement::Node(n) => {
667 let mut children = n.children_with_tokens();
668 if let (Some(only_child), None) = (children.next(), children.next()) {
669 return only_ident(only_child);
670 }
671 }
672 }
673 None
674}
675
676struct PatternIterator {
677 iter: SyntaxElementChildren,
678}
679
680impl Iterator for PatternIterator {
681 type Item = SyntaxElement;
682
683 fn next(&mut self) -> Option<SyntaxElement> {
684 while let Some(element) = self.iter.next() {
685 if !element.kind().is_trivia() {
686 return Some(element);
687 }
688 }
689 None
690 }
691}
692
693impl PatternIterator {
694 fn new(parent: &SyntaxNode) -> Self {
695 Self { iter: parent.children_with_tokens() }
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use crate::{MatchFinder, SsrRule};
703
704 #[test]
705 fn parse_match_replace() {
706 let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
707 let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
708
709 let (db, position, selections) = crate::tests::single_file(input);
710 let mut match_finder = MatchFinder::in_context(&db, position, selections);
711 match_finder.add_rule(rule).unwrap();
712 let matches = match_finder.matches();
713 assert_eq!(matches.matches.len(), 1);
714 assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
715 assert_eq!(matches.matches[0].placeholder_values.len(), 1);
716 assert_eq!(
717 matches.matches[0].placeholder_values[&Var("x".to_string())]
718 .node
719 .as_ref()
720 .unwrap()
721 .text(),
722 "1+2"
723 );
724
725 let edits = match_finder.edits();
726 assert_eq!(edits.len(), 1);
727 let edit = &edits[0];
728 let mut after = input.to_string();
729 edit.edit.apply(&mut after);
730 assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }");
731 }
732}