Skip to content

Commit 757f755

Browse files
SSR: Match paths based on what they resolve to
Also render template paths appropriately for their context.
1 parent 3975952 commit 757f755

File tree

9 files changed

+482
-61
lines changed

9 files changed

+482
-61
lines changed

crates/ra_ide/src/ssr.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,16 @@ use ra_ssr::{MatchFinder, SsrError, SsrRule};
1111
// A `$<name>` placeholder in the search pattern will match any AST node and `$<name>` will reference it in the replacement.
1212
// Within a macro call, a placeholder will match up until whatever token follows the placeholder.
1313
//
14+
// All paths in both the search pattern and the replacement template must resolve in the context
15+
// in which this command is invoked. Paths in the search pattern will then match the code if they
16+
// resolve to the same item, even if they're written differently. For example if we invoke the
17+
// command in the module `foo` with a pattern of `Bar`, then code in the parent module that refers
18+
// to `foo::Bar` will match.
19+
//
20+
// Paths in the replacement template will be rendered appropriately for the context in which the
21+
// replacement occurs. For example if our replacement template is `foo::Bar` and we match some
22+
// code in the `foo` module, we'll insert just `Bar`.
23+
//
1424
// Placeholders may be given constraints by writing them as `${<name>:<constraint1>:<constraint2>...}`.
1525
//
1626
// Supported constraints:
@@ -47,7 +57,7 @@ pub fn parse_search_replace(
4757
) -> Result<Vec<SourceFileEdit>, SsrError> {
4858
let rule: SsrRule = rule.parse()?;
4959
let mut match_finder = MatchFinder::in_context(db, position);
50-
match_finder.add_rule(rule);
60+
match_finder.add_rule(rule)?;
5161
if parse_only {
5262
return Ok(Vec::new());
5363
}

crates/ra_ssr/src/lib.rs

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ mod matching;
77
mod nester;
88
mod parsing;
99
mod replacing;
10+
mod resolving;
1011
mod search;
1112
#[macro_use]
1213
mod errors;
@@ -21,6 +22,7 @@ use hir::Semantics;
2122
use ra_db::{FileId, FilePosition, FileRange};
2223
use ra_ide_db::source_change::SourceFileEdit;
2324
use ra_syntax::{ast, AstNode, SyntaxNode, TextRange};
25+
use resolving::ResolvedRule;
2426
use rustc_hash::FxHashMap;
2527

2628
// A structured search replace rule. Create by calling `parse` on a str.
@@ -48,18 +50,34 @@ pub struct SsrMatches {
4850
pub struct MatchFinder<'db> {
4951
/// Our source of information about the user's code.
5052
sema: Semantics<'db, ra_ide_db::RootDatabase>,
51-
rules: Vec<parsing::ParsedRule>,
53+
rules: Vec<ResolvedRule>,
54+
scope: hir::SemanticsScope<'db>,
55+
hygiene: hir::Hygiene,
5256
}
5357

5458
impl<'db> MatchFinder<'db> {
5559
/// Constructs a new instance where names will be looked up as if they appeared at
5660
/// `lookup_context`.
5761
pub fn in_context(
5862
db: &'db ra_ide_db::RootDatabase,
59-
_lookup_context: FilePosition,
63+
lookup_context: FilePosition,
6064
) -> MatchFinder<'db> {
61-
// FIXME: Use lookup_context
62-
MatchFinder { sema: Semantics::new(db), rules: Vec::new() }
65+
let sema = Semantics::new(db);
66+
let file = sema.parse(lookup_context.file_id);
67+
// Find a node at the requested position, falling back to the whole file.
68+
let node = file
69+
.syntax()
70+
.token_at_offset(lookup_context.offset)
71+
.left_biased()
72+
.map(|token| token.parent())
73+
.unwrap_or_else(|| file.syntax().clone());
74+
let scope = sema.scope(&node);
75+
MatchFinder {
76+
sema: Semantics::new(db),
77+
rules: Vec::new(),
78+
scope,
79+
hygiene: hir::Hygiene::new(db, lookup_context.file_id.into()),
80+
}
6381
}
6482

6583
/// Constructs an instance using the start of the first file in `db` as the lookup context.
@@ -84,8 +102,16 @@ impl<'db> MatchFinder<'db> {
84102
/// Adds a rule to be applied. The order in which rules are added matters. Earlier rules take
85103
/// precedence. If a node is matched by an earlier rule, then later rules won't be permitted to
86104
/// match to it.
87-
pub fn add_rule(&mut self, rule: SsrRule) {
88-
self.add_parsed_rules(rule.parsed_rules);
105+
pub fn add_rule(&mut self, rule: SsrRule) -> Result<(), SsrError> {
106+
for parsed_rule in rule.parsed_rules {
107+
self.rules.push(ResolvedRule::new(
108+
parsed_rule,
109+
&self.scope,
110+
&self.hygiene,
111+
self.rules.len(),
112+
)?);
113+
}
114+
Ok(())
89115
}
90116

91117
/// Finds matches for all added rules and returns edits for all found matches.
@@ -110,8 +136,16 @@ impl<'db> MatchFinder<'db> {
110136

111137
/// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
112138
/// intend to do replacement, use `add_rule` instead.
113-
pub fn add_search_pattern(&mut self, pattern: SsrPattern) {
114-
self.add_parsed_rules(pattern.parsed_rules);
139+
pub fn add_search_pattern(&mut self, pattern: SsrPattern) -> Result<(), SsrError> {
140+
for parsed_rule in pattern.parsed_rules {
141+
self.rules.push(ResolvedRule::new(
142+
parsed_rule,
143+
&self.scope,
144+
&self.hygiene,
145+
self.rules.len(),
146+
)?);
147+
}
148+
Ok(())
115149
}
116150

117151
/// Returns matches for all added rules.
@@ -149,13 +183,6 @@ impl<'db> MatchFinder<'db> {
149183
res
150184
}
151185

152-
fn add_parsed_rules(&mut self, parsed_rules: Vec<parsing::ParsedRule>) {
153-
for mut parsed_rule in parsed_rules {
154-
parsed_rule.index = self.rules.len();
155-
self.rules.push(parsed_rule);
156-
}
157-
}
158-
159186
fn output_debug_for_nodes_at_range(
160187
&self,
161188
node: &SyntaxNode,
@@ -175,7 +202,7 @@ impl<'db> MatchFinder<'db> {
175202
// we get lots of noise. If at some point we add support for restricting rules
176203
// to a particular kind of thing (e.g. only match type references), then we can
177204
// relax this.
178-
if rule.pattern.kind() != node.kind() {
205+
if rule.pattern.node.kind() != node.kind() {
179206
continue;
180207
}
181208
out.push(MatchDebugInfo {
@@ -185,7 +212,7 @@ impl<'db> MatchFinder<'db> {
185212
"Match failed, but no reason was given".to_owned()
186213
}),
187214
}),
188-
pattern: rule.pattern.clone(),
215+
pattern: rule.pattern.node.clone(),
189216
node: node.clone(),
190217
});
191218
}

crates/ra_ssr/src/matching.rs

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
//! process of matching, placeholder values are recorded.
33
44
use crate::{
5-
parsing::{Constraint, NodeKind, ParsedRule, Placeholder},
5+
parsing::{Constraint, NodeKind, Placeholder},
6+
resolving::{ResolvedPattern, ResolvedRule},
67
SsrMatches,
78
};
89
use hir::Semantics;
@@ -51,6 +52,8 @@ pub struct Match {
5152
pub(crate) rule_index: usize,
5253
/// The depth of matched_node.
5354
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>,
5457
}
5558

5659
/// Represents a `$var` in an SSR query.
@@ -86,7 +89,7 @@ pub(crate) struct MatchFailed {
8689
/// parent module, we don't populate nested matches.
8790
pub(crate) fn get_match(
8891
debug_active: bool,
89-
rule: &ParsedRule,
92+
rule: &ResolvedRule,
9093
code: &SyntaxNode,
9194
restrict_range: &Option<FileRange>,
9295
sema: &Semantics<ra_ide_db::RootDatabase>,
@@ -102,7 +105,7 @@ struct Matcher<'db, 'sema> {
102105
/// If any placeholders come from anywhere outside of this range, then the match will be
103106
/// rejected.
104107
restrict_range: Option<FileRange>,
105-
rule: &'sema ParsedRule,
108+
rule: &'sema ResolvedRule,
106109
}
107110

108111
/// Which phase of matching we're currently performing. We do two phases because most attempted
@@ -117,14 +120,14 @@ enum Phase<'a> {
117120

118121
impl<'db, 'sema> Matcher<'db, 'sema> {
119122
fn try_match(
120-
rule: &ParsedRule,
123+
rule: &ResolvedRule,
121124
code: &SyntaxNode,
122125
restrict_range: &Option<FileRange>,
123126
sema: &'sema Semantics<'db, ra_ide_db::RootDatabase>,
124127
) -> Result<Match, MatchFailed> {
125128
let match_state = Matcher { sema, restrict_range: restrict_range.clone(), rule };
126129
// First pass at matching, where we check that node types and idents match.
127-
match_state.attempt_match_node(&mut Phase::First, &rule.pattern, code)?;
130+
match_state.attempt_match_node(&mut Phase::First, &rule.pattern.node, code)?;
128131
match_state.validate_range(&sema.original_range(code))?;
129132
let mut the_match = Match {
130133
range: sema.original_range(code),
@@ -133,11 +136,19 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
133136
ignored_comments: Vec::new(),
134137
rule_index: rule.index,
135138
depth: 0,
139+
rendered_template_paths: FxHashMap::default(),
136140
};
137141
// Second matching pass, where we record placeholder matches, ignored comments and maybe do
138142
// any other more expensive checks that we didn't want to do on the first pass.
139-
match_state.attempt_match_node(&mut Phase::Second(&mut the_match), &rule.pattern, code)?;
143+
match_state.attempt_match_node(
144+
&mut Phase::Second(&mut the_match),
145+
&rule.pattern.node,
146+
code,
147+
)?;
140148
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+
}
141152
Ok(the_match)
142153
}
143154

@@ -195,6 +206,7 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
195206
self.attempt_match_record_field_list(phase, pattern, code)
196207
}
197208
SyntaxKind::TOKEN_TREE => self.attempt_match_token_tree(phase, pattern, code),
209+
SyntaxKind::PATH => self.attempt_match_path(phase, pattern, code),
198210
_ => self.attempt_match_node_children(phase, pattern, code),
199211
}
200212
}
@@ -311,6 +323,64 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
311323
Ok(())
312324
}
313325

326+
/// Paths are matched based on whether they refer to the same thing, even if they're written
327+
/// differently.
328+
fn attempt_match_path(
329+
&self,
330+
phase: &mut Phase,
331+
pattern: &SyntaxNode,
332+
code: &SyntaxNode,
333+
) -> Result<(), MatchFailed> {
334+
if let Some(pattern_resolved) = self.rule.pattern.resolved_paths.get(pattern) {
335+
let pattern_path = ast::Path::cast(pattern.clone()).unwrap();
336+
let code_path = ast::Path::cast(code.clone()).unwrap();
337+
if let (Some(pattern_segment), Some(code_segment)) =
338+
(pattern_path.segment(), code_path.segment())
339+
{
340+
// Match everything within the segment except for the name-ref, which is handled
341+
// separately via comparing what the path resolves to below.
342+
self.attempt_match_opt(
343+
phase,
344+
pattern_segment.type_arg_list(),
345+
code_segment.type_arg_list(),
346+
)?;
347+
self.attempt_match_opt(
348+
phase,
349+
pattern_segment.param_list(),
350+
code_segment.param_list(),
351+
)?;
352+
}
353+
if matches!(phase, Phase::Second(_)) {
354+
let resolution = self
355+
.sema
356+
.resolve_path(&code_path)
357+
.ok_or_else(|| match_error!("Failed to resolve path `{}`", code.text()))?;
358+
if pattern_resolved.resolution != resolution {
359+
fail_match!("Pattern had path `{}` code had `{}`", pattern.text(), code.text());
360+
}
361+
}
362+
} else {
363+
return self.attempt_match_node_children(phase, pattern, code);
364+
}
365+
Ok(())
366+
}
367+
368+
fn attempt_match_opt<T: AstNode>(
369+
&self,
370+
phase: &mut Phase,
371+
pattern: Option<T>,
372+
code: Option<T>,
373+
) -> Result<(), MatchFailed> {
374+
match (pattern, code) {
375+
(Some(p), Some(c)) => self.attempt_match_node(phase, &p.syntax(), &c.syntax()),
376+
(None, None) => Ok(()),
377+
(Some(p), None) => fail_match!("Pattern `{}` had nothing to match", p.syntax().text()),
378+
(None, Some(c)) => {
379+
fail_match!("Nothing in pattern to match code `{}`", c.syntax().text())
380+
}
381+
}
382+
}
383+
314384
/// We want to allow the records to match in any order, so we have special matching logic for
315385
/// them.
316386
fn attempt_match_record_field_list(
@@ -449,6 +519,28 @@ impl<'db, 'sema> Matcher<'db, 'sema> {
449519
}
450520
}
451521

522+
impl Match {
523+
fn render_template_paths(
524+
&mut self,
525+
template: &ResolvedPattern,
526+
sema: &Semantics<ra_ide_db::RootDatabase>,
527+
) -> Result<(), MatchFailed> {
528+
let module = sema
529+
.scope(&self.matched_node)
530+
.module()
531+
.ok_or_else(|| match_error!("Matched node isn't in a module"))?;
532+
for (path, resolved_path) in &template.resolved_paths {
533+
if let hir::PathResolution::Def(module_def) = resolved_path.resolution {
534+
let mod_path = module.find_use_path(sema.db, module_def).ok_or_else(|| {
535+
match_error!("Failed to render template path `{}` at match location")
536+
})?;
537+
self.rendered_template_paths.insert(path.clone(), mod_path);
538+
}
539+
}
540+
Ok(())
541+
}
542+
}
543+
452544
impl Phase<'_> {
453545
fn next_non_trivial(&mut self, code_it: &mut SyntaxElementChildren) -> Option<SyntaxElement> {
454546
loop {
@@ -578,7 +670,7 @@ mod tests {
578670

579671
let (db, position) = crate::tests::single_file(input);
580672
let mut match_finder = MatchFinder::in_context(&db, position);
581-
match_finder.add_rule(rule);
673+
match_finder.add_rule(rule).unwrap();
582674
let matches = match_finder.matches();
583675
assert_eq!(matches.matches.len(), 1);
584676
assert_eq!(matches.matches[0].matched_node.text(), "foo(1+2)");

crates/ra_ssr/src/parsing.rs

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
use crate::errors::bail;
99
use crate::{SsrError, SsrPattern, SsrRule};
10-
use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, SyntaxToken, T};
10+
use ra_syntax::{ast, AstNode, SmolStr, SyntaxKind, SyntaxNode, T};
1111
use rustc_hash::{FxHashMap, FxHashSet};
1212
use std::str::FromStr;
1313

@@ -16,7 +16,6 @@ pub(crate) struct ParsedRule {
1616
pub(crate) placeholders_by_stand_in: FxHashMap<SmolStr, Placeholder>,
1717
pub(crate) pattern: SyntaxNode,
1818
pub(crate) template: Option<SyntaxNode>,
19-
pub(crate) index: usize,
2019
}
2120

2221
#[derive(Debug)]
@@ -93,16 +92,11 @@ impl RuleBuilder {
9392
placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
9493
pattern: pattern.syntax().clone(),
9594
template: Some(template.syntax().clone()),
96-
// For now we give the rule an index of 0. It's given a proper index when the rule
97-
// is added to the SsrMatcher. Using an Option<usize>, instead would be slightly
98-
// more correct, but we delete this field from ParsedRule in a subsequent commit.
99-
index: 0,
10095
}),
10196
(Ok(pattern), None) => self.rules.push(ParsedRule {
10297
placeholders_by_stand_in: self.placeholders_by_stand_in.clone(),
10398
pattern: pattern.syntax().clone(),
10499
template: None,
105-
index: 0,
106100
}),
107101
_ => {}
108102
}
@@ -171,15 +165,6 @@ impl RawPattern {
171165
}
172166
}
173167

174-
impl ParsedRule {
175-
pub(crate) fn get_placeholder(&self, token: &SyntaxToken) -> Option<&Placeholder> {
176-
if token.kind() != SyntaxKind::IDENT {
177-
return None;
178-
}
179-
self.placeholders_by_stand_in.get(token.text())
180-
}
181-
}
182-
183168
impl FromStr for SsrPattern {
184169
type Err = SsrError;
185170

0 commit comments

Comments
 (0)