Skip to content

Commit 02fc3d5

Browse files
SSR: Refactor to not rely on recursive search for nesting of matches
Previously, submatches were handled simply by searching in placeholders for more matches. That only works if we search all nodes in the tree recursively. In a subsequent commit, I intend to make search not always be recursive recursive. This commit prepares for that by finding all matches, even if they overlap, then nesting them and removing overlapping matches.
1 parent 699619a commit 02fc3d5

File tree

4 files changed

+120
-27
lines changed

4 files changed

+120
-27
lines changed

crates/ra_ssr/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! based on a template.
55
66
mod matching;
7+
mod nester;
78
mod parsing;
89
mod replacing;
910
mod search;
@@ -90,8 +91,10 @@ impl<'db> MatchFinder<'db> {
9091
/// Returns matches for all added rules.
9192
pub fn matches(&self) -> SsrMatches {
9293
let mut matches = Vec::new();
93-
self.find_all_matches(&mut matches);
94-
SsrMatches { matches }
94+
for rule in &self.rules {
95+
self.find_matches_for_rule(rule, &mut matches);
96+
}
97+
nester::nest_and_remove_collisions(matches, &self.sema)
9598
}
9699

97100
/// Finds all nodes in `file_id` whose text is exactly equal to `snippet` and attempts to match

crates/ra_ssr/src/matching.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub struct Match {
4949
pub(crate) placeholder_values: FxHashMap<Var, PlaceholderMatch>,
5050
pub(crate) ignored_comments: Vec<ast::Comment>,
5151
pub(crate) rule_index: usize,
52+
/// The depth of matched_node.
53+
pub(crate) depth: usize,
5254
}
5355

5456
/// Represents a `$var` in an SSR query.
@@ -130,10 +132,12 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
130132
placeholder_values: FxHashMap::default(),
131133
ignored_comments: Vec::new(),
132134
rule_index: rule.index,
135+
depth: 0,
133136
};
134137
// Second matching pass, where we record placeholder matches, ignored comments and maybe do
135138
// any other more expensive checks that we didn't want to do on the first pass.
136139
match_state.attempt_match_node(&mut Phase::Second(&mut the_match), &rule.pattern, code)?;
140+
the_match.depth = sema.ancestors_with_macros(the_match.matched_node.clone()).count();
137141
Ok(the_match)
138142
}
139143

crates/ra_ssr/src/nester.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//! Converts a flat collection of matches into a nested form suitable for replacement. When there
2+
//! are multiple matches for a node, or that overlap, priority is given to the earlier rule. Nested
3+
//! matches are only permitted if the inner match is contained entirely within a placeholder of an
4+
//! outer match.
5+
//!
6+
//! For example, if our search pattern is `foo(foo($a))` and the code had `foo(foo(foo(foo(42))))`,
7+
//! then we'll get 3 matches, however only the outermost and innermost matches can be accepted. The
8+
//! middle match would take the second `foo` from the outer match.
9+
10+
use crate::{Match, SsrMatches};
11+
use ra_syntax::SyntaxNode;
12+
use rustc_hash::FxHashMap;
13+
14+
pub(crate) fn nest_and_remove_collisions(
15+
mut matches: Vec<Match>,
16+
sema: &hir::Semantics<ra_ide_db::RootDatabase>,
17+
) -> SsrMatches {
18+
// We sort the matches by depth then by rule index. Sorting by depth means that by the time we
19+
// see a match, any parent matches or conflicting matches will have already been seen. Sorting
20+
// by rule_index means that if there are two matches for the same node, the rule added first
21+
// will take precedence.
22+
matches.sort_by(|a, b| a.depth.cmp(&b.depth).then_with(|| a.rule_index.cmp(&b.rule_index)));
23+
let mut collector = MatchCollector::default();
24+
for m in matches {
25+
collector.add_match(m, sema);
26+
}
27+
collector.into()
28+
}
29+
30+
#[derive(Default)]
31+
struct MatchCollector {
32+
matches_by_node: FxHashMap<SyntaxNode, Match>,
33+
}
34+
35+
impl MatchCollector {
36+
/// Attempts to add `m` to matches. If it conflicts with an existing match, it is discarded. If
37+
/// it is entirely within the a placeholder of an existing match, then it is added as a child
38+
/// match of the existing match.
39+
fn add_match(&mut self, m: Match, sema: &hir::Semantics<ra_ide_db::RootDatabase>) {
40+
let matched_node = m.matched_node.clone();
41+
if let Some(existing) = self.matches_by_node.get_mut(&matched_node) {
42+
try_add_sub_match(m, existing, sema);
43+
return;
44+
}
45+
for ancestor in sema.ancestors_with_macros(m.matched_node.clone()) {
46+
if let Some(existing) = self.matches_by_node.get_mut(&ancestor) {
47+
try_add_sub_match(m, existing, sema);
48+
return;
49+
}
50+
}
51+
self.matches_by_node.insert(matched_node, m);
52+
}
53+
}
54+
55+
/// Attempts to add `m` as a sub-match of `existing`.
56+
fn try_add_sub_match(
57+
m: Match,
58+
existing: &mut Match,
59+
sema: &hir::Semantics<ra_ide_db::RootDatabase>,
60+
) {
61+
for p in existing.placeholder_values.values_mut() {
62+
// Note, no need to check if p.range.file is equal to m.range.file, since we
63+
// already know we're within `existing`.
64+
if p.range.range.contains_range(m.range.range) {
65+
// Convert the inner matches in `p` into a temporary MatchCollector. When
66+
// we're done, we then convert it back into an SsrMatches. If we expected
67+
// lots of inner matches, it might be worthwhile keeping a MatchCollector
68+
// around for each placeholder match. However we expect most placeholder
69+
// will have 0 and a few will have 1. More than that should hopefully be
70+
// exceptional.
71+
let mut collector = MatchCollector::default();
72+
for m in std::mem::replace(&mut p.inner_matches.matches, Vec::new()) {
73+
collector.matches_by_node.insert(m.matched_node.clone(), m);
74+
}
75+
collector.add_match(m, sema);
76+
p.inner_matches = collector.into();
77+
break;
78+
}
79+
}
80+
}
81+
82+
impl From<MatchCollector> for SsrMatches {
83+
fn from(mut match_collector: MatchCollector) -> Self {
84+
let mut matches = SsrMatches::default();
85+
for (_, m) in match_collector.matches_by_node.drain() {
86+
matches.matches.push(m);
87+
}
88+
matches.matches.sort_by(|a, b| {
89+
// Order matches by file_id then by start range. This should be sufficient since ranges
90+
// shouldn't be overlapping.
91+
a.range
92+
.file_id
93+
.cmp(&b.range.file_id)
94+
.then_with(|| a.range.range.start().cmp(&b.range.range.start()))
95+
});
96+
matches
97+
}
98+
}

