aboutsummaryrefslogtreecommitdiff
path: root/crates/ssr/src/matching.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ssr/src/matching.rs')
-rw-r--r--crates/ssr/src/matching.rs777
1 files changed, 777 insertions, 0 deletions
diff --git a/crates/ssr/src/matching.rs b/crates/ssr/src/matching.rs
new file mode 100644
index 000000000..ffc7202ae
--- /dev/null
+++ b/crates/ssr/src/matching.rs
@@ -0,0 +1,777 @@
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, UfcsCallInfo},
7 SsrMatches,
8};
9use base_db::FileRange;
10use hir::Semantics;
11use rustc_hash::FxHashMap;
12use std::{cell::Cell, iter::Peekable};
13use syntax::ast::{AstNode, AstToken};
14use syntax::{ast, SyntaxElement, SyntaxElementChildren, SyntaxKind, SyntaxNode, SyntaxToken};
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<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, 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, 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_ufcs) = self.rule.pattern.ufcs_function_calls.get(pattern) {
194 if let Some(code) = ast::MethodCallExpr::cast(code.clone()) {
195 return self.attempt_match_ufcs_to_method_call(phase, pattern_ufcs, &code);
196 }
197 if let Some(code) = ast::CallExpr::cast(code.clone()) {
198 return self.attempt_match_ufcs_to_ufcs(phase, pattern_ufcs, &code);
199 }
200 }
201 if pattern.kind() != code.kind() {
202 fail_match!(
203 "Pattern had `{}` ({:?}), code had `{}` ({:?})",
204 pattern.text(),
205 pattern.kind(),
206 code.text(),
207 code.kind()
208 );
209 }
210 // Some kinds of nodes have special handling. For everything else, we fall back to default
211 // matching.
212 match code.kind() {
213 SyntaxKind::RECORD_EXPR_FIELD_LIST => {
214 self.attempt_match_record_field_list(phase, pattern, code)
215 }
216 SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code),
217 SyntaxKind::PATH => self.attempt_match_path(phase, pattern, code),
218 _ => self.attempt_match_node_children(phase, pattern, code),
219 }
220 }
221
222 fn attempt_match_node_children(
223 &self,
224 phase: &mut Phase,
225 pattern: &SyntaxNode,
226 code: &SyntaxNode,
227 ) -> Result<(), MatchFailed> {
228 self.attempt_match_sequences(
229 phase,
230 PatternIterator::new(pattern),
231 code.children_with_tokens(),
232 )
233 }
234
235 fn attempt_match_sequences(
236 &self,
237 phase: &mut Phase,
238 pattern_it: PatternIterator,
239 mut code_it: SyntaxElementChildren,
240 ) -> Result<(), MatchFailed> {
241 let mut pattern_it = pattern_it.peekable();
242 loop {
243 match phase.next_non_trivial(&mut code_it) {
244 None => {
245 if let Some(p) = pattern_it.next() {
246 fail_match!("Part of the pattern was unmatched: {:?}", p);
247 }
248 return Ok(());
249 }
250 Some(SyntaxElement::Token(c)) => {
251 self.attempt_match_token(phase, &mut pattern_it, &c)?;
252 }
253 Some(SyntaxElement::Node(c)) => match pattern_it.next() {
254 Some(SyntaxElement::Node(p)) => {
255 self.attempt_match_node(phase, &p, &c)?;
256 }
257 Some(p) => fail_match!("Pattern wanted '{}', code has {}", p, c.text()),
258 None => fail_match!("Pattern reached end, code has {}", c.text()),
259 },
260 }
261 }
262 }
263
264 fn attempt_match_token(
265 &self,
266 phase: &mut Phase,
267 pattern: &mut Peekable<PatternIterator>,
268 code: &syntax::SyntaxToken,
269 ) -> Result<(), MatchFailed> {
270 phase.record_ignored_comments(code);
271 // Ignore whitespace and comments.
272 if code.kind().is_trivia() {
273 return Ok(());
274 }
275 if let Some(SyntaxElement::Token(p)) = pattern.peek() {
276 // If the code has a comma and the pattern is about to close something, then accept the
277 // comma without advancing the pattern. i.e. ignore trailing commas.
278 if code.kind() == SyntaxKind::COMMA && is_closing_token(p.kind()) {
279 return Ok(());
280 }
281 // Conversely, if the pattern has a comma and the code doesn't, skip that part of the
282 // pattern and continue to match the code.
283 if p.kind() == SyntaxKind::COMMA && is_closing_token(code.kind()) {
284 pattern.next();
285 }
286 }
287 // Consume an element from the pattern and make sure it matches.
288 match pattern.next() {
289 Some(SyntaxElement::Token(p)) => {
290 if p.kind() != code.kind() || p.text() != code.text() {
291 fail_match!(
292 "Pattern wanted token '{}' ({:?}), but code had token '{}' ({:?})",
293 p.text(),
294 p.kind(),
295 code.text(),
296 code.kind()
297 )
298 }
299 }
300 Some(SyntaxElement::Node(p)) => {
301 // Not sure if this is actually reachable.
302 fail_match!(
303 "Pattern wanted {:?}, but code had token '{}' ({:?})",
304 p,
305 code.text(),
306 code.kind()
307 );
308 }
309 None => {
310 fail_match!("Pattern exhausted, while code remains: `{}`", code.text());
311 }
312 }
313 Ok(())
314 }
315
316 fn check_constraint(
317 &self,
318 constraint: &Constraint,
319 code: &SyntaxNode,
320 ) -> Result<(), MatchFailed> {
321 match constraint {
322 Constraint::Kind(kind) => {
323 kind.matches(code)?;
324 }
325 Constraint::Not(sub) => {
326 if self.check_constraint(&*sub, code).is_ok() {
327 fail_match!("Constraint {:?} failed for '{}'", constraint, code.text());
328 }
329 }
330 }
331 Ok(())
332 }
333
334 /// Paths are matched based on whether they refer to the same thing, even if they're written
335 /// differently.
336 fn attempt_match_path(
337 &self,
338 phase: &mut Phase,
339 pattern: &SyntaxNode,
340 code: &SyntaxNode,
341 ) -> Result<(), MatchFailed> {
342 if let Some(pattern_resolved) = self.rule.pattern.resolved_paths.get(pattern) {
343 let pattern_path = ast::Path::cast(pattern.clone()).unwrap();
344 let code_path = ast::Path::cast(code.clone()).unwrap();
345 if let (Some(pattern_segment), Some(code_segment)) =
346 (pattern_path.segment(), code_path.segment())
347 {
348 // Match everything within the segment except for the name-ref, which is handled
349 // separately via comparing what the path resolves to below.
350 self.attempt_match_opt(
351 phase,
352 pattern_segment.generic_arg_list(),
353 code_segment.generic_arg_list(),
354 )?;
355 self.attempt_match_opt(
356 phase,
357 pattern_segment.param_list(),
358 code_segment.param_list(),
359 )?;
360 }
361 if matches!(phase, Phase::Second(_)) {
362 let resolution = self
363 .sema
364 .resolve_path(&code_path)
365 .ok_or_else(|| match_error!("Failed to resolve path `{}`", code.text()))?;
366 if pattern_resolved.resolution != resolution {
367 fail_match!("Pattern had path `{}` code had `{}`", pattern.text(), code.text());
368 }
369 }
370 } else {
371 return self.attempt_match_node_children(phase, pattern, code);
372 }
373 Ok(())
374 }
375
376 fn attempt_match_opt<T: AstNode>(
377 &self,
378 phase: &mut Phase,
379 pattern: Option<T>,
380 code: Option<T>,
381 ) -> Result<(), MatchFailed> {
382 match (pattern, code) {
383 (Some(p), Some(c)) => self.attempt_match_node(phase, &p.syntax(), &c.syntax()),
384 (None, None) => Ok(()),
385 (Some(p), None) => fail_match!("Pattern `{}` had nothing to match", p.syntax().text()),
386 (None, Some(c)) => {
387 fail_match!("Nothing in pattern to match code `{}`", c.syntax().text())
388 }
389 }
390 }
391
392 /// We want to allow the records to match in any order, so we have special matching logic for
393 /// them.
394 fn attempt_match_record_field_list(
395 &self,
396 phase: &mut Phase,
397 pattern: &SyntaxNode,
398 code: &SyntaxNode,
399 ) -> Result<(), MatchFailed> {
400 // Build a map keyed by field name.
401 let mut fields_by_name = FxHashMap::default();
402 for child in code.children() {
403 if let Some(record) = ast::RecordExprField::cast(child.clone()) {
404 if let Some(name) = record.field_name() {
405 fields_by_name.insert(name.text().clone(), child.clone());
406 }
407 }
408 }
409 for p in pattern.children_with_tokens() {
410 if let SyntaxElement::Node(p) = p {
411 if let Some(name_element) = p.first_child_or_token() {
412 if self.get_placeholder(&name_element).is_some() {
413 // If the pattern is using placeholders for field names then order
414 // independence doesn't make sense. Fall back to regular ordered
415 // matching.
416 return self.attempt_match_node_children(phase, pattern, code);
417 }
418 if let Some(ident) = only_ident(name_element) {
419 let code_record = fields_by_name.remove(ident.text()).ok_or_else(|| {
420 match_error!(
421 "Placeholder has record field '{}', but code doesn't",
422 ident
423 )
424 })?;
425 self.attempt_match_node(phase, &p, &code_record)?;
426 }
427 }
428 }
429 }
430 if let Some(unmatched_fields) = fields_by_name.keys().next() {
431 fail_match!(
432 "{} field(s) of a record literal failed to match, starting with {}",
433 fields_by_name.len(),
434 unmatched_fields
435 );
436 }
437 Ok(())
438 }
439
440 /// Outside of token trees, a placeholder can only match a single AST node, whereas in a token
441 /// tree it can match a sequence of tokens. Note, that this code will only be used when the
442 /// pattern matches the macro invocation. For matches within the macro call, we'll already have
443 /// expanded the macro.
444 fn attempt_match_token_tree(
445 &self,
446 phase: &mut Phase,
447 pattern: &SyntaxNode,
448 code: &syntax::SyntaxNode,
449 ) -> Result<(), MatchFailed> {
450 let mut pattern = PatternIterator::new(pattern).peekable();
451 let mut children = code.children_with_tokens();
452 while let Some(child) = children.next() {
453 if let Some(placeholder) = pattern.peek().and_then(|p| self.get_placeholder(p)) {
454 pattern.next();
455 let next_pattern_token = pattern
456 .peek()
457 .and_then(|p| match p {
458 SyntaxElement::Token(t) => Some(t.clone()),
459 SyntaxElement::Node(n) => n.first_token(),
460 })
461 .map(|p| p.text().to_string());
462 let first_matched_token = child.clone();
463 let mut last_matched_token = child;
464 // Read code tokens util we reach one equal to the next token from our pattern
465 // or we reach the end of the token tree.
466 while let Some(next) = children.next() {
467 match &next {
468 SyntaxElement::Token(t) => {
469 if Some(t.to_string()) == next_pattern_token {
470 pattern.next();
471 break;
472 }
473 }
474 SyntaxElement::Node(n) => {
475 if let Some(first_token) = n.first_token() {
476 if Some(first_token.to_string()) == next_pattern_token {
477 if let Some(SyntaxElement::Node(p)) = pattern.next() {
478 // We have a subtree that starts with the next token in our pattern.
479 self.attempt_match_token_tree(phase, &p, &n)?;
480 break;
481 }
482 }
483 }
484 }
485 };
486 last_matched_token = next;
487 }
488 if let Phase::Second(match_out) = phase {
489 match_out.placeholder_values.insert(
490 Var(placeholder.ident.to_string()),
491 PlaceholderMatch::from_range(FileRange {
492 file_id: self.sema.original_range(code).file_id,
493 range: first_matched_token
494 .text_range()
495 .cover(last_matched_token.text_range()),
496 }),
497 );
498 }
499 continue;
500 }
501 // Match literal (non-placeholder) tokens.
502 match child {
503 SyntaxElement::Token(token) => {
504 self.attempt_match_token(phase, &mut pattern, &token)?;
505 }
506 SyntaxElement::Node(node) => match pattern.next() {
507 Some(SyntaxElement::Node(p)) => {
508 self.attempt_match_token_tree(phase, &p, &node)?;
509 }
510 Some(SyntaxElement::Token(p)) => fail_match!(
511 "Pattern has token '{}', code has subtree '{}'",
512 p.text(),
513 node.text()
514 ),
515 None => fail_match!("Pattern has nothing, code has '{}'", node.text()),
516 },
517 }
518 }
519 if let Some(p) = pattern.next() {
520 fail_match!("Reached end of token tree in code, but pattern still has {:?}", p);
521 }
522 Ok(())
523 }
524
525 fn attempt_match_ufcs_to_method_call(
526 &self,
527 phase: &mut Phase,
528 pattern_ufcs: &UfcsCallInfo,
529 code: &ast::MethodCallExpr,
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_ufcs.function != code_resolved_function {
537 fail_match!("Method call resolved to a different function");
538 }
539 if code_resolved_function.has_self_param(self.sema.db) {
540 if let (Some(pattern_type), Some(expr)) = (&pattern_ufcs.qualifier_type, &code.expr()) {
541 self.check_expr_type(pattern_type, expr)?;
542 }
543 }
544 // Check arguments.
545 let mut pattern_args = pattern_ufcs
546 .call_expr
547 .arg_list()
548 .ok_or_else(|| match_error!("Pattern function call has no args"))?
549 .args();
550 self.attempt_match_opt(phase, pattern_args.next(), code.expr())?;
551 let mut code_args =
552 code.arg_list().ok_or_else(|| match_error!("Code method call has no args"))?.args();
553 loop {
554 match (pattern_args.next(), code_args.next()) {
555 (None, None) => return Ok(()),
556 (p, c) => self.attempt_match_opt(phase, p, c)?,
557 }
558 }
559 }
560
561 fn attempt_match_ufcs_to_ufcs(
562 &self,
563 phase: &mut Phase,
564 pattern_ufcs: &UfcsCallInfo,
565 code: &ast::CallExpr,
566 ) -> Result<(), MatchFailed> {
567 use ast::ArgListOwner;
568 // Check that the first argument is the expected type.
569 if let (Some(pattern_type), Some(expr)) = (
570 &pattern_ufcs.qualifier_type,
571 &code.arg_list().and_then(|code_args| code_args.args().next()),
572 ) {
573 self.check_expr_type(pattern_type, expr)?;
574 }
575 self.attempt_match_node_children(phase, pattern_ufcs.call_expr.syntax(), code.syntax())
576 }
577
578 fn check_expr_type(
579 &self,
580 pattern_type: &hir::Type,
581 expr: &ast::Expr,
582 ) -> Result<(), MatchFailed> {
583 use hir::HirDisplay;
584 let code_type = self.sema.type_of_expr(&expr).ok_or_else(|| {
585 match_error!("Failed to get receiver type for `{}`", expr.syntax().text())
586 })?;
587 if !code_type
588 .autoderef(self.sema.db)
589 .any(|deref_code_type| *pattern_type == deref_code_type)
590 {
591 fail_match!(
592 "Pattern type `{}` didn't match code type `{}`",
593 pattern_type.display(self.sema.db),
594 code_type.display(self.sema.db)
595 );
596 }
597 Ok(())
598 }
599
600 fn get_placeholder(&self, element: &SyntaxElement) -> Option<&Placeholder> {
601 only_ident(element.clone()).and_then(|ident| self.rule.get_placeholder(&ident))
602 }
603}
604
605impl Match {
606 fn render_template_paths(
607 &mut self,
608 template: &ResolvedPattern,
609 sema: &Semantics<ide_db::RootDatabase>,
610 ) -> Result<(), MatchFailed> {
611 let module = sema
612 .scope(&self.matched_node)
613 .module()
614 .ok_or_else(|| match_error!("Matched node isn't in a module"))?;
615 for (path, resolved_path) in &template.resolved_paths {
616 if let hir::PathResolution::Def(module_def) = resolved_path.resolution {
617 let mod_path = module.find_use_path(sema.db, module_def).ok_or_else(|| {
618 match_error!("Failed to render template path `{}` at match location")
619 })?;
620 self.rendered_template_paths.insert(path.clone(), mod_path);
621 }
622 }
623 Ok(())
624 }
625}
626
627impl Phase<'_> {
628 fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
629 loop {
630 let c = code_it.next();
631 if let Some(SyntaxElement::Token(t)) = &c {
632 self.record_ignored_comments(t);
633 if t.kind().is_trivia() {
634 continue;
635 }
636 }
637 return c;
638 }
639 }
640
641 fn record_ignored_comments(&mut self, token: &SyntaxToken) {
642 if token.kind() == SyntaxKind::COMMENT {
643 if let Phase::Second(match_out) = self {
644 if let Some(comment) = ast::Comment::cast(token.clone()) {
645 match_out.ignored_comments.push(comment);
646 }
647 }
648 }
649 }
650}
651
652fn is_closing_token(kind: SyntaxKind) -> bool {
653 kind == SyntaxKind::R_PAREN || kind == SyntaxKind::R_CURLY || kind == SyntaxKind::R_BRACK
654}
655
656pub(crate) fn record_match_fails_reasons_scope<F, T>(debug_active: bool, f: F) -> T
657where
658 F: Fn() -> T,
659{
660 RECORDING_MATCH_FAIL_REASONS.with(|c| c.set(debug_active));
661 let res = f();
662 RECORDING_MATCH_FAIL_REASONS.with(|c| c.set(false));
663 res
664}
665
666// For performance reasons, we don't want to record the reason why every match fails, only the bit
667// of code that the user indicated they thought would match. We use a thread local to indicate when
668// we are trying to match that bit of code. This saves us having to pass a boolean into all the bits
669// of code that can make the decision to not match.
670thread_local! {
671 pub static RECORDING_MATCH_FAIL_REASONS: Cell<bool> = Cell::new(false);
672}
673
674fn recording_match_fail_reasons() -> bool {
675 RECORDING_MATCH_FAIL_REASONS.with(|c| c.get())
676}
677
678impl PlaceholderMatch {
679 fn new(node: &SyntaxNode, range: FileRange) -> Self {
680 Self { node: Some(node.clone()), range, inner_matches: SsrMatches::default() }
681 }
682
683 fn from_range(range: FileRange) -> Self {
684 Self { node: None, range, inner_matches: SsrMatches::default() }
685 }
686}
687
688impl NodeKind {
689 fn matches(&self, node: &SyntaxNode) -> Result<(), MatchFailed> {
690 let ok = match self {
691 Self::Literal => {
692 mark::hit!(literal_constraint);
693 ast::Literal::can_cast(node.kind())
694 }
695 };
696 if !ok {
697 fail_match!("Code '{}' isn't of kind {:?}", node.text(), self);
698 }
699 Ok(())
700 }
701}
702
703// If `node` contains nothing but an ident then return it, otherwise return None.
704fn only_ident(element: SyntaxElement) -> Option<SyntaxToken> {
705 match element {
706 SyntaxElement::Token(t) => {
707 if t.kind() == SyntaxKind::IDENT {
708 return Some(t);
709 }
710 }
711 SyntaxElement::Node(n) => {
712 let mut children = n.children_with_tokens();
713 if let (Some(only_child), None) = (children.next(), children.next()) {
714 return only_ident(only_child);
715 }
716 }
717 }
718 None
719}
720
721struct PatternIterator {
722 iter: SyntaxElementChildren,
723}
724
725impl Iterator for PatternIterator {
726 type Item = SyntaxElement;
727
728 fn next(&mut self) -> Option<SyntaxElement> {
729 while let Some(element) = self.iter.next() {
730 if !element.kind().is_trivia() {
731 return Some(element);
732 }
733 }
734 None
735 }
736}
737
738impl PatternIterator {
739 fn new(parent: &SyntaxNode) -> Self {
740 Self { iter: parent.children_with_tokens() }
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use crate::{MatchFinder, SsrRule};
748
749 #[test]
750 fn parse_match_replace() {
751 let rule: SsrRule = "foo($x) ==>> bar($x)".parse().unwrap();
752 let input = "fn foo() {} fn bar() {} fn main() { foo(1+2); }";
753
754 let (db, position, selections) = crate::tests::single_file(input);
755 let mut match_finder = MatchFinder::in_context(&db, position, selections);
756 match_finder.add_rule(rule).unwrap();
757 let matches = match_finder.matches();
758 assert_eq!(matches.matches.len(), 1);
759 assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");
760 assert_eq!(matches.matches[0].placeholder_values.len(), 1);
761 assert_eq!(
762 matches.matches[0].placeholder_values[&Var("x".to_string())]
763 .node
764 .as_ref()
765 .unwrap()
766 .text(),
767 "1+2"
768 );
769
770 let edits = match_finder.edits();
771 assert_eq!(edits.len(), 1);
772 let edit = &edits[0];
773 let mut after = input.to_string();
774 edit.edit.apply(&mut after);
775 assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }");
776 }
777}