crates/ra_ssr/src/search.rs

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,41 @@
11
//! Searching for matches.
22
3-
use crate::{matching, Match, MatchFinder};
3+
use crate::{matching, parsing::ParsedRule, Match, MatchFinder};
44
use ra_db::FileRange;
55
use ra_syntax::{ast, AstNode, SyntaxNode};
66

77
impl<'db> MatchFinder<'db> {
8-
pub(crate) fn find_all_matches(&self, matches_out: &mut Vec<Match>) {
8+
/// Adds all matches for `rule` to `matches_out`. Matches may overlap in ways that make
9+
/// replacement impossible, so further processing is required in order to properly nest matches
10+
/// and remove overlapping matches. This is done in the `nesting` module.
11+
pub(crate) fn find_matches_for_rule(&self, rule: &ParsedRule, matches_out: &mut Vec<Match>) {
912
// FIXME: Use resolved paths in the pattern to find places to search instead of always
1013
// scanning every node.
11-
self.slow_scan(matches_out);
14+
self.slow_scan(rule, matches_out);
1215
}
1316

14-
fn slow_scan(&self, matches_out: &mut Vec<Match>) {
17+
fn slow_scan(&self, rule: &ParsedRule, matches_out: &mut Vec<Match>) {
1518
use ra_db::SourceDatabaseExt;
1619
use ra_ide_db::symbol_index::SymbolsDatabase;
1720
for &root in self.sema.db.local_roots().iter() {
1821
let sr = self.sema.db.source_root(root);
1922
for file_id in sr.iter() {
2023
let file = self.sema.parse(file_id);
2124
let code = file.syntax();
22-
self.slow_scan_node(code, &None, matches_out);
25+
self.slow_scan_node(code, rule, &None, matches_out);
2326
}
2427
}
2528
}
2629

2730
fn slow_scan_node(
2831
&self,
2932
code: &SyntaxNode,
33+
rule: &ParsedRule,
3034
restrict_range: &Option<FileRange>,
3135
matches_out: &mut Vec<Match>,
3236
) {
33-
for rule in &self.rules {
34-
if let Ok(mut m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
35-
// Continue searching in each of our placeholders.
36-
for placeholder_value in m.placeholder_values.values_mut() {
37-
if let Some(placeholder_node) = &placeholder_value.node {
38-
// Don't search our placeholder if it's the entire matched node, otherwise we'd
39-
// find the same match over and over until we got a stack overflow.
40-
if placeholder_node != code {
41-
self.slow_scan_node(
42-
placeholder_node,
43-
restrict_range,
44-
&mut placeholder_value.inner_matches.matches,
45-
);
46-
}
47-
}
48-
}
49-
matches_out.push(m);
50-
return;
51-
}
37+
if let Ok(m) = matching::get_match(false, rule, &code, restrict_range, &self.sema) {
38+
matches_out.push(m);
5239
}
5340
// If we've got a macro call, we already tried matching it pre-expansion, which is the only
5441
// way to match the whole macro, now try expanding it and matching the expansion.
@@ -60,14 +47,15 @@ impl<'db> MatchFinder<'db> {
6047
// i.e. we don't want to match something that came from the macro itself.
6148
self.slow_scan_node(
6249
&expanded,
50+
rule,
6351
&Some(self.sema.original_range(tt.syntax())),
6452
matches_out,
6553
);
6654
}
6755
}
6856
}
6957
for child in code.children() {
70-
self.slow_scan_node(&child, restrict_range, matches_out);
58+
self.slow_scan_node(&child, rule, restrict_range, matches_out);
7159
}
7260
}
7361
}

0 commit comments

Comments
 (0)